Perché la sintassi C per array, puntatori e funzioni è stata progettata in questo modo?


16

Dopo aver visto (e posto!) Tante domande simili a

Cosa int (*f)(int (*a)[5])significa in C?

e anche visto che avevano creato un programma per aiutare le persone a capire la sintassi C, non posso fare a meno di chiedermi:

Perché la sintassi di C è stata progettata in questo modo?

Ad esempio, se stavo progettando puntatori, tradurrei "un puntatore a un array di puntatori a 10 elementi" in

int*[10]* p;

e non

int* (*p)[10];

che penso che la maggior parte delle persone concorderebbe è molto meno semplice.

Quindi mi chiedo, perché la sintassi non intuitiva? C'è stato un problema specifico che la sintassi risolve (forse un'ambiguità?) Di cui non sono a conoscenza?


2
Sai che non ci sono risposte reali a questa e ad altre domande. Giusto? Quello che otterrai sono solo ipotesi.
BЈовић

7
@VJo - potrebbe darsi che ci sia una risposta "reale" (cioè oggettiva) - autori di linguaggi e comitati di standard hanno esplicitamente giustificato (o almeno spiegato) molte di queste decisioni.
detly

Non credo che la tua sintassi proposta sia necessariamente più o meno "intuitiva" della sintassi C. C è quello che è; una volta che l'hai imparato, non avrai più queste domande. Se non l'hai imparato ... beh, forse è questo il vero problema.
Caleb,

1
@Caleb: Divertente come lo hai concluso così facilmente, perché l'ho imparato e avevo ancora questa domanda ...
user541686

1
Il cdeclcomando è molto utile per decodificare dichiarazioni C complesse. C'è anche un'interfaccia web su cdecl.org .
Keith Thompson,

Risposte:


16

La mia comprensione della storia è che si basa su due punti principali ...

In primo luogo, gli autori del linguaggio hanno preferito rendere la sintassi incentrata sulla variabile piuttosto che sul tipo. Cioè, volevano che un programmatore guardasse la dichiarazione e pensasse "se scrivo l'espressione *func(arg), ciò si tradurrà in un int; se scrivo *arg[N]avrò un float" piuttosto che " funcdeve essere un puntatore a una funzione che prende questo e restituendo quella ".

La voce C su Wikipedia afferma che:

L'idea di Ritchie era di dichiarare gli identificatori in contesti simili al loro uso: "la dichiarazione riflette l'uso".

... citando p122 di K & R2 che, purtroppo, non ho bisogno di trovare la citazione estesa per te.

In secondo luogo, è davvero molto difficile trovare una sintassi per la dichiarazione che sia coerente quando si ha a che fare con livelli arbitrari di indiretta. Il tuo esempio potrebbe funzionare bene per esprimere il tipo che hai pensato fuori di testa lì, ma si adatta a una funzione che prende un puntatore a una matrice di quei tipi e restituisce qualche altro casino orribile? (Forse lo fa, ma hai controllato? Puoi provarlo? ).

Ricorda, parte del successo di C è dovuto al fatto che i compilatori sono stati scritti per molte piattaforme diverse, e quindi sarebbe stato meglio ignorare un certo grado di leggibilità per rendere i compilatori più facili da scrivere.

Detto questo, non sono un esperto di grammatica o scrittura di compilatori. Ma so abbastanza per sapere che c'è molto da sapere;)


2
"rendere i compilatori più facili da scrivere" ... tranne C è noto per essere difficile da analizzare (superato solo da C ++).
Jan Hudec,

1
@JanHudec - Beh ... sì. Questa non è una dichiarazione a tenuta stagna. Ma mentre C è impossibile analizzare come grammatica libera dal contesto, una volta che una persona ha escogitato un modo per analizzarla, questo smette di essere il passo difficile. E il fatto è che è stato prolifico nei suoi primi tempi a causa della gente in grado di battere facilmente i compilatori, quindi K&R deve aver raggiunto un certo equilibrio. (Nel famigerato The Rise of "Worse is Better" di Richard Gabriel , dà per scontato - e si lamenta - il fatto che sia facile scrivere un compilatore C per una nuova piattaforma.)
Detly

Sono contento di essere corretto su questo, a proposito - non so molto su analisi e grammatica. Vado più sull'inferenza dal fatto storico.
Detly

12

Molte delle stranezze del linguaggio C possono essere spiegate dal modo in cui i computer funzionavano quando è stato progettato. C'erano quantità molto limitate di memoria di archiviazione, quindi era molto importante ridurre al minimo le dimensioni dei file di codice sorgente stessi. La pratica di programmazione negli anni '70 e '80 era quella di assicurarsi che il codice sorgente contenesse il minor numero possibile di caratteri e preferibilmente nessun commento eccessivo sul codice sorgente.

Questo è ovviamente ridicolo oggi, con spazio di archiviazione praticamente illimitato su hard disk. Ma fa parte del motivo per cui C ha una sintassi così strana in generale.


Per quanto riguarda specificamente i puntatori di array, il tuo secondo esempio dovrebbe essere int (*p)[10];(sì, la sintassi è molto confusa). Potrei forse leggerlo come "puntatore int a array di dieci" ... il che ha un po 'senso. Se non fosse per la parentesi, il compilatore lo interpreterebbe come una matrice di dieci puntatori, il che darebbe alla dichiarazione un significato completamente diverso.

Poiché i puntatori a matrice e i puntatori a funzione hanno entrambi una sintassi abbastanza oscura in C, la cosa sensata da fare è di eliminare la stranezza. Forse così:

Esempio oscuro:

int func (int (*arr_ptr)[10])
{
  return 0;
}

int main()
{
  int array[10];
  int (*arr_ptr)[10]  = &array;
  int (*func_ptr)(int(*)[10]) = &func;

  func_ptr(arr_ptr);
}

Esempio non oscuro, equivalente:

typedef int array_t[10];
typedef int (*funcptr_t)(array_t*);


int func (array_t* arr_ptr)
{
  return 0;
}

int main()
{
  int        array[10];
  array_t*   arr_ptr  = &array; /* non-obscure array pointer */
  funcptr_t  func_ptr = &func;  /* non-obscure function pointer */

  func_ptr(arr_ptr);
}

Le cose possono diventare ancora più oscure se si ha a che fare con array di puntatori a funzioni. O il più oscuro di tutti: funzioni che restituiscono puntatori a funzioni (leggermente utili). Se non usi typedef per tali cose, diventerai rapidamente pazzo.


Ah, finalmente una risposta ragionevole. :-) Sono curioso di sapere come la particolare sintassi ridurrebbe effettivamente la dimensione del codice sorgente, ma comunque è un'idea plausibile e ha senso. Grazie. +1
user541686

Direi che era meno sulla dimensione del codice sorgente e più sulla scrittura del compilatore, ma sicuramente +1 per "typdef away the weirdness". La mia salute mentale è migliorata notevolmente il giorno in cui ho capito che avrei potuto farlo.
detly

2
[Citazione necessaria] sulla cosa dimensione del codice sorgente. Non ho mai sentito parlare di una tale limitazione (anche se forse è qualcosa di "tutti sanno").
Sean McMillan,

1
Beh, ho codificato programmi negli anni '70 in COBOL, Assembler, CORAL e PL / 1 su IBM, DEC e XEROX kit e non ho mai incontrato una limitazione della dimensione del codice sorgente. Limitazioni su dimensioni dell'array, dimensioni eseguibili, dimensioni del nome del programma, ma mai dimensioni del codice sorgente.
James Anderson,

1
@Sean McMillan: Non credo che la dimensione del codice sorgente fosse una limitazione (considera che a quel tempo linguaggi verbosi come Pascal erano piuttosto popolari). E anche se fosse stato così, penso che sarebbe stato molto semplice pre-analizzare il codice sorgente e sostituire le parole chiave lunghe con codici brevi a un byte (come facevano ad esempio alcuni interpreti Basic). Quindi trovo un po 'debole l'argomento "C è conciso perché inventato in un periodo in cui era disponibile meno memoria".
Giorgio,

7

È piuttosto semplice: int *psignifica che *pè un int; int a[5]significa che a[i]è un int.

int (*f)(int (*a)[5])

Significa che *fè una funzione, *aè un array di cinque numeri interi, quindi fè una funzione che prende un puntatore a un array di cinque numeri interi e restituisce int. Tuttavia, in C non è utile passare un puntatore a un array.

Le dichiarazioni C raramente lo complicano.

Inoltre, puoi chiarire usando typedefs:

typedef int vec5[5];
int (*f)(vec5 *a);

4
Ci scusiamo se questo sembra maleducato (non voglio dire che lo sia), ma penso che ti sia perso l'intero punto della domanda ...: \
user541686

2
@Mehrdad: non posso dirti cosa c'era nella mente di Kernighan e Ritchie; Ti ho detto la logica dietro la sintassi. Non conosco la maggior parte delle persone, ma non credo che la tua sintassi suggerita sia più chiara.
Kevin Cline,

Sono d'accordo - è insolito vedere una dichiarazione così complicata.
Caleb,

Il design del C Dichiarazioni essere anteriore typedef, const, volatile, e la possibilità di inizializzare le cose all'interno di dichiarazioni. Molte delle fastidiose ambiguità della sintassi della dichiarazione (ad esempio se si int const *p, *q;dovrebbe legare constal tipo o al dichiarante) non potrebbero sorgere nella lingua come originariamente progettato. Vorrei che la lingua avesse aggiunto due punti tra il tipo e il dichiarando, ma ha permesso la sua omissione quando si utilizzavano i tipi di "parole riservate" incorporati senza qualificatori. Il significato di int: const *p,*q;e int const *: p,*q;sarebbe stato chiaro.
supercat

3

Penso che devi considerare * [] come operatori collegati a una variabile. * è scritto prima di una variabile, [] dopo.

Leggiamo l'espressione del tipo

int* (*p)[10];

L'elemento più interno è p, una variabile, quindi

p

significa: p è una variabile.

Prima della variabile c'è un *, l'operatore * viene sempre messo prima dell'espressione a cui fa riferimento, quindi,

(*p)

significa: la variabile p è un puntatore. Senza () l'operatore [] a destra avrebbe una precedenza più alta, vale a dire

**p[]

sarebbe analizzato come

*(*(p[]))

Il prossimo passo è []: poiché non esiste ulteriore (), [] ha quindi una precedenza maggiore rispetto a quella esterna *, quindi

(*p)[]

significa: (la variabile p è un puntatore) a un array. Quindi abbiamo il secondo *:

* (*p)[]

significa: ((la variabile p è un puntatore) a un array) di puntatori

Finalmente hai l'operatore int (un nome di tipo), che ha la precedenza più bassa:

int* (*p)[]

significa: (((la variabile p è un puntatore) a un array) di puntatori) a numero intero.

Quindi l'intero sistema si basa su espressioni di tipo con operatori e ogni operatore ha le proprie regole di precedenza. Ciò consente di definire tipi molto complessi.


0

Non è così difficile quando inizi a pensare e C non è mai stato un linguaggio molto semplice. E int*[10]* pdavvero non è più facile di int* (*p)[10] E in che tipo di k sarebbeint*[10]* p, k;


2
k sarebbe una revisione del codice fallita, posso capire cosa farà il compilatore, potrei anche essere disturbato, ma non riesco a capire cosa intendesse il programmatore - fallire ............
mattnz

e perché k non avrebbe superato la revisione del codice?
Dainius,

1
perché il codice è illeggibile e non mantenibile. Il codice non è corretto da correggere, ovviamente corretto e probabilmente rimarrà corretto durante la manutenzione. Il fatto che si debba chiedere quale tipo k sarà un segno che il codice non riesce a soddisfare questi requisiti di base.
mattnz,

1
Essenzialmente ci sono 3 (in questo caso) dichiarazioni di variabili di diverso tipo sulla stessa riga, ad es. Int * p, int i [10] e int k. Questo è inaccettabile. Sono accettabili dichiarazioni multiple dello stesso tipo, a condizione che le variabili abbiano una qualche forma di relazione, ad esempio larghezza, altezza, profondità; Tieni a mente che molte persone programmano usando int * p, quindi cosa ho in 'int * p, i;'.
mattnz,

1
Quello che @mattnz sta cercando di dire è che puoi essere intelligente quanto vuoi, ma è tutto insignificante quando il tuo intento non è ovvio e / o il tuo codice è scritto male / illeggibile. Questo tipo di cose si traduce spesso in codice rotto e tempo perso. Inoltre, pointer to inte intnon sono nemmeno dello stesso tipo, quindi dovrebbero essere dichiarati separatamente. Periodo. Ascolta l'uomo. Ha un rappresentante di 18k per un motivo.
Braden Best
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.