Perché esiste l’operatore freccia (->) in C?

L’operatore punto ( . ) Viene utilizzato per accedere a un membro di una struct, mentre l’operatore freccia ( -> ) in C viene utilizzato per accedere a un membro di una struttura a cui fa riferimento il puntatore in questione.

Il puntatore stesso non ha alcun membro a cui si possa accedere con l’operatore punto (in realtà è solo un numero che descrive una posizione nella memoria virtuale in modo che non abbia alcun membro). Quindi, non ci sarebbe alcuna ambiguità se abbiamo appena definito l’operatore punto per dereferenziare automaticamente il puntatore se è usato su un puntatore (un’informazione che è nota al compilatore in fase di compilazione afaik).

Quindi, perché i creatori della lingua hanno deciso di rendere le cose più complicate aggiungendo questo operatore apparentemente non necessario? Qual è la grande decisione di progettazione?

Interpreterò la tua domanda come due domande: 1) perché -> esiste anche, e 2) perché . non dereferenzia automaticamente il puntatore. Le risposte ad entrambe le domande hanno radici storiche.

Perché -> esiste persino?

In una delle primissime versioni del linguaggio C (che chiamerò CRM per ” C Reference Manual “, che è arrivato con la 6a edizione di Unix nel maggio 1975), operator -> 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. Nei membri della struttura CRM è stato implementato il concetto globale di offset di byte , che può essere aggiunto a qualsiasi valore di indirizzo senza restrizioni di tipo. Tutti i nomi di tutti i membri della struct avevano un significato globale indipendente (e, quindi, doveva essere unico). Ad esempio potresti dichiarare

 struct S { int a; int b; }; 

e il nome a rappresenterebbe l’offset 0, mentre il nome b rappresenterebbe l’offset 2 (assumendo il tipo int di dimensione 2 e nessuna spaziatura). La lingua richiesta a tutti i membri di tutte le strutture nell’unità di traduzione ha nomi univoci o indica lo stesso valore di offset. Ad esempio nella stessa unità di traduzione si potrebbe inoltre dichiarare

 struct X { int a; int x; }; 

e che sarebbe OK, dal momento che il nome a sarebbe coerentemente per offset 0. Ma questa ulteriore dichiarazione

 struct Y { int b; int a; }; 

sarebbe formalmente invalido, dal momento che ha tentato di “ridefinire” a come offset 2 b come offset 0.

Ed è qui che entra in gioco l’operatore -> . Poiché ogni nome di membro della struct ha il proprio significato globale autosufficiente, le espressioni supportate da linguaggio 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 compito è stato interpretato dal compilatore come “prendere l’indirizzo 5 , aggiungere l’offset 2 ad esso e assegnare 42 al valore int all’indirizzo risultante”. Cioè quanto sopra dovrebbe assegnare 42 al valore int all’indirizzo 7 . Si noti che questo uso di -> non interessa il tipo di espressione sul lato sinistro. Il lato sinistro è stato interpretato come un valore numerico rvalue (sia esso un puntatore o un intero).

Questa sorta di inganno non era ansible con * e . combinazione. Non potevi farlo

 (*i).b = 42; 

poiché *i è già un’espressione non valida. L’operatore * , poiché è separato da . , 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 dell’operando di sinistra.

Come ha notato Keith nei commenti, questa differenza tra -> e * + . combinazione è ciò che CRM si riferisce a come “rilassamento del requisito” in 7.1.8: Tranne il rilassamento del requisito che E1 sia di tipo puntatore, l’espressione E1−>MOS è esattamente equivalente a (*E1).MOS

Successivamente, in K & R C molte delle funzionalità originariamente descritte in CRM sono state notevolmente rielaborate. L’idea di “struct member as global offset identifier” è stata completamente rimossa. E la funzionalità dell’operatore -> divenne completamente identica alla funzionalità di * e . combinazione.

Perché non può dereferenziare automaticamente il puntatore?

Di nuovo, nella versione CRM del linguaggio, l’operando di sinistra del . l’operatore doveva essere un lvalue . Questo era l’ unico requisito imposto a quell’operando (ed è questo che lo ha reso diverso da -> , come spiegato sopra). Si noti che CRM non ha richiesto l’operando di sinistra di . avere un tipo struct. Richiedeva solo che fosse un lvalue, qualsiasi valore. Ciò significa che nella versione CRM di C potresti scrivere codice come questo

 struct S { int a, b; }; struct T { float x, y, z; }; struct T c; cb = 55; 

In questo caso il compilatore scriverebbe 55 in un valore int posizionato all’offset di byte 2 nel blocco di memoria continua noto come c , anche se la struct T tipo struct T non aveva campo denominato b . Il compilatore non si preoccuperebbe del tipo effettivo di c affatto. Tutto ciò che importa è che c era un lvalue: una sorta di blocco di memoria scrivibile.

Ora nota che se hai fatto questo

 S *s; ... sb = 42; 

il codice sarebbe considerato valido (dato che s è anche un lvalue) e il compilatore cercherebbe semplicemente di scrivere i dati nel puntatore stesso , con offset di byte 2. Inutile dire che cose come questa potrebbero facilmente causare un sovraccarico della memoria, ma la lingua non si è occupata di tali argomenti.

Cioè in quella versione della lingua la tua idea proposta sull’operatore di sovraccarico . per i tipi di puntatore non funzionerebbe: operatore . aveva già un significato molto specifico quando usato con i puntatori (con i puntatori lvalue o con qualsiasi lvalue). Era una funzionalità molto strana, senza dubbio. Ma era lì in quel momento.

Naturalmente, questa strana funzionalità non è una ragione molto forte contro l’introduzione di sovraccarico . operatore per i puntatori (come suggerito) nella versione rielaborata di C – K & R C. Ma non è stato fatto. Forse in quel momento c’era un codice legacy scritto nella versione CRM di C che doveva essere supportato.

(L’URL per il Manuale di riferimento del 1975 C potrebbe non essere stabile. Un’altra copia, forse con alcune sottili differenze, è qui .)

Al di là delle ragioni storiche (buone e già riportate), c’è anche un piccolo problema con la precedenza degli operatori: l’operatore punto ha una priorità più alta dell’operatore stella, quindi se hai struct contenente un puntatore a struct contenente un puntatore a struct … Questi due sono equivalenti:

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

Ma il secondo è chiaramente più leggibile. L’operatore di frecce ha la priorità più alta (proprio come punto) e associa da sinistra a destra. Penso che sia più chiaro di usare l’operatore punto sia per i puntatori a struct e struct, perché conosciamo il tipo dall’espressione senza dover guardare la dichiarazione, che potrebbe anche essere in un altro file.

C fa anche un buon lavoro per non rendere nulla di ambiguo.

Certo, il punto potrebbe essere sovraccaricato per indicare entrambe le cose, ma la freccia fa in modo che il programmatore sappia che sta operando su un puntatore, proprio come quando il compilatore non ti consente di mescolare due tipi incompatibili.