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 astarebbe per offset 0, mentre il nome bstarebbe per offset 2 (assumendo il inttipo 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 arappresenterebbe costantemente l'offset 0. Ma questa dichiarazione aggiuntiva
struct Y {
int b;
int a;
};
sarebbe formalmente non valido, poiché ha tentato di "ridefinire" acome offset 2 e bcome 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 2e assegna 42al intvalore all'indirizzo risultante". Vale a dire quanto sopra assegnato 42al intvalore 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*.E1E1−>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à 55in un intvalore posizionato all'offset di byte 2 nel blocco di memoria continua noto come c, anche se type struct Tnon ha un campo denominato b. Il compilatore non si preoccuperebbe affatto del tipo effettivo di c. Tutto quello che gli importava è che cfosse 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 sstesso , 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 .)