Il y a trois pièces à ce puzzle.
Le premier élément est que l'espace blanc en C et C++ n'est normalement pas significatif au-delà de la séparation d'éléments adjacents qui sont par ailleurs indiscernables.
Au cours de la phase de prétraitement, le texte source est décomposé en une séquence de jetons - identificateurs, ponctuateurs, littéraux numériques, littéraux de chaîne, etc. Cette séquence de jetons est ensuite analysée du point de vue de la syntaxe et de la signification. Le tokenizer est "gourmand" et construit le plus long token valide possible. Si vous écrivez quelque chose comme
inttest;
le tokenizer ne voit que deux jetons - l'identifiant inttest
suivi du ponctuateur ;
. Il ne reconnaît pas int
en tant que mot-clé distinct à ce stade (cela se fera plus tard dans le processus). Ainsi, pour que la ligne soit lue comme une déclaration d'un entier nommé test
nous devons utiliser des espaces pour séparer les jetons d'identification :
int test;
En *
ne fait pas partie d'un identifiant ; il s'agit d'un jeton (ponctuateur) distinct. Ainsi, si vous écrivez
int*test;
le compilateur voit 4 tokens distincts - int
, *
, test
y ;
. Ainsi, les espaces blancs ne sont pas significatifs dans les déclarations de pointeurs, et tous les éléments de la rubrique
int *test;
int* test;
int*test;
int * test;
sont interprétés de la même manière.
La deuxième pièce du puzzle est le fonctionnement des déclarations en C et C++ 1 . Les déclarations sont divisées en deux parties principales - une séquence de spécificateurs de déclaration (spécificateurs de classe de stockage, spécificateurs de type, qualificateurs de type, etc.), suivie d'une liste séparée par des virgules de déclarants . Dans la déclaration
unsigned long int a[10]={0}, *p=NULL, f(void);
les spécificateurs de déclaration sont unsigned long int
et les déclarants sont a[10]={0}
, *p=NULL
y f(void)
. Le déclarant introduit le nom de la chose déclarée ( a
, p
y f
) ainsi que des informations sur la nature de tableau, de pointeur et de fonction de cette chose. Un déclarant peut également avoir un initialisateur associé.
Le type de a
est un "tableau à 10 éléments de unsigned long int
". Ce type est entièrement spécifié par l'élément combinaison des spécificateurs de déclaration et du déclarant, et la valeur initiale est spécifiée avec l'initialisateur ={0}
. De même, le type de p
est un "pointeur sur unsigned long int
"Ce type est spécifié par la combinaison des spécificateurs de déclaration et du déclarant, et est initialisé à NULL
. Et le type de f
est une "fonction qui renvoie unsigned long int
"par le même raisonnement.
C'est la clé - il n'y a pas de "pointeur vers" spécificateur de type Tout comme il n'y a pas de spécificateur de type "array-of", il n'y a pas de spécificateur de type "function-returning". Nous ne pouvons pas déclarer un tableau comme
int[10] a;
car l'opérande de la fonction []
L'opérateur est a
no int
. De même, dans la déclaration
int* p;
l'opérande de *
es p
no int
. Mais comme l'opérateur d'indirection est unaire et que les espaces blancs ne sont pas significatifs, le compilateur ne se plaindra pas si nous l'écrivons de cette manière. Cependant, il est toujours interprété comme int (*p);
.
Par conséquent, si vous écrivez
int* p, q;
l'opérande de *
es p
Il sera donc interprété comme
int (*p), q;
Ainsi, tous les
int *test1, test2;
int* test1, test2;
int * test1, test2;
font la même chose - dans les trois cas, test1
est l'opérande de *
et a donc le type "pointeur sur int
", tandis que test2
a le type int
.
Les déclarants peuvent être arbitrairement complexes. Vous pouvez avoir des tableaux de pointeurs :
T *a[N];
vous pouvez avoir des pointeurs sur des tableaux :
T (*a)[N];
vous pouvez avoir des fonctions qui renvoient des pointeurs :
T *f(void);
vous pouvez avoir des pointeurs vers des fonctions :
T (*f)(void);
vous pouvez avoir des tableaux de pointeurs vers des fonctions :
T (*a[N])(void);
vous pouvez avoir des fonctions qui renvoient des pointeurs vers des tableaux :
T (*f(void))[N];
vous pouvez avoir des fonctions renvoyant des pointeurs vers des tableaux de pointeurs vers des fonctions renvoyant des pointeurs vers des tableaux de pointeurs vers des tableaux de pointeurs vers des tableaux de pointeurs vers des tableaux de pointeurs. T
:
T *(*(*f(void))[N])(void); // yes, it's eye-stabby. Welcome to C and C++.
et ensuite vous avez signal
:
void (*signal(int, void (*)(int)))(int);
qui se lit comme suit
signal -- signal
signal( ) -- is a function taking
signal( ) -- unnamed parameter
signal(int ) -- is an int
signal(int, ) -- unnamed parameter
signal(int, (*) ) -- is a pointer to
signal(int, (*)( )) -- a function taking
signal(int, (*)( )) -- unnamed parameter
signal(int, (*)(int)) -- is an int
signal(int, void (*)(int)) -- returning void
(*signal(int, void (*)(int))) -- returning a pointer to
(*signal(int, void (*)(int)))( ) -- a function taking
(*signal(int, void (*)(int)))( ) -- unnamed parameter
(*signal(int, void (*)(int)))(int) -- is an int
void (*signal(int, void (*)(int)))(int); -- returning void
et cela ne fait qu'effleurer la surface de ce qui est possible. Mais remarquez que les notions de tableau, de pointeur et de fonction font toujours partie du déclarateur, et non du spécificateur de type.
Une chose à laquelle il faut faire attention - const
peut modifier à la fois le type de pointeur et le type pointé :
const int *p;
int const *p;
Les deux déclarations ci-dessus p
comme un pointeur sur un const int
objet. Vous pouvez écrire une nouvelle valeur dans p
en le faisant pointer sur un autre objet :
const int x = 1;
const int y = 2;
const int *p = &x;
p = &y;
mais vous ne pouvez pas écrire sur l'objet pointé :
*p = 3; // constraint violation, the pointed-to object is const
Cependant,
int * const p;
déclare p
en tant que const
pointeur sur une valeur non-const int
; vous pouvez écrire à l'organisme p
pointe vers
int x = 1;
int y = 2;
int * const p = &x;
*p = 3;
mais vous ne pouvez pas définir p
pour pointer vers un autre objet :
p = &y; // constraint violation, p is const
Ce qui nous amène à la troisième pièce du puzzle : la raison pour laquelle les déclarations sont structurées de cette manière.
L'objectif est que la structure d'une déclaration reflète étroitement la structure d'une expression dans le code ("la déclaration imite l'utilisation"). Par exemple, supposons que nous ayons un tableau de pointeurs vers int
nommée ap
et nous voulons accéder au int
pointée par la valeur i
Le "troisième élément". Nous accédons à cette valeur de la manière suivante :
printf( "%d", *ap[i] );
En expression *ap[i]
a le type int
; ainsi, la déclaration de ap
s'écrit
int *ap[N]; // ap is an array of pointer to int, fully specified by the combination
// of the type specifier and declarator
Le déclarateur *ap[N]
a la même structure que l'expression *ap[i]
. Les opérateurs *
y []
se comportent de la même manière dans une déclaration que dans une expression - []
a une priorité plus élevée que l'unaire *
, de sorte que l'opérande de *
es ap[N]
(il est interprété comme *(ap[N])
).
Autre exemple, supposons que nous ayons un pointeur sur un tableau de int
nommée pa
et nous voulons accéder à la valeur de l'élément i
Le "troisième élément". Nous l'écrirons sous la forme suivante
printf( "%d", (*pa)[i] );
Le type de l'expression (*pa)[i]
es int
La déclaration s'écrit donc
int (*pa)[N];
Là encore, les mêmes règles de préséance et d'associativité s'appliquent. Dans ce cas, nous ne voulons pas déréférencer l'élément i
ème élément de pa
, nous voulons accéder au i
Le troisième élément de ce qui pa
pointe vers Nous devons donc regrouper explicitement les *
l'opérateur avec pa
.
En *
, []
y ()
font tous partie du programme expression dans le code, de sorte qu'ils font tous partie de la déclarant dans la déclaration. Le déclarateur indique comment utiliser l'objet dans une expression. Si vous avez une déclaration comme int *p;
qui vous indique que l'expression *p
dans votre code produira un int
valeur. Par extension, il vous indique que l'expression p
produit une valeur de type "pointeur sur int
"ou int *
.
Qu'en est-il de choses telles que les plâtres et les sizeof
où nous utilisons des éléments tels que (int *)
ou sizeof (int [10])
ou des choses de ce genre ? Comment lire quelque chose comme
void foo( int *, int (*)[10] );
Il n'y a pas de déclarateur, les *
y []
qui modifient directement le type ?
Eh bien, non - il y a toujours un déclarant, mais avec un identifiant vide (connu sous le nom de Déclarateur abstrait ). Si nous représentons un identificateur vide par le symbole λ, nous pouvons lire ces choses comme suit (int *λ)
, sizeof (int λ[10])
et
void foo( int *λ, int (*λ)[10] );
et elles se comportent exactement comme n'importe quelle autre déclaration. int *[10]
représente un tableau de 10 pointeurs, tandis que int (*)[10]
représente un pointeur sur un tableau.
Et maintenant, la partie de cette réponse consacrée à l'opinion. Je n'aime pas beaucoup la convention du C++ qui consiste à déclarer les pointeurs simples en tant que
T* p;
et le considérer mauvaise pratique pour les raisons suivantes :
- Ce n'est pas cohérent avec la syntaxe ;
- Elle introduit de la confusion (comme en témoignent cette question, tous les doublons de cette question, les questions sur la signification de
T* p, q;
tous les doublons à Ceux-ci questions, etc ;)
- Il n'y a pas de cohérence interne - déclarer un tableau de pointeurs en tant que
T* a[N]
est asymétrique à l'usage (à moins que vous n'ayez l'habitude d'écrire * a[i]
);
- Elle ne peut pas être appliquée aux types pointeur vers tableau ou pointeur vers fonction (à moins que vous ne créiez un typedef juste pour pouvoir appliquer l'attribut
T* p
convention proprement dite, ce qui... non );
- La raison invoquée - "cela met l'accent sur le caractère de pointeur de l'objet" - est fallacieuse. Elle ne peut s'appliquer aux types de tableaux ou de fonctions, et je pense que ces qualités sont tout aussi importantes à souligner.
En fin de compte, il s'agit simplement d'une réflexion confuse sur le fonctionnement des systèmes de types des deux langages.
Il y a de bonnes raisons de déclarer les articles séparément ; contourner une mauvaise pratique ( T* p, q;
) n'en fait pas partie. Si vous écrivez vos déclarateurs correctement ( T *p, q;
), vous risquez moins de créer la confusion.
Je considère que cela revient à écrire délibérément tous vos simples for
boucles comme
i = 0;
for( ; i < N; )
{
...
i++;
}
Syntaxiquement valide, mais déroutant, et l'intention est susceptible d'être mal interprétée. Toutefois, la T* p;
La convention est ancrée dans la communauté C++, et je l'utilise dans mon propre code C++ parce que la cohérence dans la base de code est une bonne chose, mais cela me démange à chaque fois que je le fais.
- J'utiliserai la terminologie C - la terminologie C++ est un peu différente, mais les concepts sont en grande partie les mêmes.