Menu

Pointeurs et tableaux en C : dégradation, arr[i] = *(arr+i) et le piège de sizeof

En C, un nom de tableau se transforme en pointeur sur son premier élément presque partout où vous l'utilisez. Cette page explique cette dégradation, pourquoi arr[i] est littéralement *(arr+i), pourquoi la taille du tableau doit voyager séparément vers les fonctions, et en quoi un pointeur sur tableau diffère d'un tableau de pointeurs.

Cette page contient des éditeurs exécutables - modifiez, exécutez et voyez la sortie instantanément.

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'est10 int disposés en mémoireune variable contenant une adresse
sizeof40 - le bloc entier8 - juste le pointeur
Affectablenon : arr = x; est une erreuroui : p = x; convient
& donneint (*)[10]int **
D'où vient la mémoiredéclarée avec le tableaude 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.

Coddy programming languages illustration

Apprendre à coder avec Coddy

COMMENCER