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