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 a
représenterait le décalage 0, tandis que le nom b
représenterait le décalage 2 (en supposant le int
type 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 a
repré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" 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 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 2
et l'affecter 42
à la int
valeur à l'adresse résultante". C'est-à-dire que ce qui précède attribuerait 42
une int
valeur à 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 *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 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 E1
est 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é 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 55
dans une int
valeur positionnée à 2 octets dans le bloc de mémoire continue c
, même si type struct T
n'avait pas de champ nommé b
. Le compilateur ne se soucierait pas du tout du type réel c
. Tout ce qui c
importait, 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 s
s'agit également d'une valeur l) et le compilateur tenterait simplement d'écrire des données dans le pointeur s
lui - 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 .)