Tutoriel C - Les pointeurs

 

Préface

Les pointeurs sont quasiment toujours LE point noir des débutants en C, et ce malgré la quantité de tutoriaux sur le sujet. En effet il s'agit de quelque chose qui n'a pas d'équivalent dans beaucoup de langages, et qui est donc un peu plus compliquée à assimiler.

Je vais essayer de rassembler dans ce tutoriel tout ce qui m'a enfin permit de comprendre comment ça fonctionnait. Les explications ne seront peut-être pas des plus rigoureuses, mais l'important c'est que ça marche, non ? ;)

 

Rappels

Avant de parler de pointeurs il va falloir parler des variables qui ne sont pas des pointeurs. Une variable en C est tout simplement un endroit réservé dans la mémoire, et destiné à recevoir une certaine quantité de valeurs. Par exemple le fait d'écrire :

char variable;

Attribue dans la mémoire 1 octet (char = 1 octet) qui pourra donc contenir une valeur. Mais cet emplacement mémoire qui contient une valeur a une adresse. L'adresse est comme son nom l'indique ce qui permet de localiser la variable dans la mémoire. C'est donc également un nombre, mais bien évidement différent de la valeur contenue. À noter aussi que la "taille" de cette adresse est fixe, les adresses de char ne sont pas plus petites que celles de long.
En clair, quand vous déclarez une variable comme nous venons de le faire, peu importe son nom qui disparaitra à la compilation; seule son adresse compte.
Nous voici donc avec nos variables qui sont uniquement représentée par une certaine quantité d'octets à une adresse de la mémoire, ces octets représentant une valeur. En reprenant notre exemple, on accede à la valeur avec "variable", et à l'adresse avec "&variable".

Rappellez vous aussi que :

variable = 5;

est valide, mais par contre :

&variable = 654898;

Ne l'est pas ! On ne peut pas modifier ainsi l'adresse d'une variable. Une fois qu'elle est déclarée, elle a une adresse fixe que l'on ne peut pas changer. C'est à peu près tout ce qu'il y a à dire, avant de passer cette fois aux pointeurs.

 

Banalités

Un pointeur est une variable également, mais une variable un peu particulière. En effet au lieu de contenir une valeur ordinaire, elle contient l'adresse d'une autre variable. Par exemple :

char *pointeur,variable;
pointeur = &variable;

On fixe ainsi la valeur de notre variable "pointeur" à l'adresse de la variable "variable". On a alors un pointeur qui "pointe" sur la variable "variable". Notez qu'une erreur fréquente est d'écrire :

char *pointeur,variable;
pointeur = variable;

Ceci aura pour effet de faire pointer "pointeur" sur une zone de la mémoire qui correspond à la valeur de "variable", or à priori il y a toutes les chances pour que cette zone de mémoire soit occupée par autre chose. On a là un pointeur qui pointe vers une adresse inconnue.
Je m'explique. Admettons que "variable" contienne la valeur 200. Alors, écrire "pointeur = variable" fera pointer notre "pointeur" sur l'adresse 200, qui peut à priori contenir n'importe quoi : nous n'en savont rien.
Une fois que nous avons correctement indiqué au pointeur de pointer sur l'adresse de la variable "variable". Il est alors possible de modifier "variable" en passant par son pointeur, et en utilisant "*" :

char *pointeur,variable;
pointeur = &variable;
*pointeur = 3;

Le fait d'utiliser "*pointeur" signifie qu'on ne modifie pas la valeur de "pointeur", mais la valeur de la variable sur laquelle "pointeur" est en train de pointer. En clair ce n'est pas "pointeur" qui est modifiée mais "variable". En fait, "pointeur" contient l'adresse à laquelle est stoquée la valeur de "variable", donc en utilisant "*pointeur" on modifie les données situées à cette adresse.
Retenez que la valeur de "pointeur" et l'adresse à laquelle pointe "pointeur" sont deux termes qui désignent exactement la même chose, puisque que la valeur d'un pointeur est une adresse.

J'ai bien l'impression que ce passage est légerement indigeste, alors résumons :
Quand on fait "pointeur = &variable", on lie en quelque sortes le pointeur à cette variable, ce qui signifie qu'on pourra modifier "variable" par l'intermédiaire de "pointeur".

Une image vaut parfois mieux que des pages d'explications, alors voilà un petit shéma avec des valeurs bidon (pour clarifier, les adresse seront précedées de # bien qu'il s'agisse également de nombres).

char *pointeur; // Adresse : #500 (ne changera jamais), Valeur : inconnue (non définie pour l'instant)
char variable; // Adresse : #800 (ne changera jamais), Valeur : inconnue (non définie pour l'instant)
pointeur = &variable; // Valeur de "pointeur" : #800 (l'adresse de "variable").
*pointeur = 5; // Valeur de "pointeur" : #800, Valeur contenue à l'adresse "#800" (qui correspond à "variable") : 5

Evitez donc à tout prix d'utiliser *pointeur sans être sûr de connaitre l'endroit où vont "atterir" vos données. Dans notre exemple, regardez la ligne 3. Si nous avions utilisé "*pointeur = &variable", le programme aurait très certainement buggé. Pourquoi ? Simplement parceque pour l'instant, la valeur de "pointeur" n'a pas été définie, "pointeur" pointe n'importe ou, et donc stoquer une valeur dans "*pointeur" revient à écrire n'importe ou dans la mémoire, de manière totalement aléatoire.

Maintenant que vous avez les explications, voici un autre exemple un peu plus complexe :

char *pointeur,variable1,variable2;
pointeur = &variable1;
*pointeur = 3;
pointeur = &variable2;
*pointeur = 5;

En apparence, nous n'avons touché qu'à la variable pointeur. Pourtant ce code modifie les deux variable "variable1" et "variable2". Pourquoi ?
Simplement parcequ'à la 2eme ligne, on fait pointer "pointeur" sur la première variable (on stoque l'adresse de "variable1" dans "pointeur"). On modifie ensuite la valeur pointée par "pointeur" (qui est donc "variable1"). "variable1" vaut alors 3.
Puis on change la cible de "pointeur" en le faisant pointer cette fois sur "variable2". On modifie également la valeur de la variable pointée (cette fois, il s'agit de "variable2"). Et à la fin de l'appel les deux variables ont donc été modifiées.

Vous commencez peut-être à vous douter de l'interet des pointeurs : modifier la valeur située à l'adresse vers laquelle il pointe peut modifier plusieurs variables différentes, suivant le contexte. He bien il ne s'agit que de l'un des interets des pointeurs, puisqu'il y en a en fait beaucoup d'autres qui les rendent indispensables dès que l'on veut aborder des programmes sophistiqués.

 

Pointeurs en parametre

Passons à un problème un peu plus courant. Vous voulez une fonction qui prennent en parametre deux variables, et qui échange leurs valeurs. En géneral le débutant essaye une fonction comme ceci :

void Echange(short variable1,short variable2)
{
short temporaire;
temporaire = variable1;
variable1 = variable2;
variable2 = temporaire;
}

Mais cela ne risque pas de marcher : quand vous appellez cette fonction, les parametres "variable1" et "variable2" sont fixés aux deux valeurs que nous voulions échanger. Puis "variable1" et "variable2" sont échangées, et la fonction est terminée. Les deux variables que nous voulions échanger n'ont absolument pas été modifiées, seules "variable1" et "variable2" l'ont été.

Il suffit alors d'utiliser les pointeurs, pour pointer vers les variables à échanger et les modifier elles, pas les parametres. Voici la fonction :

void Echange(short *variable1,short *variable2)
{
short temporaire;
temporaire = *variable1;
*variable1 = *variable2;
*variable2 = temporaire;
}

Vous n'avez plus qu'à utiliser Echange(&a,&b) pour échanger les valeurs contenues dans les variables a et b (à supposer que a et b soient de type short, puisque notre fonction fonctionne avec des shorts). Attention à ne pas appeller Echange(a,b), nous avons déjà vu cette erreur plus haut : cela enregistre dans les pointeurs les valeurs de a et de b au lieu de leurs adresses, et a donc toutes les chances d'écrire n'importe ou en mémoire.

Comment est-ce que cette fonction procede ?
En appellant la fonction, les deux parametres sont les adresses des variables à échanger. On déclare donc comme argument de la fonction deux pointeurs, qui pointeront donc chacun sur une des valeurs. Il suffit alors de modifier les valeurs pointées en utilisant "*variable1" et "*variable2". Attention à ne pas utiliser "variable1" et "variable2", qui ne représentent pas les valeurs des variables passées en parametre, mais leurs adresses.

 

Pointeurs et tableaux

Vous le saviez peut-être, mais les tableaux en C ne sont que des pointeurs. Quand vous déclarez un tableau comme ceci :

char table[10];

"table" n'est en fait qu'un pointeur qui pointe vers la 1ere valeur du tableau, les 9 autres sont tout simplement rangées à la suite. Dans cet exemple, si l'adresse de l'élement [0] du tableau est 400, alors celle de l'élement 1 est 401, celle de l'élement 2 est 402, et ainsi de suite.
D'ailleurs au lieu d'écrire table[4], vous pouvez écrire *(table + 4), cela revient exactement au même : vous vous réferez à la valeur située à l'adresse "table + 4", c'est à dire "400 + 4", qui correspond à l'élement [4] du tableau.

Notez quand même une chose importante : ceci est vrai parceque table est un tableau de char, et que donc chacun des élements prends 1 octet. Dans le cas d'un tableau de short par exemple, si l'élement [0] est à l'adresse 400, l'élement [1] est à l'adresse 402.
Par contre, vous devez toujours faire *(table + 4) et non pas *(table + 8) pour acceder à l'élement [4], car comme table est de type short, le fait d'ajouter 4 ajoute en fait 4 * sizeof(short) à l'adresse, c'est à dire 8 (un short fait 2 octets).

 

Pointeurs et chaines

Oui, les chaines sont aussi des pointeurs :)
Plus précisement ce sont des tableaux. Ecrire :

unsigned char str[] = "Hello, World!";

est équivalent à :

unsigned char str[] = {'H','e','l','l','o',',',' ','W','o','r','l','d','!',0};

Remarquez d'ailleurs le 0 à la fin de la déclaration sous forme de tableau. En effet une chaine est toujours terminée par un zéro, et c'est pourquoi l'écriture :

unsigned char str[3] = "aaa";

Pose problème si vous utilisez des fonctions de gestion de chaines : il n'y a pas la place de mettre ce 0, donc une fonction comme DrawStr va lire la chaine après le 3eme 'a', sans s'arrêter.
Après cette petite parenthèse, revenons aux chaines. Maintenant que vous savez qu'il s'agit de pointeurs, vous pouvez deviner comment les passer en argument : dans notre exemple il suffit tout simplement de passer 'str' en parametre, et d'indiquer à la fonction qu'il s'agit d'un pointeur sur un char. Ainsi *str indique le 1er caractère de la chaine, *(str + 1) le second, etc...
Comme autre exemple, voilà une fonction qui dessiner une chaine caractère par caractère. Ceci n'a pas vraiment d'autre interet qu'utiliser les informations de ce paragraphe, puisqu'utiliser DrawStr est plus optimisé.

void MyDrawStr(short x,short y,unsigned char *str)
{
while (*str)
{
DrawChar(x,y,*str++);
x += 10; // En admettant que chaque caractère fasse 10 pixels de largeur
}
}

Cette fonction teste le caractère pointé (donc, au premier passage, il s'agit du 1er caractère de la chaine), et stoppe la boucle si il est nul. Elle dessine ensuite le caractère et incrémente le pointeur, ce qui a pour effet de décaler le pointeur sur le caractère suivant. Elle modifie ensuite la coordonnée X pour le prochain caractère, et teste à nouveau...
Tous les caractères seront donc affichés un par un, à 10 pixels d'intervale, jusqu'à ce que l'on arrive au caractère nul. Voilà pourquoi il est si important, en son absence il est impossible de savoir où la chaine se termine.

 

Pointeurs de pointeurs, tableaux de pointeurs

- Fin du tutoriel

 

Vertyos