UKOnline

Pointeur

On a vu au début de ce livre qu'une variable est caractérisée par trois éléments : son nom, son type et sa valeur (c'est-à-dire la donnée qu'elle stocke). Une variable est stockée dans la mémoire de l'ordinateur pendant l'exécution d'un programme. La mémoire peut être simplement vue comme une suite d'octets, et chaque octet possède une adresse qui permet de l'identifier. Voyons par exemple l'instruction suivante :

Lors de l'exécution, une zone dans le mémoire va être attribuée à cette variable. Le nom a de la variable est utilisé pour faire référence à cette zone mémoire, qui possède une certaine adresse, par exemple 2000. La valeur de la variable est 17, c'est-à-dire que la zone mémoire correspondant contient la donnée 17.

La figure 1 illustre cela. On indique sur la gauche les adresses en mémoire, et on voit que la variable a est bien associée à la zone mémoire qui commence à l'octet d'adresse 2000 et qui se termine avec l'octet d'adresse 2003 (un int occupant 4 octets en mémoire).

Variable en mémoire
Exemple d'une variable int a initialisée à 17 dans la mémoire.

Le type pointeur

Un pointeur est une variable qui stocke une adresse. Par exemple, on pourrait vouloir stocker l'adresse de la variable a de l'exemple qu'on vient de voir à la figure 1. Pour cela, il nous faut une variable de type pointeur vers un int. On écrit cela en faisant suivre int par un astérisque ( * ). Déclarons une telle variable :

La variable p est donc une variable de type int*. Remarquez qu'on a collé l'astérisque au nom de la variable plutôt qu'au type. On aurait pu écrire int* p, mais par convention on préfère coller l'astérisque au nom de la variable.

Ce qu'il faut donc bien comprendre est qu'un pointeur est une variable comme une autre, et la donnée qu'elle peut contenir est une adresse d'une zone mémoire. Une variable de type pointeur occupe donc également de la place en mémoire, plus exactement 8 octets. La situation en mémoire est donc maintenant celle illustrée sur la figure 2. La variable a est toujours là, et on voit en plus la variable p qui occupe les 8 octets à partir de celui à l'adresse 2004 jusqu'à celui à l'adresse 2011, compris.

Pointeur en mémoire
Une variable a de type int initialisée à 17 et une variable p de type int* (pointeur) non-initialisée.

Dans l'exemple qu'on a vu, on a donc déclaré la variable pointeur avec le type int*. Ce type signifie en fait que la variable est un pointeur et contient donc une adresse, mais également que à cette adresse se trouve stocké une donnée de type int. On peut évidemment créer des pointeurs d'autres types comme ceux montrés ci-dessous :

Opérateur d'adresse &

On peut connaitre l'adresse d'une variable grâce à l'opérateur d'adresse. Cet opérateur, noté &, est à utiliser suivi d'un nom de variable et renvoie l'adresse de cette variable. On peut ensuite stocker cette adresse dans une variable de type pointeur. On pourrait donc écrire :

La seconde instruction récupère donc l'adresse de la variable a et la stocke dans la variable p. On se retrouve donc avec la situation de la figure 3. Les adresses sont des nombres entiers positifs sur 8 octets. Par exemple, si on exécutait les deux lignes de code d'au-dessus et qu'on affichait la valeur de la variable p, on aurait par exemple :

0x7fff59b284f8

On peut afficher la valeur d'un pointeur avec la fonction printf en utilisant la balise %p. Il s'agit évidemment de la représentation en hexadécimal de l'adresse. La représentation décimale de l'adresse correspondante est 140734698259704.

Pointeur initialisé en mémoire
Une variable a de type int initialisée à 17 et une variable p de type int* (pointeur) contenant l'adresse de a.

Opérateur de déréférencement *

On peut consulter la valeur qui se trouve dans la mémoire à une certaine adresse en utilisant l'opérateur de déréférencement. Cet opérateur, noté *, est à utiliser suivi d'une variable de type pointeur et renvoie la valeur se trouvant à l'adresse stockée dans le pointeur. On peut ensuite stocker cette valeur dans une variable du bon type. On pourrait donc écrire :

La troisième instruction déclare une nouvelle variable b de type int. Ensuite, elle affecte à cette variable la valeur qui se trouve en mémoire, à l'adresse qui est contenue dans la variable p. On lit donc l'opération *p comme « la valeur qui se trouve en mémoire à l'adresse stockée dans la variable p ». La figure 4 montre l'état de la mémoire après exécution de ces trois instructions.

Déréférencement
La variable b reçoit la valeur se trouvant à l'adresse stockée dans la variable p.

L'opérateur de déréférencement ne peut s'appliquer sur sur une variable de type pointeur. Si vous tentez de l'appliquer sur un autre type de variable, cela produira une erreur lors de la compilation.

Pointeur de pointeur

Un pointeur est également une variable, et il possède donc aussi une adresse. On peut vouloir stocker cette adresse dans un pointeur. Comme il s'agit d'une variable pointeur, il faudra donc utiliser un *, et l'adresse qui y sera stockée sera celle d'une variable de type pointeur, par exemple int*. L'exemple suivant montre l'utilisation d'un pointeur de pointeur :

La figure 5 montre les différentes variables en mémoire. Une variable est de type int et les deux autres sont des pointeurs. La première des deux pointe vers un int et la seconde pointe vers un int*.

Pointeur de pointeur
La variable ap est un pointeur vers la variable p.

Retour sur le type pointeur

Terminons cette section en examinant de nouveau le type pointeur qu'on utilise pour déclarer les variables de type pointeur. On peut placer l'astérisque où on veut lors de la déclaration :

Ces deux notations sont similaires, mais se lisent différemment apportant ainsi une meilleure compréhension des pointeurs. La première déclaration, la plus couramment utilisée, se lit comme « La valeur se trouvant à l'adresse stockée dans le pointeur a est de type int ». La deuxième déclaration se lit « La variable b est de type pointeur de int ».

La figure 6 montre deux déclarations de variables de type pointeur. On peut voir que la variable value est de type pointeur car il y a un * contre le nom de la variable. La différence entre les deux variables est que la première pointe vers un int alors que la seconde pointe vers un int*, à savoir un pointeur de int.

Type pointeur
Le type pointeur.

Pointeur NULL et pointeur générique

Il y a deux pointeurs particuliers que l'on peut utiliser dans certaines situations spécifiques. Le pointeur NULL est celui qui ne pointe vers aucune zone mémoire. On peut voir ce pointeur comme l'adresse zéro. On le note simplement avec la constante NULL. Voici un exemple de code qui utilise un tel pointeur NULL :

L'exécution de ces deux instructions va afficher l'adresse qui est stockée dans la variable p, à savoir le pointeur NULL. Notez bien la différence entre une variable de type pointeur non-initialisée et une variable pointeur initialisé à NULL.

0x0

Il y a aussi le pointeur générique qui est un type particulier de pointeur qui permet de stocker un pointeur vers n'importe quel type de donnée. Il se note void* et peut donc être converti vers n'importe quel autre type de pointeur, et ces derniers peuvent être convertis vers lui. On peut donc par exemple écrire les instructions suivantes :

On récupère donc l'adresse de la variable c que l'on stocke dans le pointeur générique g. Il s'agit donc d'une variable pouvant stocker n'importe quel pointeur. L'instruction suivante converti ensuite le pointeur générique en un char*. Les pointeurs g et p possèdent exactement la même valeur, à savoir l'adresse de la variable c.

0x7fff582c64fb
0x7fff582c64fb