Les tableaux et les pointeurs sont des choses différentes en C qui se comportent pareil la plupart du temps. Ce « la plupart du temps » rend le sujet glissant : du code qui traite un tableau comme un pointeur fonctionne généralement, jusqu'au jour où il atterrit dans l'un des deux endroits où la différence compte et où la réponse est silencieusement fausse.
Cette page trace la limite précisément.
La dégradation de tableau : la règle derrière tout
Dans presque toutes les expressions, un nom de tableau est automatiquement converti en pointeur sur son premier élément. Cela s'appelle la dégradation. arr devient &arr[0], de type int *.
Le nom du tableau et l'adresse de son premier élément s'affichent à l'identique. C'est la dégradation en action, et c'est pourquoi vous pouvez affecter un tableau à un pointeur sans &.
Il existe exactement trois endroits où la dégradation n'a pas lieu : comme opérande de sizeof, comme opérande de &, et lors de l'initialisation d'un tableau de caractères depuis un littéral de chaîne. Partout ailleurs, supposez la dégradation.
arr[i] est littéralement *(arr + i)
La norme C définit l'opérateur d'indice en termes d'arithmétique de pointeurs : a[b] signifie *(a + b). L'indexation n'est pas une fonctionnalité distincte - c'est un raccourci.
Deux conséquences en découlent. D'abord, un pointeur peut être indexé : p[2] est *(p + 2), c'est pourquoi les fonctions recevant un pointeur peuvent encore utiliser la syntaxe familière arr[i]. Ensuite, 2[arr] compile - *(2 + arr) est la même adresse que *(arr + 2). Personne n'écrit cela hors d'un quiz, mais cela tranche la question de savoir si l'indexation est « vraiment » de l'arithmétique de pointeurs.
Passer un tableau à une fonction
À cause de la dégradation, une fonction ne reçoit jamais un tableau. Elle reçoit un pointeur.
Trois choses à retirer de cet exemple.
La taille dans int arr[100] est de la documentation, rien de plus. Le compilateur réécrit le paramètre en int *arr et ne vérifie jamais que vous avez passé 100 de quoi que ce soit.
La longueur doit voyager séparément. La fonction a une adresse et aucune idée de l'étendue des données. C'est pourquoi pratiquement toute fonction C prenant un tableau prend un compte : memcpy(dst, src, n), fread(ptr, size, count, f), qsort(base, nmemb, size, cmp). Les chaînes ne font exception que parce qu'elles portent leur propre marqueur de fin, le '\0'.
Passer un pointeur est peu coûteux et les données sont partagées. Aucune copie du tableau n'est faite, donc la fonction peut modifier les éléments de l'appelant - ce qui est souvent exactement ce que vous voulez :
Marquez les paramètres en lecture seule const int * comme le fait print_all. Cela documente l'intention et laisse le compilateur attraper une écriture accidentelle.
Le piège de sizeof
Voici le bug qui attrape chaque programmeur C une fois. sizeof est l'un des deux endroits où la dégradation n'a pas lieu - il donne donc la vraie taille du tableau là où le tableau est déclaré, et la taille d'un pointeur partout où le tableau s'est dégradé.
Dans main, sizeof data vaut 40 octets et la longueur se calcule à 10. Dans inspect, sizeof arr est la taille d'un pointeur - 8 sur une machine 64 bits - et la « longueur » sort à 2. Le code paraît identique et est discrètement faux.
La règle qui en découle : ne calculez la longueur d'un tableau que dans la portée où il a été déclaré, et transmettez-la de là.
Le & d'un tableau n'a pas le même type
L'autre endroit où la dégradation n'a pas lieu est l'opérateur &. &arr est un pointeur sur le tableau entier, pas sur son premier élément. Les deux contiennent la même adresse ; la différence est ce que « ajouter 1 » signifie.
Même adresse de départ, pas différent. arr a le type int * et avance d'un élément ; &arr a le type int (*)[5] et avance du tableau entier. C'est le mécanisme qui fait fonctionner le passage d'une ligne d'un tableau 2D, couvert dans tableaux multidimensionnels.
Pointeur sur tableau contre tableau de pointeurs
Deux déclarations qui se ressemblent et signifient des choses opposées :
int *p[5]; // tableau de 5 pointeurs sur int
int (*q)[5]; // pointeur sur un tableau de 5 int
[] a une précédence plus forte que *, donc int *p[5] s'analyse comme « p est un tableau, de pointeurs ». Les parenthèses dans int (*q)[5] forcent le * à s'attacher d'abord : « q est un pointeur, sur un tableau ».
int *p[5] int (*q)[5]
p[0] -> [ int ] q -> [ int | int | int | int | int ]
p[1] -> [ int ] un bloc contigu de 5
p[2] -> [ int ]
p[3] -> [ int ]
p[4] -> [ int ]
cinq adresses distinctes
Un tableau de pointeurs est le cas courant, et vous en avez déjà utilisé un : char *argv[] est un tableau de pointeurs sur des chaînes, un par argument de ligne de commande. C'est aussi ainsi qu'on détient une liste de chaînes de longueurs différentes sans gaspiller d'espace.
Notez (*q)[0] : déréférencer le pointeur pour obtenir le tableau, puis l'indexer. Les parenthèses sont de nouveau obligatoires, pour la même raison de précédence.
Là où tableaux et pointeurs diffèrent réellement
Gardez cette petite table en tête et la confusion disparaît :
Tableau int arr[10] | Pointeur int *p | |
|---|---|---|
| Ce que c'est | 10 int disposés en mémoire | une variable contenant une adresse |
sizeof | 40 - le bloc entier | 8 - juste le pointeur |
| Affectable | non : arr = x; est une erreur | oui : p = x; convient |
& donne | int (*)[10] | int ** |
| D'où vient la mémoire | déclarée avec le tableau | de là où vous le pointez |
Un nom de tableau n'est pas une variable contenant une adresse - il est le stockage, et l'adresse est calculée à la demande. C'est pourquoi vous ne pouvez pas lui affecter quoi que ce soit.
Questions fréquentes
Les tableaux et les pointeurs sont-ils la même chose en C ?
Non, mais il est facile de les confondre car un nom de tableau se dégrade en pointeur sur son premier élément dans presque toutes les expressions. Le tableau lui-même est un bloc d'éléments de taille connue ; un pointeur est une variable unique contenant une adresse. sizeof et & sont les deux endroits où la différence se voit.
Pourquoi sizeof(arr) donne-t-il une mauvaise réponse dans une fonction ?
Parce que le paramètre n'est pas un tableau. void f(int arr[]) est silencieusement réécrit en void f(int *arr), donc sizeof(arr) mesure un pointeur (8 octets sur la plupart des machines), pas le tableau d'origine. Passez la longueur en paramètre séparé.
Quelle est la différence entre int *p[5] et int (*p)[5] ?
int *p[5] est un tableau de 5 pointeurs sur int. int (*p)[5] est un pointeur unique sur un tableau de 5 int. Les parenthèses attachent d'abord le * au nom ; sans elles, [] l'emporte car il a une précédence plus forte.
arr[i] est-il vraiment la même chose que *(arr + i) ?
Oui - la norme définit a[b] comme *(a + b). C'est aussi pourquoi l'étrange i[arr] compile et fonctionne : l'addition est commutative, donc *(i + arr) est le même élément. Ne l'écrivez jamais dans du vrai code, mais cela prouve la règle.