web-dev-qa-db-fra.com

Pourquoi l'opérateur flèche (->) en C existe-t-il?

L'opérateur point (.) est utilisé pour accéder à un membre d'une structure, tandis que l'opérateur flèche (->) en C est utilisé pour accéder à un membre d'une structure qui est référencé par le pointeur en question. .

Le pointeur lui-même ne contient aucun membre auquel on pourrait accéder à l'aide de l'opérateur point (il ne s'agit en fait que d'un nombre décrivant un emplacement dans la mémoire virtuelle, de sorte qu'il ne comporte aucun membre). Donc, il n'y aurait pas d'ambiguïté si nous venons de définir l'opérateur point pour déréférencer automatiquement le pointeur s'il est utilisé sur un pointeur (une information connue du compilateur au moment de la compilation).

Alors pourquoi les créateurs de langage ont-ils décidé de compliquer les choses en ajoutant cet opérateur apparemment inutile? Quelle est la grande décision de conception?

243
Askaga

J'interpréterai votre question comme deux questions: 1) pourquoi -> existe-t-il même et 2) pourquoi . ne déréférence pas automatiquement le pointeur. Les réponses aux deux questions ont des racines historiques.

Pourquoi -> existe-t-il même?

Dans l’une des toutes premières versions du langage C (que je qualifierai de CRM pour " Manuel de référence C ", fournie avec la 6e édition Unix en mai 1975), l’opérateur -> avait très signification exclusive, pas synonyme de * et . combinaison

Le langage C décrit par CRM était très différent du C moderne à de nombreux égards. Dans la structure CRM, les membres de la structure ont mis en œuvre le concept global byte offset, qui peut être ajouté à toute valeur d'adresse sans restriction de type. C'est à dire. tous les noms de tous les membres de la structure ont une signification globale indépendante (et doivent donc être uniques). Par exemple, vous pouvez déclarer

struct S {
  int a;
  int b;
};

et nom a correspond au décalage 0, alors que nom b correspond au décalage 2 (en supposant que int type de taille 2 et aucun remplissage). La langue requise pour tous les membres de toutes les structures de l'unité de traduction porte un nom unique ou correspond à la même valeur de décalage. Par exemple. dans la même unité de traduction, vous pouvez en outre déclarer

struct X {
  int a;
  int x;
};

et ce serait OK, puisque le nom a signifierait toujours l'offset 0. Mais cette déclaration supplémentaire

struct Y {
  int b;
  int a;
};

serait formellement invalide, car il a tenté de "redéfinir" a comme décalage 2 et b comme décalage 0.

Et c’est là que l’opérateur -> entre en jeu. Étant donné que chaque nom de membre de structure a sa propre signification globale autonome, les expressions supportées par la langue comme celles-ci

int i = 5;
i->b = 42;  /* Write 42 into `int` at address 7 */
100->a = 0; /* Write 0 into `int` at address 100 */

La première affectation a été interprétée par le compilateur comme "prenez l'adresse 5, ajoutez-lui un décalage 2 et affectez 42 à la valeur int à l'adresse résultante". C'est à dire. ce qui précède attribuerait 42 à int valeur à l'adresse 7. Notez que cette utilisation de -> ne se souciait pas du type de l'expression du côté gauche. Le côté gauche a été interprété comme une adresse numérique de valeur (que ce soit un pointeur ou un entier).

Ce type de tromperie n’était pas possible avec la combinaison * et .. Tu ne pouvais pas faire

(*i).b = 42;

depuis *i est déjà une expression invalide. L'opérateur *, puisqu'il est distinct de ., impose des exigences de type plus strictes à son opérande. Pour permettre de contourner cette limitation, CRM a introduit l'opérateur ->, indépendant du type de l'opérande de gauche.

Comme Keith l'a noté dans les commentaires, la différence entre la combinaison -> et * + . correspond à ce que CRM appelle "assouplissement de l'exigence" au 7.1.8: Sauf pour l'assouplissement de l'exigence voulant que E1 soit de type pointeur, l'expression E1−>MOS est exactement équivalente à (*E1).MOS

Plus tard, dans K & R C, de nombreuses fonctionnalités initialement décrites dans CRM ont été considérablement retravaillées. L'idée de "membre struct comme identificateur global d'offset" a été complètement supprimée. Et la fonctionnalité de l'opérateur -> est devenue totalement identique à la fonctionnalité de la combinaison * et ..

Pourquoi . ne peut-il pas déréférencer automatiquement le pointeur?

De nouveau, dans la version CRM du langage, l'opérande gauche de l'opérateur . devait être une lvalue. C'était la condition seulement ​​imposée à cet opérande (et c'est ce qui le rendait différent de ->, comme expliqué ci-dessus). Notez que CRM not ​​a besoin que l'opérande gauche de . ait un type struct. Il fallait simplement que ce soit une valeur, any lvalue. Cela signifie que dans la version CRM de C, vous pouvez écrire un code comme celui-ci.

struct S { int a, b; };
struct T { float x, y, z; };

struct T c;
c.b = 55;

Dans ce cas, le compilateur écrirait 55 dans une valeur int positionnée à l'octet-offset 2 dans le bloc de mémoire continue appelé c, même si le type struct T n'avait pas de champ. nommé b. Le compilateur ne se soucierait pas du tout du type de c. Tout ce qui lui importait, c’est que c était une valeur: une sorte de bloc mémoire inscriptible.

Maintenant, notez que si vous avez fait cela

S *s;
...
s.b = 42;

le code serait considéré comme valide (puisque s est également une lvalue) et le compilateur essaierait simplement d'écrire des données dans le pointeur s lui-même, à l'octet-offset 2. Inutile de dire que de telles choses pourraient facilement entraîner une saturation de la mémoire, mais le langage ne s’y intéressait pas.

C'est à dire. dans cette version du langage, l'idée que vous proposiez de surcharger l'opérateur . pour les types de pointeur ne fonctionnerait pas: l'opérateur . avait déjà une signification très spécifique lorsqu'il était utilisé avec des pointeurs (avec des pointeurs de valeur ou avec aucune valeur du tout) . C'était une fonctionnalité très bizarre, sans aucun doute. Mais c'était là à l'époque.

Bien sûr, cette fonctionnalité étrange n’est pas une raison très forte de ne pas introduire un opérateur surchargé . pour les pointeurs (comme vous l’avez suggéré) dans la version retravaillée de C - K & R C. Mais cela n’a pas été fait. Peut-être qu’à cette époque, il existait un code hérité écrit dans la version C de CRM qui devait être pris en charge.

(L’URL du manuel de référence C de 1975 peut ne pas être stable. Une autre copie, avec peut-être quelques différences subtiles, est ici .)

330
AnT

Au-delà des raisons historiques (bonnes et déjà rapportées), il existe également un petit problème avec la priorité des opérateurs: l'opérateur dot a une priorité supérieure à celle de l'opérateur star. Si vous avez une structure contenant un pointeur à l'autre, ces deux sont équivalents:

(*(*(*a).b).c).d

a->b->c->d

Mais le second est clairement plus lisible. L’opérateur Arrow a la priorité la plus élevée (comme le point) et associe de gauche à droite. Je pense que cela est plus clair que d'utiliser l'opérateur point pour les pointeurs à struct et struct, car nous connaissons le type de l'expression sans avoir à regarder la déclaration, qui pourrait même se trouver dans un autre fichier.

38
effeffe

C fait également du bon travail en ne rendant rien ambigu.

Bien sûr, le point pourrait être surchargé pour signifier les deux choses, mais la flèche permet de s’assurer que le programmeur sait qu’il utilise un pointeur, comme si le compilateur ne vous permettait pas de mélanger deux types incompatibles.

19
mukunda