Les Pointeurs Du C
Les Pointeurs Du C
Les Pointeurs Du C
Introduction :
Les pointeurs vous harcèlent, vous hantent, vous terrorisent, ce document est fait pour
vous. Il a pour but d'aider les débutants en C/C++ à aborder les pointeurs avec le moins
d'appréhension possible. Avant de rentrer dans le vif du sujet, nous ferons un rappel sur
la définition d’une variable.
Rappel :
Une variable est destinée à contenir une valeur du type avec lequel elle est déclarée.
Physiquement cette valeur se situe en mémoire.
Prenons comme exemple un entier nommé x :
Nous allons représenter la mémoire de l'ordinateur par des cases numérotées en ordre
croissant. Le numéro de la case étant ce qu'on appellera son adresse. On considérera
qu'une variable utilise une case, même si dans la réalité elles ne prennent pas la même
quantité de mémoire selon leur type.
Voyons-en une partie sous forme d'un schéma :
Je réserve une case pour la variable x dans la mémoire, case numéro 62 dans le cas du
schéma.
J'écris la valeur 10 dans l'emplacement réservé pour x. On dit que x a pour valeur 10.
Cette valeur est située physiquement à l'emplacement &x (adresse de x) dans la mémoire
(62 dans le contexte du schéma).
Pour obtenir l'adresse d'une variable on fait précéder son nom avec l'opérateur '&'
(adresse de) :
printf("%p",&x);
Le pointeur :
Un pointeur est aussi une variable, il est destiné à contenir une adresse mémoire, c'est à
dire une valeur identifiant un emplacement en mémoire. Pour différencier un pointeur
d'une variable ordinaire, on fait précéder son nom du signe '*' lors de sa déclaration.
Poursuivons notre exemple :
Décomposons :
printf("%d",*px);
On se rend vite compte qu'un pointeur doit être initialisé avec une adresse valide, c'est à
dire qui a été réservée en mémoire (allouée) par le programme pour être utilisé.
Imaginez-vous l'instruction précédente, si nous n'avions pas initialisé le pointeur avec
l'adresse de x, l'écriture se ferait en un lieu indéterminé de la mémoire.
Dans l'exemple précédent vous avez dû remarquer que nous avons donné un type au
pointeur (int *), même si dans un système donné un pointeur a toujours la même taille (4
octets pour un système à adressage sur 32 bits), le langage impose de leur donner un
type. Si vous ne savez pas à l'avance sur quel type de données il va pointer vous pouvez lui
donner le type pointeur void.
Il est d'usage de préfixer le nom des variables de type pointeur de la lettre "p" ceci pour
une meilleure lisibilité du code.
Pointeurs et tableaux :
int Tab[10]={5,8,4,3,9,6,5,4,3,8};
L'instruction suivante affiche bien la valeur du premier élément du tableau par pointeur
déréférencé.
printf("%d",*Tab);
Ceci nous montre que le nom d'un tableau peut trés bien s'utiliser comme un pointeur sur
son premier élément. On peut alors tout à fait déclarer un pointeur et l'initialiser avec le
nom du tableau. A condition bien sûr qu'il soit du même type, pointeur sur des entiers
dans notre cas :
int *pTab;
pTab = Tab;
On peut donc se servir de ce pointeur comme s'il était un tableau et des opérateurs
crochets [] pour accéder à ses éléments :
printf("%d",pTab[0]); // Affiche 5.
Mais ce n'est pas un autre tableau c'est le même que Tab, il référence le même
emplacement en mémoire :
pTab[0]++;
printf("%d",Tab[0]); // Affiche 6.
printf("%d",Tab[10]);
J'ai l'affichage d'une valeur ne faisant pas partie de mon tableau. Ceci est dû au fait que je
vais lire une valeur en dehors des limites du tableau sans que le système signale une
erreur. L'opérateur crochet n'étant qu'une écriture simplifiée du pointeur déréférencé,
ceci :
printf("%d",Tab[10]);
printf("%d",*(Tab+10));
Reprenons le cas de notre tableau Tab. De même que pTab, puisqu'initialisé avec Tab,
C'est un pointeur sur son premier élément. Si j'incrémente le pointeur pTab il ne
contiendra pas l'adresse du premier élément du tableau + 1, mais l'adresse de l'élément
suivant. La valeur de l'adresse qu'il contient sera donc incrémentée de la taille du type
qu'il référence. Ceci est l'une des raisons pour lesquelles il faut donner un type à un
pointeur.
Donc si j'écris :
printf("%d",*(Tab+1)); // Affiche 8.
pTab++;
printf("%d\n",pTab[0]);
J'ai aussi l'affichage du deuxième élément du tableau d'origine, puisque mon pointeur
ayant été incrémenté d'une unité, il contient maintenant l'adresse du deuxième élément
du tableau.
On peut de la même façon décrémenter un pointeur, lui ajouter ou lui soustraire une
valeur numérique entière (attention toutefois à ne pas sortir des zones de mémoire
allouées). Donc si j'écris :
pTab = Tab+4;
printf("%d\n",pTab[0]); // Affiche 9.
J'ai l'affichage du 5ème élément du tableau car pTab est maintenant un pointeur sur son
5ème élément.
Une autre utilité des pointeurs est de permettre à des fonctions d'accéder aux données
elles même et non à des copies. Prenons pour exemple une fonction qui échange la valeur
de deux entiers :
Pour que la fonction puisse affecter leurs nouvelles valeurs à chaque variable elle doit y
avoir accès, le passage des variables par pointeur permet donc à la fonction d'accéder aux
variables par pointeur déréférencé.
Voyons l'exemple d'utilisation de la fonction :
int a;
int b;
a = 5;
b = 10;
printf("a = %d\nb = %d\n",a,b);
exchange(&a, &b);
printf("a = %d\nb = %d\n",a,b);
Le passage de paramètres sous forme de pointeur est aussi utilisé pour passer un tableau
en tant que paramètre de fonction, c'est d'ailleurs la seule solution possible dans le cas
d'un tableau. La fonction reçoit donc un pointeur sur le premier élément du tableau, mais
la fonction ne devant pas accéder en dehors des limites du tableau, elle doit pouvoir en
contrôler le traitement dans ce cas. Pour une chaîne de caractères, on peut tester la
présence du caractère de fin de chaîne '\0', mais dans la majorité des autres cas il faudra
fournir à la fonction la taille du tableau. Prenons comme exemple une fonction qui
retourne la plus grande valeur d'un tableau d'entier :
if(tab[x]>nmax) nmax=tab[x];
return nmax;
}
Nous lui fournissons donc l'adresse du premier élément du tableau comme premier
paramètre et la taille du tableau en second paramètre. Dans l'implémentation de la
fonction nous utilisons le pointeur comme s'il était un tableau d'entier (utilisation de
l'opérateur crochet "[]" pour accéder à ses éléments). Voici un code d'utilisation de la
fonction max :
ou
Ces écritures étant équivalentes à la première, la fonction recevra dans tous les cas une
copie du pointeur sur le tableau.
Conclusion :
Ce document n'ayant pour but que d'aborder les pointeurs, j'espère tout de même qu'il
vous aura un peu aidé à leurs compréhensions. Il faut bien sûr ne les utiliser que quand
cela est nécessaire. En C++ d'autre type de données permettent d'en faire abstraction.
Signalons aussi que l'API Windows utilise abondamment les pointeurs comme paramètres
de fonctions. Les pointeurs de fonctions y sont utilisés pour aller chercher des fonctions
incluses dans les librairies dynamiques (dll).