Pourquoi l'astérisque se trouve-t-il avant le nom de la variable plutôt qu'après le type?

194

Pourquoi la plupart des programmeurs C nomment-ils des variables comme ceci:

int *myVariable;

plutôt que comme ça:

int* myVariable;

Les deux sont valides. Il me semble que l'astérisque fait partie du type et non du nom de la variable. Quelqu'un peut-il expliquer cette logique?

WBlasko
la source
1
Le deuxième style semble plus intuitif en général, mais le premier est la voie à suivre pour éviter les bogues liés au type dans le code. Si vous êtes vraiment attaché à ce dernier style, vous pouvez toujours y aller typedefs, mais cela ajoutera une complexité inutile, à mon humble avis.
Cloud

Réponses:

256

Ils sont EXACTEMENT équivalents. Cependant, dans

int *myVariable, myVariable2;

Il semble évident que myVariable a le type int * , tandis que myVariable2 a le type int . Dans

int* myVariable, myVariable2;

il peut sembler évident que les deux sont de type int * , mais ce n'est pas correct comme l' myVariable2a fait le type int .

Par conséquent, le premier style de programmation est plus intuitif.

Luiscubal
la source
138
peut-être, mais je ne mélangerais pas les types dans une seule déclaration.
BobbyShaftoe
17
@BobbyShaftoe D'accord. Même après avoir lu tous les arguments ici, je m'en tiens à int* someVardes projets personnels. Cela a plus de sens.
Alyssa Haroldsen le
32
@Kupiakos Cela n'a plus de sens que lorsque vous apprenez la syntaxe de déclaration de C basée sur "les déclarations suivent l'utilisation". Les déclarations utilisent exactement la même syntaxe que l'utilisation des variables de même type. Lorsque vous déclarez un tableau de ints, il ne ressemble pas à : int[10] x. Ce n'est tout simplement pas la syntaxe de C. La grammaire analyse explicitement comme:, int (*x)et non comme (int *) x, donc placer l'astérisque à gauche est tout simplement trompeur et basé sur une mauvaise compréhension de la syntaxe de déclaration C.
Peaker
8
Correction: par conséquent, vous ne devez jamais déclarer plus d'une variable sur une seule ligne. En général, vous ne devriez pas motiver un certain style de codage basé sur un autre style de codage non lié, mauvais et dangereux.
Lundin
7
C'est pourquoi je m'en tiens à une variable par déclaration de pointeur. Il n'y a absolument aucune confusion si vous le faites à la int* myVariable; int myVariable2;place.
Patrick Roberts
124

Si vous le regardez d'une autre manière, il *myVariableest de type int, ce qui a du sens.

biozinc
la source
6
C'est mon explication préférée, et fonctionne bien car elle explique les bizarreries de déclaration de C en général - même la syntaxe du pointeur de fonction dégoûtante et gnarly.
Benjamin Pollack
12
C'est plutôt chouette, car vous pouvez imaginer qu'il n'y a pas de types de pointeurs réels. Il n'y a que des variables qui, lorsqu'elles sont correctement référencées ou déréférencées, vous donnent l'un des types primitifs.
biozinc
1
En fait, '* myVariable' peut être de type NULL. Pour aggraver les choses, il pourrait s'agir simplement d'un emplacement de mémoire aléatoire.
qonf
4
qonf: NULL n'est pas un type. myVariablepeut être NULL, auquel cas *myVariableprovoque une erreur de segmentation, mais il n'y a pas de type NULL.
Daniel Roethlisberger
28
Ce point peut être trompeur dans un tel contexte:, int x = 5; int *pointer = &x;parce qu'il suggère que nous définissons l'int *pointerà une certaine valeur, pas le pointerlui - même.
rafalcieslak
40

Parce que le * dans cette ligne se lie plus étroitement à la variable qu'au type:

int* varA, varB; // This is misleading

Comme @Lundin le souligne ci-dessous, const ajoute encore plus de subtilités à réfléchir. Vous pouvez contourner complètement cela en déclarant une variable par ligne, ce qui n'est jamais ambigu:

int* varA;
int varB;

L'équilibre entre un code clair et un code concis est difficile à trouver - une douzaine de lignes redondantes int a;ne sont pas bonnes non plus. Pourtant, je par défaut à une déclaration par ligne et je m'inquiète de combiner le code plus tard.

ojrac
la source
2
Eh bien, le premier exemple trompeur est à mes yeux une erreur de conception. Si je pouvais, je supprimerais complètement cette manière de déclaration de C et je ferais en sorte que les deux soient de type int *.
Adam Bajger
1
"le * se lie plus étroitement à la variable qu'au type" C'est un argument naïf. Considérez int *const a, b;. Où le * "lie"? Le type de aest int* const, alors comment dire que le * appartient à la variable alors qu'il fait partie du type lui-même?
Lundin le
1
Spécifique à la question, ou couvrant tous les cas: choisissez-en un. Je ferai une note, mais c'est un autre bon argument pour ma dernière suggestion: une déclaration par ligne réduit les possibilités de gâcher cela.
ojrac
39

Ce que personne n'a mentionné ici jusqu'à présent, c'est que cet astérisque est en fait "l' opérateur de déréférencement " en C.

*a = 10;

La ligne ci-dessus ne signifie pas que je veux attribuer 10à a, cela signifie que je veux attribuer 10à n'importe quel emplacement mémoire pointé a. Et je n'ai jamais vu personne écrire

* a = 10;

Avez-vous? L' opérateur de déréférence est donc presque toujours écrit sans espace. C'est probablement pour le distinguer d'une multiplication coupée sur plusieurs lignes:

x = a * b * c * d
  * e * f * g;

Ce *eserait trompeur, n'est-ce pas?

Ok, maintenant que signifie réellement la ligne suivante:

int *a;

La plupart des gens diraient:

Cela signifie que ac'est un pointeur vers une intvaleur.

C'est techniquement correct, la plupart des gens aiment le voir / le lire de cette façon et c'est ainsi que les normes C modernes le définiraient (notez que le langage C lui-même est antérieur à toutes les normes ANSI et ISO). Mais ce n'est pas la seule façon de voir les choses. Vous pouvez également lire cette ligne comme suit:

La valeur déréférencée de aest de type int.

Donc, en fait, l'astérisque dans cette déclaration peut également être vu comme un opérateur de déréférencement, ce qui explique également son emplacement. Et c'est aqu'un pointeur n'est pas vraiment déclaré du tout, c'est implicite par le fait que la seule chose que vous pouvez réellement déréférencer est un pointeur.

La norme C ne définit que deux significations pour l' *opérateur:

  • opérateur d'indirection
  • opérateur de multiplication

Et l'indirection n'est qu'une seule signification, il n'y a pas de signification supplémentaire pour déclarer un pointeur, il y a juste une indirection, ce que fait l'opération de déréférencement, elle effectue un accès indirect, donc aussi dans une instruction comme int *a;celle-ci est un accès indirect ( *signifie accès indirect) et donc la deuxième déclaration ci-dessus est beaucoup plus proche de la norme que la première.

Mecki
la source
7
Merci de m'avoir évité d'écrire une autre réponse ici. BTW Je lis généralement le int a, *b, (*c)();comme quelque chose comme "déclarer les objets suivants comme int: l'objet a, l'objet pointé par bet l'objet renvoyé par la fonction pointée par c".
Antti Haapala
1
Le *in int *a;n'est pas un opérateur, et ce n'est pas un déréférencement a(qui n'est même pas encore défini)
MM
1
@MM Veuillez nommer la page et le numéro de ligne de toute norme ISO C où cette norme dit que l'astérisque peut être autre chose que la multiplication ou l'indirection. Il ne montre que la "déclaration du pointeur" par exemple, il ne définit nulle part une troisième signification pour l'astérisque (et il ne définit aucune signification, ce ne serait pas un opérateur). Oh, et je n'ai nulle part prétendu que quelque chose était "défini", vous l'avez inventé. Ou comme Jonathan Leffler hat l'a dit, dans le standard C, * est toujours "grammaire", ça ne fait pas partie des spécificateurs de déclaration listés (donc ça ne fait pas partie d'une déclaration, donc ça doit être un opérateur)
Mecki
1
@MM 6.7.6.1 ne montre la déclaration que par exemple, exactement comme je l'ai dit, cela ne donne aucun sens à *(il ne donne un sens qu'à l'expression totale, pas à l' *intérieur de l'expression!). Il dit que "int a;" déclare un pointeur, ce qu'il fait, n'a jamais prétendu autrement, mais sans signification donnée *, le lire comme * la valeur déréférencée de ais an int est toujours totalement valide, car cela a la même signification factuelle. Rien, vraiment rien d'écrit dans 6.7.6.1 ne contredirait cette déclaration.
Mecki
2
Il n'y a pas d '"expression totale". int *a;est une déclaration, pas une expression. an'est pas déréférencé par int *a;. an'existe même pas encore au moment où le *est en cours de traitement. Pensez-vous que int *a = NULL;c'est un bogue parce qu'il déréférence un pointeur nul?
MM
18

Je vais sortir sur un membre ici et dire que il y a une réponse claire à cette question , à la fois pour les déclarations variables et pour les types de paramètres et de retour, qui est que l'astérisque devrait aller à côté du nom: int *myVariable;. Pour comprendre pourquoi, regardez comment vous déclarez d'autres types de symboles en C:

int my_function(int arg); pour une fonction;

float my_array[3] pour un tableau.

Le modèle général, appelé déclaration suit l'utilisation , est que le type d'un symbole est divisé en la partie avant le nom et les parties autour du nom, et ces parties autour du nom imitent la syntaxe que vous utiliseriez pour obtenir un valeur du type à gauche:

int a_return_value = my_function(729);

float an_element = my_array[2];

et: int copy_of_value = *myVariable;.

C ++ jette une clé dans les travaux avec des références, car la syntaxe au point où vous utilisez les références est identique à celle des types valeur, vous pouvez donc affirmer que C ++ adopte une approche différente de C. D'autre part, C ++ conserve la même comportement de C dans le cas des pointeurs, donc les références sont vraiment les plus étranges à cet égard.

Injektilo
la source
11

C'est juste une question de préférence.

Lorsque vous lisez le code, la distinction entre les variables et les pointeurs est plus facile dans le second cas, mais cela peut prêter à confusion lorsque vous mettez à la fois des variables et des pointeurs d'un type commun sur une seule ligne (ce qui est souvent découragé par les directives du projet, car diminue la lisibilité).

Je préfère déclarer les pointeurs avec leur signe correspondant à côté du nom du type, par exemple

int* pMyPointer;
macbirdie
la source
3
La question concerne C, où il n'y a pas de références.
Antti Haapala
Merci de l'avoir signalé, bien que la question ne portait pas sur les pointeurs ou les références, mais sur le formatage du code, en gros.
macbirdie
9

Un grand gourou a dit un jour: «Lisez-le à la manière du compilateur, vous devez».

http://www.drdobbs.com/conversationsa-midsummer-nights-madness/184403835

Certes, c'était sur le sujet du placement des const, mais la même règle s'applique ici.

Le compilateur le lit comme suit:

int (*a);

pas aussi:

(int*) a;

Si vous prenez l'habitude de placer l'étoile à côté de la variable, cela facilitera la lecture de vos déclarations. Il évite également les maux de vue tels que:

int* a[10];

-- Éditer --

Pour expliquer exactement ce que je veux dire quand je dis que c'est analysé comme int (*a), cela signifie que cela *se lie plus étroitement à ace qu'il ne le fait à int, de la même manière que dans l'expression 4 + 3 * 7 3se lie plus étroitement à 7cela en 4raison de la priorité plus élevée de *.

Avec des excuses pour l'art ascii, un synopsis de l'AST pour l'analyse int *aressemble à peu près à ceci:

      Declaration
      /         \
     /           \
Declaration-      Init-
Secifiers       Declarator-
    |             List
    |               |
    |              ...
  "int"             |
                Declarator
                /       \
               /        ...
           Pointer        \
              |        Identifier
              |            |
             "*"           |
                          "a"

Comme il est clairement montré, *se lie plus étroitement à apuisque leur ancêtre commun est Declarator, alors que vous devez monter tout en haut de l'arbre Declarationpour trouver un ancêtre commun qui implique le int.

dgnuff
la source
Non, le compilateur lit très certainement le type comme (int*) a.
Lundin le
@Lundin C'est le grand malentendu que la plupart des programmeurs C ++ modernes ont. L'analyse d'une déclaration de variable ressemble à ceci. Étape 1. Lisez le premier jeton, qui devient le "type de base" de la déclaration. intdans ce cas. Étape 2. Lisez une déclaration, y compris les décorations de type. *adans ce cas. Étape 3 Lisez le caractère suivant. S'il s'agit d'une virgule, consommez-le et revenez à l'étape 2. Si le point-virgule s'arrête. Si quelque chose d'autre jette une erreur de syntaxe. ...
dgnuff
@Lundin ... Si l'analyseur le lisait comme vous le suggérez, alors nous serions en mesure d'écrire int* a, b;et d'obtenir une paire de pointeurs. Le point que je fais est que la *liaison à la variable et est analysée avec elle, pas avec le type pour former le "type de base" de la déclaration. C'est aussi une des raisons pour lesquelles les typedefs ont été introduits pour permettre la typedef int *iptr; iptr a, b;création de quelques pointeurs. En utilisant un typedef, vous pouvez lier le *au int.
dgnuff
1
... Certes, le compilateur "combine" le type de base du Declaration-Specifieravec les "décorations" du Declaratorpour arriver au type final pour chaque variable. Cependant, il ne "déplace" pas les décorations vers le spécificateur de déclaration, sinon int a[10], b;cela produirait des résultats complètement ridicules, il analyse int *a, b[10];comme int *a , b[10] ;. Il n'y a pas d'autre moyen de le décrire qui ait du sens.
dgnuff
1
Eh bien, la partie importante ici n'est pas vraiment la syntaxe ou l'ordre d'analyse du compilateur, mais que le type de int*aest finalement lu par le compilateur comme: " ahas type int*". C'est ce que je veux dire avec mon commentaire initial.
Lundin
4

Les personnes qui préfèrent int* x;essaient de forcer leur code dans un monde fictif où le type est à gauche et l'identifiant (nom) est à droite.

Je dis "fictif" parce que:

En C et C ++, dans le cas général, l'identifiant déclaré est entouré des informations de type.

Cela peut sembler fou, mais vous savez que c'est vrai. Voici quelques exemples:

  • int main(int argc, char *argv[])signifie " mainest une fonction qui prend un intet un tableau de pointeurs vers charet renvoie un int." En d'autres termes, la plupart des informations de type se trouvent à droite. Certaines personnes pensent que les déclarations de fonctions ne comptent pas parce qu'elles sont en quelque sorte «spéciales». OK, essayons une variable.

  • void (*fn)(int)signifie fnest un pointeur vers une fonction qui prend un intet ne renvoie rien.

  • int a[10]déclare «a» comme un tableau de 10 ints.

  • pixel bitmap[height][width].

  • De toute évidence, j'ai sélectionné des exemples avec beaucoup d'informations de type à droite pour faire valoir mon point. Il y a beaucoup de déclarations où la plupart - sinon la totalité - du type se trouve à gauche, comme struct { int x; int y; } center.

Cette syntaxe de déclaration est née du désir de K&R d'avoir des déclarations reflétant l'usage. La lecture de déclarations simples est intuitive, et la lecture de déclarations plus complexes peut être maîtrisée en apprenant la règle droite-gauche-droite (appelez parfois la règle en spirale ou simplement la règle droite-gauche).

C est assez simple pour que de nombreux programmeurs C adoptent ce style et écrivent des déclarations simples comme int *p.

En C ++, la syntaxe est devenue un peu plus complexe (avec des classes, des références, des modèles, des classes enum) et, en réaction à cette complexité, vous verrez plus d'efforts pour séparer le type de l'identifiant dans de nombreuses déclarations. En d'autres termes, vous pourriez voir plus de int* pdéclarations de style -style si vous extrayez une large bande de code C ++.

Dans les deux langues, vous pouvez toujours avoir le type sur le côté gauche des déclarations de variables en (1) ne déclarant jamais plusieurs variables dans la même instruction, et (2) en utilisant typedefs (ou les déclarations d'alias, qui, ironiquement, mettent l'alias identificateurs à gauche des types). Par exemple:

typedef int array_of_10_ints[10];
array_of_10_ints a;
Adrian McCarthy
la source
Petite question, pourquoi ne peut pas void (* fn) (int) signifie "fn est une fonction qui accepte un int, et renvoie (void *)?
Soham Dongargaonkar
1
@ a3y3: Parce que les parenthèses (*fn)gardent le pointeur associé à fnplutôt qu'au type de retour.
Adrian McCarthy
Je l'ai. Je pense que c'est assez important pour être ajouté dans votre réponse, je suggérerai une modification bientôt.
Soham Dongargaonkar
2
Je pense que cela encombre la réponse sans ajouter beaucoup de valeur. C'est simplement la syntaxe de la langue. La plupart des gens qui demandent pourquoi la syntaxe est de cette façon connaissent probablement déjà les règles. Quiconque est confus sur ce point peut voir la clarification dans ces commentaires.
Adrian McCarthy
3

Parce que cela a plus de sens lorsque vous avez des déclarations comme:

int *a, *b;
Greg Rogers
la source
5
C'est littéralement un exemple de pose de question. Non, cela n'a pas plus de sens de cette façon. "int * a, b" pourrait tout aussi bien faire des deux pointeurs.
MichaelGG
2
@MichaelGG Vous avez la queue qui remue le chien là-bas. Bien sûr, K&R aurait pu spécifier que int* a, b;fait bun pointeur vers int. Mais ils ne l'ont pas fait. Et avec raison. Dans le cadre de votre système proposé quel est le type de bla déclaration suivante: int* a[10], b;?
dgnuff
2

Un grand nombre des arguments de cette rubrique sont simplement subjectifs et l'argument concernant «l'étoile se lie au nom de la variable» est naïf. Voici quelques arguments qui ne sont pas que des opinions:


Les qualificatifs de type de pointeur oublié

Formellement, l'étoile n'appartient ni au type ni au nom de la variable, elle fait partie de son propre élément grammatical nommé pointer . La syntaxe formelle C (ISO 9899: 2018) est:

(6.7) déclaration:
spécificateurs de déclaration init-declarator-list opt ;

Où les spécificateurs de déclaration contiennent le type (et le stockage) et la liste init-declarator-list contient le pointeur et le nom de la variable. Ce que nous voyons si nous disséquons davantage la syntaxe de cette liste de déclarateurs:

(6.7.6) déclarateur:
pointeur opt déclarateur direct
...
(6.7.6) pointeur:
* type-qualifier-list opt
* type-qualifier-list opt pointeur

Lorsqu'un déclarateur est la déclaration complète, un déclarateur direct est l'identificateur (nom de la variable) et un pointeur est l'étoile suivie d'une liste de qualificatifs de type facultatifs appartenant au pointeur lui-même.

Ce qui rend incohérents les différents arguments de style concernant "l'étoile appartient à la variable", c'est qu'ils ont oublié ces qualificatifs de type pointeur. int* const x, int *const xou int*const x?

Considérez int *const a, b;, quels sont les types de aet b? Pas si évident que "l'étoile appartient à la variable" plus longtemps. Au contraire, on commencerait à se demander où constappartient le.

Vous pouvez certainement faire un argument valable selon lequel l'étoile appartient au qualificatif de type pointeur, mais pas beaucoup au-delà.

La liste des qualificatifs de type pour le pointeur peut poser des problèmes à ceux qui utilisent le int *astyle. Ceux qui utilisent des pointeurs à l'intérieur d'un typedef(ce que nous ne devrions pas, très mauvaise pratique!) Et pensent que "l'étoile appartient au nom de la variable" ont tendance à écrire ce bogue très subtil:

    /*** bad code, don't do this ***/
    typedef int *bad_idea_t; 
    ...
    void func (const bad_idea_t *foo);

Cela compile proprement. Maintenant, vous pourriez penser que le code est rendu correct. Pas si! Ce code est accidentellement une fausse exactitude de const.

Le type de fooest en fait int*const*- le pointeur le plus extérieur a été rendu en lecture seule, pas les données pointées. Donc, à l'intérieur de cette fonction, nous pouvons le faire **foo = n;et cela changera la valeur de la variable dans l'appelant.

En effet, dans l'expression const bad_idea_t *foo, le *n'appartient pas au nom de la variable ici! En pseudo-code, cette déclaration de paramètre doit être lue comme const (bad_idea_t *) fooet non comme (const bad_idea_t) *foo. L'étoile appartient au type pointeur masqué dans ce cas - le type est un pointeur et un pointeur qualifié const est écrit comme *const.

Mais alors la racine du problème dans l'exemple ci-dessus est la pratique de cacher des pointeurs derrière un typedefet non le *style.


Concernant la déclaration de plusieurs variables sur une seule ligne

La déclaration de plusieurs variables sur une seule ligne est largement reconnue comme une mauvaise pratique 1) . CERT-C résume bien cela comme:

DCL04-C. Ne déclarez pas plus d'une variable par déclaration

Il suffit de lire l'anglais, accepte alors de bon sens que la déclaration devrait être une déclaration.

Et peu importe si les variables sont des pointeurs ou non. Déclarer chaque variable sur une seule ligne rend le code plus clair dans presque tous les cas.

Donc, l'argument selon lequel le programmeur est confus int* a, best mauvais. La racine du problème est l'utilisation de plusieurs déclarateurs, et non le placement du fichier *. Quel que soit le style, vous devriez plutôt écrire ceci:

    int* a; // or int *a
    int b;

Un autre argument sain mais subjectif serait que, étant donné que int* ale type de aest indiscutable int*, l'étoile appartient donc au qualificatif de type.

Mais au fond, ma conclusion est que bon nombre des arguments présentés ici sont simplement subjectifs et naïfs. Vous ne pouvez pas vraiment faire un argument valable pour l'un ou l'autre style - c'est vraiment une question de préférence personnelle subjective.


1) CERT-C DCL04-C .

Lundin
la source
Plutôt amusant, si vous lisez cet article que j'ai lié, il y a une courte section sur le sujet de typedef int *bad_idea_t; void func(const bad_idea_t bar); Comme l'enseigne le grand prophète Dan Saks "Si vous placez toujours le constaussi loin que possible à droite, sans changer le sens sémantique", cela cesse complètement être un problème. Cela rend également vos constdéclarations plus cohérentes à lire. "Tout ce qui est à droite du mot const est ce qui est const, tout à gauche est son type." Cela s'appliquera à tous les consts dans une déclaration. Essayez-le avecint const * * const x;
dgnuff
1

Pour déclarer plusieurs pointeurs sur une seule ligne, je préfère int* a, * b;lequel déclare plus intuitivement "a" comme un pointeur vers un entier, et ne mélange pas les styles en déclarant également "b". Comme quelqu'un l'a dit, je ne déclarerais pas deux types différents dans la même déclaration de toute façon.

heyitsluke
la source
-2

Considérer

int *x = new int();

Cela ne se traduit pas par

int *x;
*x = new int();

Cela se traduit en fait par

int *x;
x = new int();

Cela rend la int *xnotation quelque peu incohérente.

Urquhart
la source
newest un opérateur C ++. Sa question est étiquetée "C" et non "C ++".
Sapphire_Brick le