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


264

L' .operatore punto ( ) viene utilizzato per accedere a un membro di una struttura, 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 membri a cui è possibile accedere con l'operatore punto (in realtà è solo un numero che descrive una posizione nella memoria virtuale, quindi non ha membri). Quindi, non ci sarebbe alcuna ambiguità se avessimo semplicemente definito l'operatore punto per dereferenziare automaticamente il puntatore se è usato su un puntatore (un'informazione che è nota al compilatore in fase di compilazione).

Allora perché i creatori di lingue hanno deciso di rendere le cose più complicate aggiungendo questo operatore apparentemente non necessario? Qual è la grande decisione progettuale?


1
Correlati: stackoverflow.com/questions/221346/… - inoltre, è possibile eseguire l'override ->
Krease

16
@Chris Quello riguarda il C ++ che ovviamente fa una grande differenza. Ma dal momento che stiamo parlando del motivo per cui C è stato progettato in questo modo, facciamo finta che siamo tornati negli anni '70 - prima che esistesse C ++.
Mistico il

5
La mia ipotesi migliore è che l'operatore freccia esiste per esprimere visivamente "guardalo! Hai a che fare con un puntatore qui"
Chris

4
A prima vista, sento che questa domanda è molto strana. Non tutte le cose sono pensate con cura. Se mantieni questo stile per tutta la vita, il tuo mondo sarebbe pieno di domande. La risposta che ha ottenuto il maggior numero di voti è davvero istruttiva e chiara. Ma non colpisce il punto chiave della tua domanda. Segui lo stile della tua domanda, posso fare troppe domande. Ad esempio, la parola chiave "int" è l'abbreviazione di "numero intero"; perché la parola chiave "double" non è anche più breve?
Junwanghe,

1
@junwanghe Questa domanda rappresenta in realtà una preoccupazione valida: perché l' .operatore ha una precedenza maggiore rispetto *all'operatore? In caso contrario, potremmo avere * ptr.member e var.member.
Milleniumbug

Risposte:


358

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 .)


10
E la sezione 7.1.8 del Manuale di riferimento C citato dice "Tranne per il rilassamento del requisito che E1 sia di tipo puntatore, l'espressione '' E1−> MOS '' è esattamente equivalente a '' (* E1) .MOS ' '."
Keith Thompson,

1
Perché non è *istato un valore di qualche tipo predefinito (int?) All'indirizzo 5? Quindi (* i) .b avrebbe funzionato allo stesso modo.
Casuale 832

5
@Leo: Beh, alcune persone amano il linguaggio C come assemblatore di livello superiore. In quel periodo della storia del C, il linguaggio era effettivamente un assemblatore di livello superiore.
Un

29
Huh. Quindi questo spiega perché molte strutture in UNIX (ad es. struct stat) Antepongono i loro campi (ad es st_mode.).
icktoofay,

5
@ perfectionm1ng: Sembra che bell-labs.com sia stata rilevata da Alcatel-Lucent e le pagine originali siano sparite. Ho aggiornato il link ad un altro sito, anche se non posso dire per quanto tempo rimarrà attivo. Comunque, cercare su Google il "manuale di riferimento di Ritchie C" di solito trova il documento.
AnT

46

Oltre alle ragioni storiche (buone e già segnalate), c'è anche un piccolo problema con la precedenza degli operatori: l'operatore punto ha una priorità più alta rispetto all'operatore stella, quindi se si dispone di una struttura contenente puntatore a struttura contenente un puntatore a struttura ... Questi due sono equivalenti:

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

a->b->c->d

Ma il secondo è chiaramente più leggibile. L'operatore freccia ha la massima priorità (proprio come un punto) e associa da sinistra a destra. Penso che sia più chiaro dell'uso dell'operatore punto sia per i puntatori che per strutturare e strutturare, perché conosciamo il tipo dall'espressione senza dover guardare la dichiarazione, che potrebbe anche essere in un altro file.


2
Con i tipi di dati nidificati contenenti sia strutture che puntatori a strutture, ciò può rendere le cose più difficili in quanto devi pensare di scegliere l'operatore giusto per ogni accesso di submember. Potresti finire con ab-> c-> d o a-> bc-> d (ho avuto questo problema quando ho usato la libreria freetype - dovevo cercare sempre il suo codice sorgente). Inoltre, questo non spiega perché non sarebbe possibile lasciare che il compilatore dereferenzi il puntatore automaticamente quando si tratta di puntatori.
Askaga,

3
Mentre i fatti che stai affermando sono corretti, non rispondono in alcun modo alla mia domanda originale. Spieghi l'uguaglianza di a-> e * (a). notazioni (che sono già state spiegate più volte in altre domande) oltre a dare una vaga affermazione sul fatto che il design del linguaggio sia in qualche modo arbitrario. Non ho trovato la tua risposta molto utile, quindi il downvote.
Askaga,

16
@effeffe, l'OP sta dicendo che il linguaggio avrebbe potuto facilmente interpretarsi a.b.c.dcome (*(*(*a).b).c).d, rendendo l' ->operatore inutile. Quindi la versione dell'OP ( a.b.c.d) è ugualmente leggibile (rispetto a a->b->c->d). Ecco perché la tua risposta non risponde alla domanda del PO.
Shahbaz,

4
@Shahbaz Questo potrebbe essere il caso di un programmatore Java, un programmatore C / C ++ capirà a.b.c.de a->b->c->dcome due cose molto diverse: il primo è un singolo accesso alla memoria di un oggetto secondario nidificato (in questo caso esiste un solo oggetto memoria ), il secondo è tre accessi alla memoria, che inseguono i puntatori attraverso quattro probabili oggetti distinti. Questa è una grande differenza nel layout della memoria, e credo che C abbia ragione nel distinguere questi due casi in modo molto visibile.
cmaster - ripristina monica il

2
@Shahbaz Non intendevo che come un insulto ai programmatori Java, sono semplicemente usati per un linguaggio con puntatori completamente impliciti. Se fossi stato cresciuto come programmatore Java, probabilmente avrei pensato allo stesso modo ... Comunque, penso davvero che l'overloading dell'operatore che vediamo in C sia meno che ottimale. Tuttavia, riconosco che siamo stati viziati dai matematici che sovraccaricano liberamente i loro operatori praticamente per tutto. Capisco anche la loro motivazione, poiché l'insieme dei simboli disponibili è piuttosto limitato. Immagino, alla fine, è solo la domanda su dove si disegna la linea ...
cmaster - reintegrare monica il

19

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

Certo il punto potrebbe essere sovraccarico per significare entrambe le cose, ma la freccia si assicura che il programmatore sappia che sta operando su un puntatore, proprio come quando il compilatore non ti consente di mescolare due tipi incompatibili.


4
Questa è la risposta semplice e corretta. C cerca principalmente di evitare di sovraccaricare quale IMO è una delle cose migliori di C.
jforberg

10
Molte cose in C sono ambigue e sfocate. Ci sono conversioni di tipo implicite, gli operatori matematici sono sovraccarichi, l'indicizzazione concatenata fa qualcosa di completamente diverso a seconda che si stia indicizzando un array multidimensionale o un array di puntatori e qualsiasi cosa potrebbe essere una macro che nasconde qualcosa (la convenzione di denominazione maiuscola aiuta lì, ma C no) t).
PSkocik,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.