Ce tutoriel a été initialement rédigé sur le Site du Zéro par Marc Mongenet sous licence CC BY-SA.
Bienvenue
Ainsi, tu es à la recherche de la vérité sur les tableaux et pointeurs en C ? Mais laisse-moi d’abord deviner ce qui te conduit à lire ce tutoriel…
Peut-être as-tu présomptueusement affirmé qu’un tableau est un pointeur ? Et quelqu’un qui connait la vérité t’aura dirigé ici… Ou alors tu auras essayé de passer un tableau de tableaux en argument d’une fonction qui attend un pointeur de pointeur, comme ceci.
void f(int **ppint);
void g(void) {
int tab[3][4];
f(tab); /* grossière erreur ! */
}
Et le compilateur aura émis un avertissement que tu ne comprends pas.
ex1.c: In function ‘g’:
ex1.c:5:2: warning: passing argument 1 of 'f' from incompatible pointer type [-Wincompatible-pointer-types]
5 | f(tab); /* grossière erreur ! */
| ^~~
| |
| int (*)[4]
ex1.c:1:6: note: expected 'int **' but argument is of type 'int (*)[4]'
1 | void f(int **ppint);
| ~~~~~~^~~~~
Et tu auras peut-être même fait fi de l’avertissement du compilateur , lancé ton programme, et il plante lamentablement à la première utilisation de ppint
dans la fonction f
…
On m’aurait menti ?
Oui, les tutoriels mentent, car les débutants ne pourraient pas affronter la vérité toute nue sur les tableaux et les pointeurs.
Mais pour commencer, revoyons ce qui est à l’origine des malentendus, c’est-à-dire le pieux mensonge enseigné aux débutants. Il est généralement écrit dans des termes ressemblant à ceux-ci : « Un tableau se comporte comme un pointeur constant sur son élément initial. »
Notons d’abord qu’un (bon) tutoriel ne prétendra jamais qu’un tableau est un pointeur, mais qu’il se comporte comme, ou se convertit en, etc. Affirmer qu’un tableau est un pointeur est abusif.
- Récapitulation de ce qui devrait être déjà connu
- Déclaration de tableau et pointeur
- La vérité vraie
- sizeof tab, &tab, et char tab[] = "chaine"
- char tab[] et char *p
- lvalue et register
- Pointeur de pointeur et tableau de tableaux
- Paramètres formels : char*argv[] vs char**argv
- C89 et C11
Récapitulation de ce qui devrait être déjà connu
Revoyons en quoi « un tableau se comporte comme un pointeur constant sur son élément initial. »
#include <stdio.h>
int main(void)
{
int tab[3];
int *p;
p = tab;
p = tab + 2;
tab[1] = 5;
p[-1] = 6;
printf("hello, world\n");
/* tab = p; /* ne compile pas */
return 0;
}
Explications ligne par ligne
int tab[3];
Un tableau de 3 int
appelé tab
est défini.
int *p;
Un pointeur de int
appelé p
est défini.
p = tab;
tab
se comporte comme un pointeur sur son élément initial, qui est affecté à p
. Le pointeur p
pointe donc sur tab[0]
.
Essaie de deviner la suite des explications avant d’ouvrir la partie secrète qui suit.
p = tab + 2;
tab
se comporte comme un pointeur sur son élément initial, 2
est ajouté à ce pointeur (arithmétique de pointeur), et le résultat est affecté à p
. Le pointeur p
pointe donc sur tab[2]
.
tab[1] = 5;
L’instruction tab[1] = 5;
est équivalente à *(tab + 1) = 5;
, car par définition T[N]
est équivalent à *((T)+(N))
. tab
est donc converti en un pointeur sur son élément initial, puis 1
est ajouté (arithmétique de pointeur), puis le pointeur résultant est déréférencé pour affecter 5
à l’objet pointé.
p[-1] = 6;
C’est le cas simplifié de tab[1] = 5;
car p
est un pointeur, il n’y a même pas besoin de conversion. À noter que malgré l’indice négatif, on pointe toujours dans le même tableau car p
pointe sur tab[2]
.
printf("hello, world\n");
La chaine "hello, world\n"
est un tableau de 14 char
. Ce tableau se comporte comme un pointeur sur son élément initial, soit la lettre 'h'
. Ce pointeur est passé à la fonction printf
, qui attend justement un pointeur de char
. Eh oui, au cas où tu ne t’en serais pas rendu compte, on tombe sur une conversion de tableau en pointeur dès le hello world
!
tab = p;
Cette ligne ne compile pas car tab
est constant. Il est donc impossible d’affecter une valeur à tab
.
Déclaration de tableau et pointeur
Ce chapitre repasse en revue ce qui est mis en mémoire lorsqu’on déclare un tableau, et lorsqu’on déclare un pointeur. Ça devrait être connu, mais sait-on jamais.
Quand on déclare int t1[3];
on obtient trois int
en mémoire, et rien d’autre.
Quand on déclare int *p1;
on obtient un pointeur de int
en mémoire, et rien d’autre.
Quand on déclare int t2[3][4];
on obtient trois tableaux de quatre int
, soit douze int
en mémoire, et rien d’autre.
Quand on déclare int **p2;
on obtient un pointeur de pointeur de int
en mémoire, et rien d’autre.
Quand on déclare int *t3[3];
on obtient trois pointeurs de int
en mémoire, et rien d’autre.
Quand on déclare int (*p3)[3];
on obtient un pointeur de tableau de 3 int
en mémoire, et rien d’autre.
Initialisation et représentation en mémoire
Logiquement (si si, c’est logique, les trucs illogiques, c’est pour les derniers chapitres), on peut écrire :
int t1[3] = { 1, 2, 3 };
int *p1 = &t1[0];
int t2[4][3] = {{1,2,3}, {4,5,6}, {7,8,9}, {10,11,12}};
int **p2 = &p1;
int *t3[3] = { &t1[1], &t2[0][0], &t2[1][2] };
int (*p3)[3] = &t2[3];
Voici ce que ça pourrait donner en mémoire, avec des int
de 4 bytes et des pointeurs de 8 bytes.
int t1[3] = { 1, 2, 3 };
adresses ...|8 |12 |16 |20
données ...|0001|0002|0003|...
int *p1 = &t1[0];
adresses ...|24 |32
données ...|00000008|...
int t2[4][3] = {{1,2,3}, {4,5,6}, {7,8,9}, {10,11,12}};
adresses ...|32 |36 |40 |44 |48 |52 |56 |60 |64 |68 |72 |76 |80
données ...|0001|0002|0003|0004|0005|0006|0007|0008|0009|0010|0011|0012|...
int **p2 = &p1;
adresses ...|80 |88
données ...|00000024|...
int *t3[3] = { &t1[1], &t2[0][0], &t2[1][2] };
adresses ...|88 |96 |104 |112
données ...|00000012|00000032|00000052|...
int (*p3)[3] = &t2[3];
adresses ...|112 |120
données ...|00000068|...
Enfin, on ne peut évidemment pas écrire ce qui suit, puisqu'un tableau n’est pas un pointeur, et un pointeur n’est pas un tableau.
int t1[3] = 0; /* FAUX */
int *p = { 1, 2, 3 }; /* FAUX */
Déclarations externes
Comme un tableau n’est pas un pointeur, lorsqu’on déclare un tableau ou un pointeur sans le définir (avec le mot clé extern
), il faut que la définition soit respectivement celle d’un tableau ou d’un pointeur. Sinon, apparaît un bogue qui révèle bien les différences de représentation mémoire entre tableau et pointeur.
Observons le comportement du programme bogué suivant, composé des fichiers source main.c
et text.c
.
/* main.c */
#include <stdio.h>
extern char *p1;
extern char tab1[];
extern char p2[]; /* déclaration de tableau alors que p2 est défini en pointeur */
extern char *tab2; /* déclaration de pointeur alors que tab2 est défini en tableau */
int main(void) {
puts(p1);
puts(tab1);
puts(p2);
puts(tab2);
return 0;
}
/* text.c */
char *p1 = "p1";
char tab1[] = "tab1";
char *p2 = "p2";
char tab2[] = "tab2";
$ gcc main.c text.c -Wall
$ ./a.out
p1
tab1
ÿ@
Erreur de segmentation
$
Les deux premières chaines s’affichent parfaitement, comme il se doit, puisque les déclarations externes correspondent bien aux définitions.
Pour la troisième chaine, l’affichage est incohérent. En effet, en compilant main.c
mon compilateur a cru que p2
est un tableau, et a produit du langage machine qui affiche les bytes constituant ce qui est en fait un pointeur. D’où le ÿ@
sur mon PC. Selon la norme C, il s’agit d’un comportement indéterminé (C11 §6.2.7, al. 2 : « All declarations that refer to the same object or function shall have compatible type; otherwise, the behavior is undefined. »).
Enfin, le puts(tab2)
créé une erreur de segmentation, car les bytes de la chaine "tab2"
sont pris pour un pointeur, qui pointe sans surprise à une adresse invalide. Évidemment, encore un comportement indéterminé.
La vérité vraie
La source de la vérité
La vérité sur les tableaux et les pointeurs se trouve actuellement dans le chapitre 6.3.2.1 Lvalues, arrays, and function designators, paragraphe 3, de la norme internationale ISO/IEC 9899:2011 (C11 pour les intimes).
Eh oui, la vérité nue est en anglais. Ça permet d’amortir le choc.
Voici une traduction.
Le mot clé, c’est le mot expression. Une expression n’est pas un objet en mémoire, c’est un bout de code source, comme 2+3
, &i
, ou simplement tab
.
Quand il lit l’expression 2+3
, le compilateur peut directement calculer le résultat : 5
(de type int
).
Quand il lit l’expression &i
, le compilateur génère les instructions machine qui calculent l’adresse de i
(de type int*
si i
est de type int
).
Quand il lit l’expression tab
, le compilateur génère les instructions machine qui calculent l’adresse de l’élément initial de tab
(de type int*
si tab
est de type int[]
). Sauf avec sizeof
, &
, et "chaine d'initialisation"
, mais ça c’est le chapitre suivant.
Enfin, je rappelle qu'une déclaration n’est pas une expression. Déclarer un tableau ne revient donc évidemment pas du tout à déclarer un pointeur. Quand on écrit int* p
, tab[3];
il y a deux déclarations, mais aucune expression, donc aucune conversion. (Et le premier de classe qui lève la main pour me dire que, si, le 3
tout seul dans tab[3]
est une expression constante, je lui demanderai de se taire pour ne pas embrouiller les autres).
Je dois encore signaler que la vérité a un peu évolué. Du temps de C89 (trouvé sur http://flash-gordon.me.uk/ansi.c.txt), c’était :
En C89, ce ne sont pas toutes les expressions qui sont converties, mais seulement les lvalues. Étant donné qu’un tableau est une lvalue, c’est une différence subtile qui ne se révèle que dans des cas assez tordus. Et pour ne pas décourager les lecteurs qui ont suivi jusqu’ici, je n’illustrerai cette différence qu’au dernier chapitre du tutoriel.
Bon, maintenant que la vérité est dévoilée, j’espère que tout est devenu beaucoup plus clair.
Sinon, des exemples sont donnés dans la suite du tutoriel…
sizeof tab, &tab, et char tab[] = "chaine"
Sauf quand elle est l’opérande de l’opérateur sizeof, de l’opérateur _Alignof ou de l’opérateur unaire &, ou est une chaine de caractères littérale utilisée pour initialiser un tableau, une expression de type "tableau de type" est convertie en une expression de type "pointeur de type" qui pointe sur l’élément initial de l’objet tableau et n’est pas une lvalue.
Comme revu dans l’introduction, lorsqu’une expression de type tableau est l’opérande de l’opérateur d’affectation (p=tab
), d’addition (tab+2
), d’appel de fonction (printf("hello, world\n")
), et de beaucoup d’autres opérateurs, alors elle se comporte comme un pointeur constant sur l’élément initial du tableau.
En fait, il n’existe que quatre cas où une expression de type tableau se comporte clairement comme un tableau. Ils sont soulignés dans l’extrait de la norme ci-dessus, et illustrés ci-dessous.
sizeof tab
#include <stdio.h>
int main(void)
{
char tab[3];
char *p = tab;
printf("sizeof tab vaut %zu.\n", (int)sizeof tab);
printf("sizeof p vaut %zu.\n", (int)sizeof p);
return 0;
}
Ce code affiche le texte suivant sur mon système :
sizeof tab vaut 3.
sizeof p vaut 8.
Il se peut que la taille de p
soit différente de 8 sur ton système (4 est une valeur courante).
Ce qu’il est important de constater, c’est que la taille du tableau tab
est différente de celle du pointeur p
. En effet, lorsque l’opérande de sizeof
est de type tableau, alors le résultat est la taille du tableau, et pas la taille d’un pointeur sur un élément du tableau.
On peut se demander pourquoi, une fonction size_t my_strlen(char s[]){return sizeof s;}
retourne la taille d’un pointeur de char
, et pas la taille du tableau contenant la chaine de caractères s
. La réponse est très simple : parce que dans ce cas, s
est un pointeur de char
. C’est une particularité des déclarations de paramètres de fonction sur laquelle je reviendrai plus tard…
_Alignof
#include <stdalign.h>
#include <stdio.h>
int main(void)
{
char tab[3];
char *p = tab;
printf("alignof tab vaut %zu.\n", alignof tab);
printf("alignof p vaut %zu.\n", alignof p);
return 0;
}
Ce code produit la sortie suivante.
alignof tab vaut 1.
alignof p vaut 8.
À nouveau, on peut constater une différence entre le tableau tab
et le pointeur p
. Dans le cas du tableau, c’est la contrainte d’alignement du type de ses éléments qui est retournée (donc celle du type char
, qui est toujours de 1), alors que pour p
, c’est la contrainte d’alignement du type pointeur sur char
qui est retournée.
&tab
int main(void)
{
int tab[3];
int *p;
int **pp;
int (*pt)[3];
p = &tab;
pp = &tab;
pt = &tab;
return 0;
}
Dans le code ci-dessus, deux des trois affectations de pointeur provoquent un avertissement de la part du compilateur.
$ gcc ex3.c
ex3.c: In function ‘main’:
ex3.c:8:7: warning: assignment to 'int *' from incompatible pointer type 'int (*)[3]' [-Wincompatible-pointer-types]
8 | p = &tab;
| ^
ex3.c:9:8: warning: assignment to 'int **' from incompatible pointer type 'int (*)[3]' [-Wincompatible-pointer-types]
9 | pp = &tab;
| ^
$
Seule l’affectation pt = &tab
ne provoque aucun avertissement. En effet, le type de pt
est le même que celui de l’expression &tab
: « pointeur de tableau de 3 int
», ce qui s’écrit int(*)[3]
en C.
On remarque en particulier que le type de l’expression &tab
n’est pas int**
. Ce serait le cas si l’expression tab
était convertie en pointeur de int
(soit int*
) avant l’opération unaire &
. Mais ce n’est pas le cas ; le type de &tab
est donc un pointeur sur un tableau, et pas un pointeur sur un pointeur.
Attention à la paire de parenthèses en notant le type int(*)[3]
. Si on écrit int*[3]
, cela signifie « tableau de 3 pointeurs de int
», car l’opérateur *
a une précédence inférieure à l’opérateur []
.
char tab[] = "chaine"
int main(void)
{
char *p = "hello";
char t1[] = "world";
char t2[] = p; /* FAUX */
char t3[] = (char*)"foo"; /* FAUX */
return 0;
}
Pourquoi les initialisations de t2
et t3
ne compilent pas, alors que celle de t1
est parfaitement valable ? Car un tableau ne peut pas être initialisé avec une valeur scalaire, or un pointeur est une valeur scalaire, et les initialisateurs de t2
et t3
sont des pointeurs.
En revanche, la chaine "world"
qui sert à initialiser t1
n’est pas convertie en pointeur. La chaine "world"
est de type tableau de 6 char
et l’initialisation fait de t1
un tableau de 6 char
. C’est équivalent à char t1[]={'w','o','r','l','d',0};
.
Enfin pour initialiser p
, le tableau "hello"
est classiquement converti en un pointeur sur son élément initial.
Pour être propre, il faudrait écrire const char *p = "hello";
mais ça compliquerait inutilement les explications.
char tab[] et char *p
Note : cette partie est inspirée du topic Différence entre char *tab
et char tab[]
d’Arthurus.
char t1[] = { 'h', 'e', 'l', 'l', 'o', 0 };
char t2[] = "hello";
char *p = "hello"; /* BOF */
t1[0] = 'H'; /* OK */
t2[0] = 'H'; /* OK */
p[0] = 'H'; /* FAUX */
p = t1; /* OK */
p[0] = 'H' /* OK */
Comme on l’a vu précédemment, t1
et t2
sont des tableaux de 6 char
. On peut librement changer la valeur de leurs éléments. Mais pourquoi l’instruction commentée /* FAUX */
est-elle fausse, alors qu’elle compile sans avertissement ?
C’est faux car dans la déclaration char *p = "hello";
il y a deux objets en mémoire : le pointeur p
et un tableau constant anonyme qui contient la chaine "hello"
. Les tableaux anonymes sont courants, on en trouve par exemple un dans printf("hello, world\n")
.
Voilà ce qui arrive avec char *p = "hello";
: le compilateur crée un tableau de char
statique quelque-part, et ce quelque-part peut être une zone de mémoire où les modifications sont interceptées par le système d’exploitation. Et en cas d’interception, un système comme Linux écrit « erreur de segmentation » et stoppe le programme intercepté.
Bien sûr, le pointeur p
lui-même est modifiable, comme le montre l’expression p = t1
.
Pourquoi ai-je commenté la déclaration de p
avec /* BOF */
? Car lorsqu’on pointe vers un objet que l’on n’est pas censé modifier, il vaut mieux utiliser le mot clé const
afin que le compilateur émette un message en cas de tentative de modification de l’objet pointé.
const char *p = "hello"; /* bien */
p[0] = 'H'; /* FAUX et message: erreur: assignment of read-only location ‘*p’ */
On peut encore noter que le compilateur a le droit de réutiliser les chaines littérales. Par exemple le programme suivant ne nécessite qu’une seule chaine "hello, world\n"
en mémoire.
#include <stdio.h>
int main(void)
{
printf("hello, world\n");
printf("hello, world\n");
printf("world\n");
return 0;
}
Accessoirement, la réutilisation signifie que si la chaine littérale n’est pas protégée contre l’écriture, la modifier peut introduire des bogues plus subtils qu’une erreur de segmentation.
lvalue et register
Sauf quand elle est l’opérande de l’opérateur sizeof, de l’opérateur _Alignof, ou de l’opérateur unaire &, ou est une chaîne de caractères littérale utilisée pour initialiser un tableau, une expression de type "tableau de type" est convertie en une expression de type "pointeur de type" qui pointe sur l’élément initial de l’objet tableau et n’est pas une lvalue. Si le tableau est d’une classe de stockage registre, le comportement est indéterminé.
lvalue
Sous ce terme technique se cache une réalité simple : un tableau ne peut pas être affecté, autrement dit, il ne peut pas être la valeur à gauche (left value) d’une affectation. Et bien sûr, il ne peut pas non plus être incrémenté ni changé d’une quelconque manière.
Pour être tout à fait rigoureux, à la base, un tableau est une lvalue non modifiable. Et la conversion qui donne un pointeur sur l’élément initial produit une simple valeur, qui n’est pas une lvalue.
void f(void)
{
int t1[3], t2[3], *p;
t1 = 0; /* erreur */
t1 = t2; /* erreur */
t1++; /* erreur */
++t1; /* erreur */
t1--; /* erreur */
--t1; /* erreur */
t1 += 1; /* erreur */
t1 -= 1; /* erreur */
}
Notons que si l’on remplace t1
par p
dans chaque instruction ci-dessus, alors il n’y a plus d’erreur, car p
est une lvalue modifiable.
register int t[3]
Ce cas est exotique. Il faut savoir qu’un objet de classe de stockage registre n’existe pas forcément en mémoire : il peut être entièrement contenu dans un registre du processeur. Prendre l’adresse d’un objet de classe de stockage registre n’a donc pas de sens déterminé. Cela rend donc les règles de conversion de tableau en pointeur indéterminée…
Un petit test avec GCC donne ceci.
/* ex.c */
#include <stdio.h>
int main(void)
{
register int t[3] = { 1, 2, 3 };
printf("%d\n", (int)sizeof t);
printf("%d\n", t[1]);
return 0;
}
$ gcc ex.c -pedantic
ex.c: In function ‘main’:
ex.c:8:18: warning: ISO C forbids subscripting 'register' array [-Wpedantic]
8 | printf("%d\n", t[1]);
| ^
$
GCC est plutôt permissif, puisque sans l’option -pedantic
, il compile ce fichier source. Toutefois, selon la norme, la seule chose bien définie qu’on puisse faire avec un tableau register
, c’est prendre sa taille avec sizeof
ou déterminer l’alignement du type de ses membres (C11 §6.7.1 note 121 : « Thus, the only operators that can be applied to an array declared with storage-class specifier register are sizeof and _Alignof »).
Pointeur de pointeur et tableau de tableaux
Cette sous-partie explique en détail le bogue présenté en introduction : ce qui arrive lorsqu’on passe un tableau de tableaux à une fonction attendant un pointeur de pointeur.
Si tu veux voir des fonctions non boguées manipulant des tableaux, tableaux de tableaux, pointeurs sur tableaux, statiquement et dynamiquement, avec de jolis schémas, alors je t’invite à lire le tutoriel Tableaux, pointeurs et allocation dynamique d’Uknow.
Revenons à nos moutons, voici un code source bogué.
/* ex8.c */
#include <stdio.h>
void f(int **p, int x, int y)
{
if (p[x][y] == 42)
puts("coucou");
}
int main(void)
{
int t[2][3] = { { 0, 1, 2 }, { 3, 4, 5 } };
f(t, 0, 1);
return 0;
}
Compilons-le.
$ gcc ex8.c
ex8.c: In function ‘main’:
ex8.c:13:2: warning: passing argument 1 of 'f' from incompatible pointer type [-Wincompatible-pointer-types]
12 | f(t, 0, 1);
| ^
| |
| int (*)[3]
ex8.c:4:6: note: expected 'int **' but argument is of type 'int (*)[3]'
3 | void f(int **p, int x, int y)
| ~~~~~~^
$
Mmmh, un avertissement… Soyons fou, lançons-le.
$ ./a.out
Erreur de segmentation
$
D’où vient ce bon gros plantage ? Quelle instruction cause une erreur de segmentation ? Il s’agit de if (p[x][y] == 42)
. Voyons plus précisément ce qui se passe (sortez vos crayons, il va falloir prendre des notes).
- À l’appel
f(t, 0, 1)
on passe un pointeur sur l’élément initial det
, soit un pointeur surt[0]
, qui est un tableau de 3int
. Or la fonction attend un pointeur sur un pointeur deint
, pas un pointeur sur un tableau de 3int
. Ceci cause l’avertissement du compilateur. - Arrivé dans
void f(int **p, int x, int y)
on ap
, un pointeur de pointeur deint
, qui pointe en fait sur le tableaut[0]
initialisé avec{ 0, 1, 2 }
. - Quand on écrit
p[x][y]
(équivalent à*(p+x)[y]
), l’exécutable charge d’abordp[x]
. -
Je rappelle que si
p
est de typeint**
, alorsp[x]
est de typeint*
.-
Admettons que
p[0]
soit chargé.- Admettons que
sizeof(int)
égalesizeof(int*)
, alors ce qui est chargé est le0
de{0, 1, 2}
, ce qui donne le pointeur nul ! - Admettons que
sizeof(int)
égale la moitié desizeof(int*)
, alors ce qui est chargé est le0
et le1
de{0, 1, 2}
, ce qui donne un pointeur qui a fort peu de chance d’être valide !
- Admettons que
- Admettons donc que
p[0]
soit nul. Alors la suite de l’évaluation de l’expressionp[x][y]
évalue((int*)0)[y] , soit *(((int*)0)+y)
. Et le déréférencement de ce pointeur cause sans surprise une erreur de segmentation. CQFD.
-
Paramètres formels : char*argv[] vs char**argv
Pour connaître toute la vérité sur les tableaux et les pointeurs, il faut encore connaître le cas des déclarations de paramètre de fonction. Dans ce cas, lorsqu’on utilise la syntaxe de déclaration de tableau, on déclare en fait un pointeur ! Il s’ensuit qu’il est impossible de déclarer un paramètre formel de type tableau.
Dans une déclaration de paramètre de fonction, une paire de crochets à droite de l’identificateur déclare un pointeur. S’il y a une expression constante entre les crochets, alors elle est ignorée. J’avais prévenu dans un précédent chapitre que l’on sortirait de la logique sur la fin… On dira qu’il y a des raisons historiques à ce bazar.
Comme démonstration, voici quatre prototypes apparemment différents pour la même fonction. On constate que le compilateur ne dit rien, car en fait ils sont tous équivalents.
/* c.c */
void f(int *p);
void f(int p[]);
void f(int p[3]);
void g(int *p) { f(p); }
void f(int p[8]) { g(++p); }
$ gcc -c -Wall c.c
$
On remarque en outre que p
est bien un pointeur dans f
, et l’on peut donc logiquement l’incrémenter.
Les déclarations bien connues int main(int argc, char *argv[])
et int main(int argc, char **argv)
sont donc parfaitement équivalentes.
Il faut ensuite noter que cette règle n’est pas récursive.
void h(int p[2][3]);
void h(int p[][3]);
void h(int (*p)[3]);
/* void h(int (*p)[4]); /* incompatible */
/* void h(int **p); /* incompatible */
void h(int (*)[]); /* compatible, pointeur de tableau de type incomplet */
Le cas du tableau de type incomplet est compatible. Je pense que le type est complété par les autres déclarations. Toutefois, si cela semble le cas avec GCC, Clang rend le type incomplet en vidant le sens des précédents prototypes. On arrive un peu aux limites des compilateurs.
C89 et C11
Le dernier pour la fin… La différence entre les normes C89 et C11.
Pour comprendre cette différence, il faut reprendre la définition d’une lvalue selon C89.
Je ne connais qu’un cas où un tableau n’est pas une lvalue : lorsqu’une structure contenant un tableau est retournée par valeur par une fonction.
/* lvalue.c */
#include <stdio.h>
struct s {
int tab[3];
const char *str;
};
struct s f(void)
{
struct s res = { { 1, 2, 3 }, "foo" };
return res;
}
int main(void)
{
printf("%d\n", f().tab[0]);
printf("%c\n", f().str[0]);
return 0;
}
$ gcc lvalue.c -Wall -std=c89 -pedantic
lvalue.c: In function ‘main’:
lvalue.c:18:24: warning: ISO C90 forbids subscripting non-lvalue array [-Wpedantic]
18 | printf("%d\n", f().tab[0]);
|
$ gcc lvalue.c -Wall -std=c11 -pedantic
$ gcc lvalue.c
Comme on le voit avec le compilateur GCC, ça ne compile pas en C89 (ou C90, c’est un peu pareil), mais ça compile en C11. Et en C89, seul le tableau tab
pose problème, pas le pointeur str
. À noter que ça compile aussi sans option de compilation, c’est une des nombreuses extensions de GCC activées par défaut.