Interpreterò la tua domanda come due domande: 1) perché ->
esiste, e 2) perché .
non dereferenzia automaticamente il puntatore. Le risposte a entrambe le domande hanno radici storiche.
Perché ->
esiste?
In una delle prime versioni del linguaggio C (che farà riferimento come CRM per " C Manuale di riferimento ", che è venuto con 6 ° Edizione Unix maggio 1975), l'operatore ->
aveva un significato molto esclusivo, non è sinonimo di *
e .
combinazione
Il linguaggio C descritto da CRM era molto diverso dal C moderno per molti aspetti. In CRM i membri della struttura hanno implementato il concetto globale di byte offset , che potrebbe essere aggiunto a qualsiasi valore di indirizzo senza restrizioni di tipo. Vale a dire che tutti i nomi di tutti i membri della struttura avevano un significato globale indipendente (e, pertanto, dovevano essere univoci). Ad esempio potresti dichiarare
struct S {
int a;
int b;
};
e il nome a
starebbe per offset 0, mentre il nome b
starebbe per offset 2 (assumendo il int
tipo di dimensione 2 e nessuna imbottitura). La lingua richiesta a tutti i membri di tutte le strutture nell'unità di traduzione ha nomi univoci o rappresenta lo stesso valore di offset. Ad esempio nella stessa unità di traduzione è possibile dichiarare inoltre
struct X {
int a;
int x;
};
e sarebbe OK, dato che il nome a
rappresenterebbe costantemente l'offset 0. Ma questa dichiarazione aggiuntiva
struct Y {
int b;
int a;
};
sarebbe formalmente non valido, poiché ha tentato di "ridefinire" a
come offset 2 e b
come offset 0.
Ed è qui ->
che entra in gioco l' operatore. Poiché ogni nome di membro di strutt aveva il suo significato globale autosufficiente, il linguaggio supportava espressioni come queste
int i = 5;
i->b = 42; /* Write 42 into `int` at address 7 */
100->a = 0; /* Write 0 into `int` at address 100 */
Il primo incarico è stato interpretato dal compilatore come "prendi indirizzo 5
, aggiungi offset 2
e assegna 42
al int
valore all'indirizzo risultante". Vale a dire quanto sopra assegnato 42
al int
valore all'indirizzo 7
. Si noti che questo uso di ->
non ha avuto importanza per il tipo di espressione sul lato sinistro. Il lato sinistro è stato interpretato come un indirizzo numerico di valore (sia esso un puntatore o un numero intero).
Questo tipo di inganno non era possibile con *
e .
combinazione. Non si poteva fare
(*i).b = 42;
poiché *i
è già un'espressione non valida. L' *
operatore, poiché è separato .
, impone requisiti di tipo più rigorosi sul suo operando. Per fornire una capacità di aggirare questa limitazione, CRM ha introdotto l' ->
operatore, che è indipendente dal tipo di operando di sinistra.
Come ha notato Keith nei commenti, questa differenza tra la combinazione + ->
e è ciò che CRM si riferisce a "rilassamento del requisito" in 7.1.8: Tranne il rilassamento del requisito che è di tipo puntatore, l'espressione è esattamente equivalente a*
.
E1
E1−>MOS
(*E1).MOS
Successivamente, in K&R C molte funzionalità originariamente descritte in CRM sono state significativamente rielaborate. L'idea di "membro struct come identificatore di offset globale" è stata completamente rimossa. E la funzionalità ->
dell'operatore è diventata completamente identica alla funzionalità *
e alla .
combinazione.
Perché non è possibile riconoscere .
automaticamente il puntatore?
Ancora una volta, nella versione CRM della lingua, l'operando di sinistra dell'operatore .
doveva essere un valore . Questo era l' unico requisito imposto a quell'operando (ed è ciò che lo ha reso diverso ->
, come spiegato sopra). Si noti che CRM non richiedeva che l'operando di sinistra .
avesse un tipo di struttura. Richiedeva solo che fosse un valore, qualsiasi valore. Ciò significa che nella versione CRM di C potresti scrivere codice in questo modo
struct S { int a, b; };
struct T { float x, y, z; };
struct T c;
c.b = 55;
In questo caso il compilatore scriverà 55
in un int
valore posizionato all'offset di byte 2 nel blocco di memoria continua noto come c
, anche se type struct T
non ha un campo denominato b
. Il compilatore non si preoccuperebbe affatto del tipo effettivo di c
. Tutto quello che gli importava è che c
fosse un valore: una sorta di blocco di memoria scrivibile.
Ora nota che se lo hai fatto
S *s;
...
s.b = 42;
il codice sarebbe considerato valido (dal momento che s
è anche un valore) e il compilatore tenterebbe semplicemente di scrivere i dati nel puntatore s
stesso , all'offset di byte 2. Inutile dire che cose come queste potrebbero facilmente comportare un sovraccarico di memoria, ma il linguaggio non si occupava di tali questioni.
Vale a dire in quella versione del linguaggio la tua idea proposta di sovraccaricare l'operatore .
per i tipi di puntatore non funzionerebbe: l'operatore .
aveva già un significato molto specifico quando veniva usato con i puntatori (con puntatori lvalue o con qualsiasi lvalues). Era una funzionalità molto strana, senza dubbio. Ma era lì al momento.
Naturalmente, questa strana funzionalità non è una ragione molto forte contro l'introduzione di un .
operatore sovraccarico per i puntatori (come hai suggerito) nella versione rielaborata di C - K&R C. Ma non è stato fatto. Forse a quel tempo c'era un codice legacy scritto nella versione CRM di C che doveva essere supportato.
(L'URL del Manuale di riferimento C del 1975 potrebbe non essere stabile. Un'altra copia, forse con alcune sottili differenze, è qui .)