124 votes

Placement de l'astérisque dans les déclarations de pointeurs

J'ai récemment décidé qu'il fallait que j'apprenne enfin le C/C++, et il y a une chose que je ne comprends pas vraiment à propos des pointeurs ou, plus précisément, de leur définition.

Voici quelques exemples :

  1. int* test;
  2. int *test;
  3. int * test;
  4. int* test,test2;
  5. int *test,test2;
  6. int * test,test2;

Or, si j'ai bien compris, les trois premiers cas font tous la même chose : Test n'est pas un int, mais un pointeur sur un int.

La deuxième série d'exemples est un peu plus délicate. Dans le cas 4, test et test2 seront tous deux des pointeurs sur un int, alors que dans le cas 5, seul test est un pointeur, tandis que test2 est un "vrai" int. Qu'en est-il du cas 6 ? Même chose que dans le cas 5 ?

149voto

Milan Babuškov Points 20423

4, 5 et 6 sont la même chose, seulement test est un pointeur. Si vous voulez deux pointeurs, vous devez utiliser :

int *test, *test2;

Ou, mieux encore (pour que tout soit clair) :

int* test;
int* test2;

53voto

Ates Goral Points 47670

Les espaces blancs autour des astérisques n'ont aucune signification. Les trois signifient la même chose :

int* test;
int *test;
int * test;

Le " int *var1, var2 "est une syntaxe diabolique destinée à semer la confusion dans les esprits et qui devrait être évitée. Elle s'étend à :

int *var1;
int var2;

39voto

Scott Langham Points 17447

De nombreuses lignes directrices en matière de codage recommandent de ne déclarer que une variable par ligne . Cela permet d'éviter toute confusion du type de celle que vous avez eue avant de poser cette question. La plupart des programmeurs C++ avec lesquels j'ai travaillé semblent s'en tenir à cela.


Je sais que c'est un peu une parenthèse, mais j'ai trouvé utile de lire les déclarations à l'envers.

int* test;   // test is a pointer to an int

Cela commence à très bien fonctionner, surtout lorsque vous commencez à déclarer des pointeurs constants et qu'il devient difficile de savoir si c'est le pointeur qui est constant ou si c'est la chose sur laquelle pointe le pointeur qui est constante.

int* const test; // test is a const pointer to an int

int const * test; // test is a pointer to a const int ... but many people write this as  
const int * test; // test is a pointer to an int that's const

35voto

Michael Burr Points 181287

Utiliser le "Règle de la spirale dans le sens des aiguilles d'une montre pour aider à analyser les déclarations C/C++ ;

Il y a trois étapes simples à suivre :

  1. En commençant par l'élément inconnu, se déplacer dans le sens des aiguilles d'une montre ; lorsque vous rencontrez les éléments suivants, remplacez-les par les énoncés anglais correspondants. les énoncés anglais correspondants :

    [X] ou [] : Tableau de taille X de... ou Tableau de taille indéfinie de...

    (type1, type2) : fonction passant par type1 et type2 retournant...

    * : pointeur(s) vers...

  2. Continuez ainsi en spirale ou dans le sens des aiguilles d'une montre jusqu'à ce que tous les jetons aient été recouverts.

  3. Commencez toujours par résoudre les problèmes entre parenthèses !

En outre, les déclarations doivent être placées dans des énoncés distincts lorsque cela est possible (ce qui est le cas la plupart du temps).

25voto

John Bode Points 33046

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 :

  1. Ce n'est pas cohérent avec la syntaxe ;
  2. 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 ;)
  3. 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] );
  4. 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 );
  5. 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.


  1. J'utiliserai la terminologie C - la terminologie C++ est un peu différente, mais les concepts sont en grande partie les mêmes.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X