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

264

L' .opérateur point ( ) est utilisé pour accéder à un membre d'une structure, tandis que l'opérateur flèche ( ->) dans 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 n'a pas de membres accessibles avec l'opérateur point (c'est en fait seulement un nombre décrivant un emplacement dans la mémoire virtuelle donc il n'a pas de membres). Ainsi, il n'y aurait aucune ambiguïté si nous venions 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 afaik).

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?

Askaga
la source
1
CONNEXES : stackoverflow.com/questions/221346/… - aussi, vous pouvez remplacer ->
Krease
16
@Chris Celui-ci concerne le C ++ qui fait bien sûr une grande différence. Mais puisque nous parlons de la raison pour laquelle C a été conçu de cette façon, supposons que nous sommes de retour dans les années 1970 - avant que C ++ n'existe.
Mysticial
5
Ma meilleure supposition est que l'opérateur de flèche existe pour exprimer visuellement "regardez-le! Vous avez affaire à un pointeur ici"
Chris
4
En un coup d'œil, je pense que cette question est très étrange. Toutes les choses ne sont pas conçues de manière réfléchie. Si vous gardez ce style dans toute votre vie, votre monde serait plein de questions. La réponse qui a obtenu le plus de votes est vraiment informative et claire. Mais cela ne touche pas le point clé de votre question. Suivez le style de votre question, je peux poser trop de questions. Par exemple, le mot-clé 'int' est l'abréviation de 'entier'; pourquoi le mot-clé «double» n'est-il pas également plus court?
junwanghe
1
@junwanghe Cette question représente en fait une préoccupation valable - pourquoi l' .opérateur a-t-il une priorité plus élevée que l' *opérateur? Sinon, nous pourrions avoir * ptr.member et var.member.
milleniumbug

Réponses:

358

J'interpréterai votre question comme deux questions: 1) pourquoi ->existe-t-il, 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 j'appellerai CRM pour " C Reference Manual ", fourni avec 6th Edition Unix en mai 1975), l'opérateur ->avait une signification très exclusive, non synonyme de combinaison *et.

Le langage C décrit par CRM était très différent du C moderne à bien des égards. Dans la structure CRM, les membres ont implémenté le concept global de décalage d'octets , qui peut être ajouté à n'importe quelle valeur d'adresse sans restriction de type. C'est-à-dire que tous les noms de tous les membres de la structure avaient une signification globale indépendante (et devaient donc être uniques). Par exemple, vous pouvez déclarer

struct S {
  int a;
  int b;
};

et nom areprésenterait le décalage 0, tandis que le nom breprésenterait le décalage 2 (en supposant le inttype de taille 2 et pas de remplissage). La langue exigeait que tous les membres de toutes les structures de l'unité de traduction aient des noms uniques ou représentent la même valeur de décalage. Par exemple, dans la même unité de traduction, vous pouvez également déclarer

struct X {
  int a;
  int x;
};

et ce serait OK, car le nom areprésenterait systématiquement le décalage 0. Mais cette déclaration supplémentaire

struct Y {
  int b;
  int a;
};

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

Et c'est là que l' ->opérateur entre en jeu. Étant donné que chaque nom de membre struct avait sa propre signification globale auto-suffisante, le langage supportait des expressions 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 "prendre l'adresse 5, lui ajouter un décalage 2et l'affecter 42à la intvaleur à l'adresse résultante". C'est-à-dire que ce qui précède attribuerait 42une intvaleur à l'adresse 7. Notez que cette utilisation de ->ne se souciait pas du type de l'expression sur le 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 genre de ruse n'était pas possible avec *et en .combinaison. Tu ne pouvais pas faire

(*i).b = 42;

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

Comme Keith l'a noté dans les commentaires, cette différence entre ->et *+ .combinaison est ce que CRM appelle «l'assouplissement de l'exigence» dans 7.1.8: à l' exception de l'assouplissement de l'exigence qui E1est de type pointeur, l'expression E1−>MOSest exactement équivalente à(*E1).MOS

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

Pourquoi ne pouvez-vous pas .déréférencer le pointeur automatiquement?

Encore une fois, dans la version CRM de la langue, l'opérande gauche de l' .opérateur devait être une valeur l . C'était la seule exigence imposée à cet opérande (et c'est ce qui le rendait différent ->, comme expliqué ci-dessus). Notez que CRM ne nécessitait pas l'opérande gauche de .pour avoir un type struct. Il fallait juste que ce soit une lvalue, n'importe quelle lvalue. Cela signifie que dans la version CRM de C, vous pouvez écrire du 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 55dans une intvaleur positionnée à 2 octets dans le bloc de mémoire continue c, même si type struct Tn'avait pas de champ nommé b. Le compilateur ne se soucierait pas du tout du type réel c. Tout ce qui cimportait, c'était une valeur l: une sorte de bloc de mémoire inscriptible.

Notez maintenant que si vous avez fait cela

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

le code serait considéré comme valide (car il ss'agit également d'une valeur l) et le compilateur tenterait simplement d'écrire des données dans le pointeur slui - même , à l'octet-offset 2. Inutile de dire que des choses comme celle-ci pourraient facilement entraîner un dépassement de mémoire, mais le langage ne se préoccupe pas de ces questions.

C'est-à-dire que dans cette version du langage, votre idée proposée de surcharger l'opérateur .pour les types de pointeurs 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 lvalue ou avec n'importe quelle valeur l). C'était une fonctionnalité très étrange, sans aucun doute. Mais c'était là à l'époque.

Bien sûr, cette fonctionnalité étrange n'est pas une raison très forte contre l'introduction d'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, un code hérité écrit dans la version CRM de C devait être pris en charge.

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

AnT
la source
10
Et la section 7.1.8 du manuel de référence C cité dit: "Sauf pour l'assouplissement de l'exigence que E1 soit de type pointeur, l'expression '' E1−> MOS '' est exactement équivalente à '' (* E1) .MOS ' "."
Keith Thompson
1
Pourquoi ne s'agit-il *ipas d'une valeur l d'un type par défaut (int?) À l'adresse 5? Alors (* i) .b aurait fonctionné de la même manière.
Random832
5
@Leo: Eh bien, certaines personnes aiment le langage C en tant qu'assembleur de niveau supérieur. À cette période de l'histoire du langage C, le langage était en fait un assembleur de niveau supérieur.
AnT
29
Huh. Cela explique donc pourquoi de nombreuses structures sous UNIX (par exemple struct stat) préfixent leurs champs (par exemple st_mode).
icktoofay
5
@ perfectionm1ng: Il semble que bell-labs.com ait été repris par Alcatel-Lucent et que les pages originales ont disparu. J'ai mis à jour le lien vers un autre site, bien que je ne puisse pas dire combien de temps celui-ci restera en place. Quoi qu'il en soit, googler pour "manuel de référence c ritchie" trouve généralement le document.
AnT
46

Au-delà des raisons historiques (bonnes et déjà signalées), il y a aussi un petit problème avec la priorité des opérateurs: l'opérateur point a une priorité plus élevée que l'opérateur étoile, donc si vous avez struct contenant pointeur vers struct contenant pointeur vers struct ... Ces deux sont équivalents:

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

a->b->c->d

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

effeffe
la source
2
Avec des types de données imbriqués contenant à la fois des structures et des pointeurs vers des structures, cela peut rendre les choses plus difficiles car vous devez penser à choisir le bon opérateur pour chaque accès de membre. Vous pourriez vous retrouver avec ab-> c-> d ou a-> bc-> d (j'ai eu ce problème lors de l'utilisation de la bibliothèque freetype - j'avais besoin de rechercher son code source tout le temps). Cela n'explique pas non plus pourquoi il ne serait pas possible de laisser le compilateur déréférencer automatiquement le pointeur lorsqu'il traite des pointeurs.
Askaga
3
Bien que les faits que vous déclarez soient exacts, ils ne répondent en aucune façon à ma question initiale. Vous expliquez l'égalité des a-> et * (a). notations (qui a déjà été expliqué plusieurs fois dans d'autres questions) ainsi que de donner une vague déclaration sur la conception du langage étant quelque peu arbitraire. Je n'ai pas trouvé votre réponse très utile, donc le downvote.
Askaga
16
@effeffe, l'OP dit que le langage aurait pu facilement être interprété a.b.c.dcomme (*(*(*a).b).c).d, rendant l' ->opérateur inutile. La version ( a.b.c.d) de l'OP est donc également lisible (par rapport à a->b->c->d). C'est pourquoi votre réponse ne répond pas à la question du PO.
Shahbaz
4
@Shahbaz Cela peut être le cas pour un programmeur java, un programmeur C / C ++ comprendra a.b.c.det a->b->c->dcomme deux choses très différentes: La première est un accès mémoire unique à un sous-objet imbriqué (il n'y a qu'un seul objet mémoire dans ce cas ), le second est constitué de trois accès à la mémoire, poursuivant des pointeurs à travers quatre objets probablement distincts. C'est une énorme différence dans la disposition de la mémoire, et je pense que C a raison de distinguer très clairement ces deux cas.
cmaster - réintègre monica
2
@Shahbaz Je ne voulais pas dire qu'en tant qu'insulte aux programmeurs java, ils sont simplement habitués à un langage avec des pointeurs totalement implicites. Si j'avais été élevé en tant que programmeur Java, je penserais probablement de la même manière ... Quoi qu'il en soit, je pense en fait que la surcharge de l'opérateur que nous voyons en C est loin d'être optimale. Cependant, je reconnais que nous avons tous été gâtés par les mathématiciens qui surchargent généreusement leurs opérateurs pour à peu près tout. Je comprends aussi leur motivation, car l'ensemble des symboles disponibles est assez limité. Je suppose qu'en fin de compte, c'est juste la question de savoir où tracer la ligne ...
cmaster - rétablir monica
19

C fait également du bon travail pour ne rien rendre ambigu.

Bien sûr, le point pourrait être surchargé pour signifier les deux choses, mais la flèche s'assure que le programmeur sait qu'il fonctionne sur un pointeur, tout comme lorsque le compilateur ne vous permet pas de mélanger deux types incompatibles.

mukunda
la source
4
Telle est la réponse simple et correcte. C essaie surtout d'éviter la surcharge, ce qui est l'une des meilleures choses à propos de C.
jforberg
10
Beaucoup de choses en C sont ambiguës et floues. Il y a des conversions de types implicites, les opérateurs mathématiques sont surchargés, l'indexation chaînée fait quelque chose de complètement différent selon que vous indexez un tableau multidimensionnel ou un tableau de pointeurs et tout peut être une macro cachant quoi que ce soit (la convention de dénomination en majuscules aide mais C ne fait pas '' t).
PSkocik