Les pointeurs contiennent des adresses, et les adresses sont des nombres - il est donc raisonnable d'attendre que l'arithmétique fonctionne sur eux. C'est le cas, mais pas tout à fait comme l'arithmétique entière ordinaire. Le C met chaque opération sur pointeur à l'échelle de la taille de ce qui est pointé, ce qui transforme « ajouter un » en un bien plus utile « passer à l'élément suivant ».
Cette unique décision de conception explique pourquoi parcourir un tableau avec un pointeur est aussi naturel en C que le parcourir avec un indice.
Ajouter 1 déplace d'un objet, pas d'un octet
Prenez un pointeur, ajoutez 1, et affichez les deux adresses :
Le int * saute de 4 octets, le double * de 8, le char * de 1 - chacun atterrit exactement là où commencerait l'objet suivant de son type. La règle est simple :
ptr + n == (adresse dans ptr) + n * sizeof(*ptr)
C'est exactement pourquoi un pointeur a un type. Une adresse nue sans type attaché n'aurait aucune idée de la distance jusqu'au « suivant ».
Parcourir un tableau par pointeur
Voici le bénéfice. Les éléments d'un tableau occupent de la mémoire consécutive, donc faire avancer un pointeur les visite dans l'ordre :
Les deux boucles affichent les mêmes cinq nombres. Dans la seconde, scores se dégrade en pointeur sur son premier élément, p++ avance d'un int, et *p lit l'élément qui s'y trouve. Disposé en mémoire :
scores[0] scores[1] scores[2] scores[3] scores[4]
+---------+---------+---------+---------+---------+
| 88 | 92 | 75 | 60 | 100 |
+---------+---------+---------+---------+---------+
^ ^ ^
p p+1 scores+5
(debut) (4 octets plus loin) (un apres la fin)
La relation entre les deux formes est exacte et mérite d'être mémorisée : scores[i] est défini comme *(scores + i). Plus de détails dans pointeurs et tableaux.
Incrémentation, décrémentation et le piège de *p++
++ et -- fonctionnent sur les pointeurs avec la même mise à l'échelle.
L'expression qui déroute tout le monde est *p++. Le ++ postfixe s'attache plus fort que le *, donc cela signifie « prendre le p courant, avancer p, puis déréférencer l'ancienne valeur » :
Trois expressions, trois sens différents :
| Expression | Pointeur après | Valeur produite |
|---|---|---|
*p++ | avancé | l'élément d'avant le déplacement |
*++p | avancé | l'élément d'après le déplacement |
(*p)++ | inchangé | l'ancienne valeur, et l'élément est incrémenté |
*p++ est idiomatique en C - c'est ainsi que s'écrivent les copies de chaînes et de tampons - mais écrivez les parenthèses dès que vous voulez dire autre chose.
Soustraire deux pointeurs
Soustraire un pointeur d'un autre donne le nombre d'éléments qui les séparent, pas le nombre d'octets :
Le type du résultat est ptrdiff_t, un type entier signé de <stddef.h>, affiché avec %td. Soustraire des pointeurs dans deux tableaux différents produit un résultat indéfini - la norme ne définit l'opération qu'à l'intérieur d'un objet.
Vous pouvez aussi soustraire un entier d'un pointeur (p - 3 recule de trois éléments), mais vous ne pouvez pas additionner deux pointeurs. « La somme de deux adresses » ne nomme rien.
Comparer des pointeurs
Les opérateurs relationnels fonctionnent sur des pointeurs dans le même tableau, et ils signifient exactement ce que la disposition mémoire suggère : p < q est vrai quand p adresse un élément antérieur.
front < back est toute l'histoire de la terminaison de la boucle, et c'est correct parce que les deux pointeurs restent dans un seul tableau. == et != sont aussi la façon naturelle de tester une boucle contre son marqueur de fin, ce qui nous amène à la règle qui rend tout cela légal.
La règle du un-après-la-fin
Le C autorise explicitement à former un pointeur vers la position située juste après le dernier élément d'un tableau. Vous pouvez le calculer, le stocker et le comparer. Vous ne pouvez pas le déréférencer.
int arr[4];
arr[0] arr[1] arr[2] arr[3] (aucun element)
+------+ +------+ +------+ +------+ +- - - -+
| | | | | | | | | |
+------+ +------+ +------+ +------+ +- - - -+
^ ^
arr arr + 4
utilisable formable et comparable
jamais dereferencable
Cette règle est ce qui rend la forme de boucle standard valide :
for (int *p = arr; p != arr + n; p++) {
/* ... */
}
À la dernière itération, p devient arr + n, la comparaison échoue, et la boucle sort - sans jamais lire cette position.
Deux choses restent malgré tout indéfinies. Former un pointeur deux éléments après la fin, ou un avant le début, est un comportement indéfini même si vous ne le déréférencez jamais :
int *bad1 = arr + n + 1; // indefini - trop loin apres la fin
int *bad2 = arr - 1; // indefini - avant le debut
Ce second cas compte en pratique : une boucle à rebours écrite for (int *p = arr + n - 1; p >= arr; p--) calcule arr - 1 à sa dernière décrémentation. Cela fonctionne sur tous les compilateurs courants et reste formellement indéfini. La boucle à rebours propre l'évite :
Les pointeurs void et l'arithmétique interdite
Un void * contient une adresse sans type attaché, c'est ainsi que malloc et qsort restent génériques. Parce qu'il n'y a pas de taille d'élément, l'arithmétique de pointeurs sur un void * n'est pas autorisée par la norme - convertissez d'abord vers un type concret (ou vers char * pour le travail au niveau de l'octet).
void *v = buffer;
// v + 1; // pas du C standard - aucune taille pour l'echelle
char *b = v;
b + 1; // ok - un octet plus loin
GCC et Clang acceptent l'arithmétique sur void * comme une extension qui le traite comme un char *, donc le code qui en dépend compile jusqu'au jour où il est construit quelque part de plus strict. Compilez avec -std=c17 -pedantic si vous voulez être prévenu.
Ce que cela vous apporte
L'arithmétique de pointeurs n'est pas une astuce d'optimisation à sortir occasionnellement - c'est le mécanisme sous l'indexation de tableaux, la manipulation de chaînes et chaque parcours de tampon de la bibliothèque standard. strlen, c'est un pointeur avancé jusqu'au '\0' terminal puis soustrait du début. memcpy, ce sont deux pointeurs avançant au même pas.
Ensuite, voyez comment l'équivalence tableau/pointeur fonctionne réellement, y compris le piège de sizeof qui attrape tout le monde la première fois qu'un tableau est passé à une fonction : pointeurs et tableaux.
Questions fréquentes
Que fait ptr + 1 en C ?
Il avance le pointeur d'un objet, pas d'un octet. Pour un int * sur une machine typique, c'est 4 octets ; pour un double *, c'est 8. Le compilateur multiplie par sizeof(*ptr) pour vous, c'est pourquoi le type du pointeur compte.
Peut-on soustraire deux pointeurs en C ?
Oui, quand les deux pointent dans le même tableau. end - start donne le nombre d'éléments qui les séparent, de type ptrdiff_t (à afficher avec %td). Soustraire des pointeurs vers des objets sans rapport est un comportement indéfini.
Est-il légal de pointer un élément après la fin d'un tableau ?
Oui - former un pointeur vers la position située juste après le dernier élément est explicitement autorisé pour que les boucles comme for (int *p = arr; p != arr + n; p++) fonctionnent. Vous pouvez calculer et comparer ce pointeur, mais le déréférencer est un comportement indéfini.
Pourquoi *p++ n'est-il pas la même chose que (*p)++ ?
*p++ incrémente le pointeur et déréférence l'ancienne valeur - il lit l'élément courant, puis avance. (*p)++ laisse le pointeur tranquille et incrémente la valeur pointée. Le ++ postfixe s'attache plus fort que *, ce sont donc les parenthèses qui changent le sens.