Tableau ou pointeur en C : sizeof, char[] et char *
Un tableau n'est pas un pointeur. sizeof, décroissance, a[i] et *(a + i), char[] contre char *, tableaux 2D et « assignment to expression with array type ».
Tu écris t[i] sur un tableau, p[i] sur un pointeur, et les deux marchent. Tu passes un tableau à une fonction qui attend un pointeur, et ça compile. Puis sizeof te renvoie 8 au lieu de 20, ou GCC refuse nom = "Yasmine". Tableaux et pointeurs se ressemblent assez pour qu'on les confonde, et diffèrent juste assez pour que la confusion coûte cher.
En bref
Un tableau est un bloc de mémoire qui contient ses éléments ; un pointeur est une variable qui contient une adresse. Dans presque toutes les expressions, le nom d'un tableau est converti en pointeur vers son premier élément, c'est la « décroissance », d'où a[i] qui s'écrit pareil des deux côtés. Mais sizeof d'un tableau donne sa taille totale (20 pour int[5]) quand sizeof d'un pointeur donne 8 sur une machine 64 bits, un tableau passé à une fonction n'y arrive que sous forme de pointeur, et un tableau ne s'affecte pas : nom = "Yasmine" donne assignment to expression with array type.
sizeof d'un tableau et sizeof d'un pointeur
Sur un tableau, sizeof donne la place de tous ses éléments ; sur un pointeur, la place d'une adresse, quel que soit ce qu'elle désigne.
#include <stdio.h>
int main(void){ int notes[5] = {12, 15, 9, 18, 11}; int *p = notes;
printf("sizeof notes : %zu\n", sizeof notes); printf("sizeof p : %zu\n", sizeof p); printf("éléments : %zu\n", sizeof notes / sizeof notes[0]); return 0;}$ gcc -Wall -Wextra -g taille.c -o taille$ ./taillesizeof notes : 20sizeof p : 8éléments : 5notes occupe 5 fois 4 octets. p ne contient qu'une adresse, 8 octets sur une machine 64 bits, même s'il désigne le premier élément de notes. La division sizeof notes / sizeof notes[0] est la façon classique de compter les éléments d'un tableau. Elle ne marche que là où le tableau est déclaré, et c'est là que les ennuis commencent.
Pourquoi sizeof donne 8 dans une fonction
Parce qu'une fonction ne reçoit jamais un tableau. Un paramètre déclaré int notes[5] est transformé par le compilateur en int *notes, et le 5 est ignoré. GCC le signale, même sans -Wall :
#include <stdio.h>
void affiche_taille(int notes[5]){ printf("dans la fonction : %zu\n", sizeof notes);}
int main(void){ int notes[5] = {12, 15, 9, 18, 11};
printf("dans main : %zu\n", sizeof notes); affiche_taille(notes); return 0;}$ gcc -Wall -Wextra -g param.c -o paramparam.c: In function 'affiche_taille':param.c:5:47: warning: 'sizeof' on array function parameter 'notes' will return size of 'int *' [-Wsizeof-array-argument] 5 | printf("dans la fonction : %zu\n", sizeof notes); | ^~~~~param.c:3:25: note: declared here 3 | void affiche_taille(int notes[5]) | ~~~~^~~~~~~~$ ./paramdans main : 20dans la fonction : 8Le warning se lit tel quel : sizeof sur un paramètre déclaré comme tableau renverra la taille d'un int *. Dans un vrai programme, l'erreur ne saute pas aux yeux. Cette moyenne compte ses notes avec la formule de la section précédente :
#include <stdio.h>
int moyenne(int notes[]){ int n = sizeof notes / sizeof notes[0]; int total = 0;
for (int i = 0; i < n; i++) total += notes[i]; return total / n;}
int main(void){ int notes[5] = {12, 15, 9, 18, 6};
printf("%d\n", moyenne(notes)); return 0;}$ ./compte13La vraie moyenne est 12. n vaut 8 / 4, soit 2, et la fonction n'a lu que les deux premières notes. Aucun crash, un résultat plausible : le pire des bugs. Il n'y a qu'une correction, passer la longueur en paramètre, int moyenne(const int *notes, size_t n). C'est pour ça que qsort, memcpy ou fgets demandent toujours une taille à côté du pointeur.
is_sorted
is_sorted reçoit un pointeur et un nombre d'éléments n, comme toute fonction C qui travaille sur un tableau. Lis exactement n valeurs, jamais a[n], et dis si elles sont dans l'ordre croissant. L'énoncé insiste sur n à 0, les voisins égaux et INT_MIN placé juste avant INT_MAX.
La décroissance : quand un tableau devient un pointeur
Dans une expression, le nom d'un tableau est converti en pointeur vers son premier élément. int *p = notes; revient à écrire int *p = ¬es[0];, et c'est la même conversion qui a lieu quand tu passes notes à une fonction. La norme en liste quelques exceptions : l'opérande de sizeof, celui de &, celui de typeof depuis C23, et la chaîne littérale qui initialise un tableau de char.
C'est aussi ce qui explique l'indexation. Par définition, a[i] est exactement *(a + i) : on décale l'adresse de i éléments, puis on lit ce qui s'y trouve. Le décalage se compte en éléments, pas en octets. L'addition étant commutative, 2[notes] est une écriture valide, que personne ne devrait utiliser mais qui prouve le mécanisme :
#include <stdio.h>
int main(void){ int notes[5] = {12, 15, 9, 18, 11};
printf("notes = %p\n", (void *)notes); printf("¬es = %p\n", (void *)¬es); printf("notes + 1 = %p\n", (void *)(notes + 1)); printf("¬es + 1 = %p\n", (void *)(¬es + 1)); printf("notes[2] = %d, *(notes + 2) = %d, 2[notes] = %d\n", notes[2], *(notes + 2), 2[notes]); return 0;}$ gcc -Wall -Wextra -g adresses.c -o adresses$ ./adressesnotes = 0xfffff7568688¬es = 0xfffff7568688notes + 1 = 0xfffff756868c¬es + 1 = 0xfffff756869cnotes[2] = 9, *(notes + 2) = 9, 2[notes] = 9Les adresses changent à chaque exécution, les écarts non. Regarde les deux premières lignes : notes et ¬es donnent la même adresse, celle du début du tableau. Leur type diffère pourtant. notes décroît en int *, un pointeur vers un entier, et notes + 1 avance de 4 octets (688 devient 68c). ¬es est un int (*)[5], un pointeur vers le tableau entier, et ¬es + 1 avance de 20 octets (688 devient 69c), juste après le dernier élément.
sum_range
sum_range additionne les entiers entre deux pointeurs, first inclus et last exclu, sans aucun paramètre de longueur. Tu n'as que l'arithmétique des pointeurs pour avancer, et tu ne dois jamais lire la case désignée par last.
char s[] ou char *s : la différence
char tab[] = "salut"; crée un tableau de 6 char et y copie le texte. char *ptr = "salut"; crée un pointeur vers une chaîne littérale, rangée ailleurs par le compilateur. Les deux s'affichent pareil avec %s, tout le reste diffère :
#include <stdio.h>
int main(void){ char tab[] = "salut"; char *ptr = "salut";
printf("sizeof tab : %zu\n", sizeof tab); printf("sizeof ptr : %zu\n", sizeof ptr); tab[0] = 'S'; printf("%s\n", tab); ptr = "bonjour"; printf("%s\n", ptr); return 0;}$ gcc -Wall -Wextra -g chaines.c -o chaines$ ./chainessizeof tab : 6sizeof ptr : 8SalutbonjourPile (stack)
- char[6]tab
- 's''a''l''u''t''\0'
- char *ptr
- litteral
Données statiques
char tab[] = "salut"; | char *ptr = "salut"; | |
|---|---|---|
| Ce que c'est | un tableau de 6 char, sur la pile | un pointeur vers une chaîne littérale |
sizeof | 6, le texte et son '\0' | 8, la taille d'une adresse |
| Modifier un caractère | oui, tab[0] = 'S' | non, la chaîne littérale est en lecture seule |
| Désigner un autre texte | non, tab = ... ne compile pas | oui, ptr = "bonjour" |
Écrire ptr[0] = 'S' compile sans un warning et plante à l'exécution : c'est l'un des cas détaillés dans Segmentation fault (core dumped) en C. Le réflexe qui l'évite : déclare const char *ptr = "salut";, et le compilateur refusera toute écriture. Pour le fonctionnement des chaînes elles-mêmes, '\0', strlen et strcpy, lis Chaînes de caractères en C.
assignment to expression with array type
Cette erreur arrive quand tu essaies d'affecter un tableau entier avec = :
#include <stdio.h>
int main(void){ char nom[20];
nom = "Yasmine"; printf("%s\n", nom); return 0;}$ gcc -Wall -Wextra -g affecte.c -o affecteaffecte.c: In function 'main':affecte.c:7:9: error: assignment to expression with array type 7 | nom = "Yasmine"; | ^Un tableau ne peut pas être la cible d'une affectation : la norme exclut explicitement les types tableau des « lvalues modifiables », ce qu'on a le droit de mettre à gauche d'un =. nom désigne directement la zone de 20 octets : aucune adresse n'est rangée nulle part, il n'y a rien à remplacer. Tu as trois solutions, selon ce que tu veux :
char nom[20];nom = "Yasmine";char nom[20] = "Yasmine"; /* à la déclaration */strcpy(nom, "Ines"); /* copie, si ça tient */const char *surnom = "Yas"; /* un pointeur, lui, */surnom = "Ine"; /* se réaffecte */L'initialisation char nom[20] = "Yasmine"; utilise aussi un =, mais la norme la traite à part : une chaîne littérale peut remplir un tableau au moment où on le déclare, et seulement à ce moment-là. La même règle interdit a = b entre deux tableaux d'int : copie-les avec une boucle ou avec memcpy(3).
Les tableaux à deux dimensions
Un tableau 2D est un tableau de tableaux, rangé ligne après ligne dans un seul bloc. int grille[2][3] contient six int contigus :
Pile (stack)
- int[2][3]grille
- 123456
- ligne 0 puis ligne 1
La décroissance s'applique au premier niveau seulement : grille devient un pointeur vers sa première ligne, de type int (*)[3]. Ce n'est pas un int **, un pointeur vers des pointeurs, et depuis GCC 14 la confusion est une erreur :
$ gcc -Wall -Wextra -g matrice.c -o matricematrice.c: In function 'main':matrice.c:17:26: error: passing argument 1 of 'somme' from incompatible pointer type [-Wincompatible-pointer-types] 17 | printf("%d\n", somme(grille, 2, 3)); | ^~~~~~ | | | int (*)[3]matrice.c:3:17: note: expected 'int **' but argument is of type 'int (*)[3]' 3 | int somme(int **m, int lignes, int colonnes) | ~~~~~~^Un int ** attend en mémoire un tableau d'adresses, une par ligne. La grille n'en contient aucune, seulement six entiers à la suite. Si le compilateur laissait passer, m[0] lirait les octets de 1 et 2 comme une adresse, sur une machine 64 bits. La fonction doit connaître la largeur d'une ligne pour calculer où commence la suivante, donc le paramètre la donne :
#include <stdio.h>
int somme(int m[][3], int lignes){ int total = 0;
for (int i = 0; i < lignes; i++) for (int j = 0; j < 3; j++) total += m[i][j]; return total;}
int main(void){ int grille[2][3] = {{1, 2, 3}, {4, 5, 6}};
printf("%d\n", somme(grille, 2)); printf("sizeof grille : %zu, sizeof grille[0] : %zu\n", sizeof grille, sizeof grille[0]); return 0;}$ gcc -Wall -Wextra -g matrice2.c -o matrice2$ ./matrice221sizeof grille : 24, sizeof grille[0] : 12int m[][3] et int (*m)[3] sont deux écritures du même paramètre. grille pèse 24 octets, une ligne 12. Le module Arrays and matrices du parcours C fait pratiquer ces parcours de lignes et de colonnes, jusqu'au remplissage d'une grille.
Questions fréquentes
Un tableau est-il un pointeur constant ?
Non, même si la comparaison circule beaucoup. Un tableau est un objet qui contient ses éléments : sizeof donne leur taille totale, et &tab est un pointeur vers le tableau entier. Un pointeur constant est une variable qui contient une adresse, 8 octets sur une machine 64 bits. Le nom d'un tableau se convertit en pointeur dans la plupart des expressions, ce qui ne fait pas de lui un pointeur.
Comment connaître la taille d'un tableau reçu en paramètre ?
On ne peut pas : la fonction ne reçoit qu'un pointeur, et la taille est perdue. Passe-la dans un paramètre à côté (size_t n), ou passe un pointeur de fin, comme sum_range(first, last). Une chaîne de caractères fait exception, son '\0' marque la fin.
Une fonction peut-elle renvoyer un tableau ?
Non, le type de retour d'une fonction ne peut pas être un tableau. Renvoie un pointeur vers un bloc alloué avec malloc, que l'appelant libérera, ou fais remplir un tableau fourni par l'appelant. Ne renvoie jamais l'adresse d'un tableau local : il disparaît au return.
Pourquoi printf("%s", tab) et printf("%s", ptr) affichent la même chose ?
Parce que tab décroît en char * au moment de l'appel. printf reçoit une adresse dans les deux cas et lit jusqu'au '\0'. La différence entre les deux n'existe que là où ils sont déclarés.
Sources
cppreference : conversion d'un tableau en pointeur et ses exceptions
cppreference : l'opérateur d'indice, E1[E2] identique à *((E1)+(E2))
cppreference : catégories de valeurs, définition d'une lvalue modifiable
GCC : options de warnings, dont -Wsizeof-array-argument et -Wincompatible-pointer-types
GCC 14 Porting Guide : incompatible-pointer-types devient une erreur
