Perché il simbolo del puntatore e il segno di moltiplicazione sono uguali in C / C ++? [duplicare]

18

Sto scrivendo un parser di codice limitato C / C ++. Ora, i segni di moltiplicazione e puntatore mi danno davvero un momento difficile, poiché entrambi sono uguali. Ad esempio,

int main ()
{
  int foo(X * p); // forward declaration
  bar(x * y);  // function call
}

Devo applicare regole speciali per risolvere se * è effettivamente un puntatore. Nel codice precedente, devo scoprire se foo() è una dichiarazione diretta e bar() è una chiamata di funzione. Il codice del mondo reale può essere molto più complesso. Se ci fossero stati diversi simboli come @ per i puntatori, allora sarebbe stato semplice.

I puntatori sono stati introdotti in C , quindi perché alcuni simboli diversi non sono stati scelti per lo stesso? La tastiera era così limitata?

[Sarà un add-on se qualcuno può far luce sul modo in cui il parser dei moderni giorni si occupa di questo? Tieni presente che, in un unico scope X può essere typename e un altro scope può essere un nome di variabile, allo stesso tempo.]

    
posta iammilind 12.12.2011 - 10:20
fonte

6 risposte

17

Sì, gli stessi simboli sono stati riutilizzati, perché non c'erano ancora UTF32. Quindi hai * come tipo di puntatore, * come operatore di dereferenziazione, * come operatore di moltiplicazione, e questo è solo in C. Hai anche un problema simile con "&" ad esempio ("&" come address-off, "&" come bit-end e "&" come parte di "& &" - logico e), e altri.

I parser lessicali distinguono tra loro in base al contesto.

nel tuo esempio, hai due percorsi diversi nel parser: uno che inizia con un tipo (una dichiarazione variabile / avanti) e uno che non lo fa (chiamata di funzione). Se c'è un'ambiguità, ottieni un errore di compilazione.

Se utilizzi un sottoinsieme di C, devi assicurarti di ottenere il sottoinsieme giusto della grammatica che gestisce questo problema.

    
risposta data 12.12.2011 - 10:29
fonte
18

Non lo so per certo, ma sarei disposto a scommettere che la risposta è perché gli inventori di C si sono esauriti (o quasi esauriti) di personaggi.

L'intenzione originale era che la lingua usasse i simboli per gli operatori e che la lingua debba essere espressa all'interno della cosiddetta tabella ASCII "inferiore", che consiste di valori ASCII da 0 a 127.

I primi 31 valori sono caratteri di controllo non stampabili, quindi ci rimane questo:

!"#$%&'()*+,-./0123456789:;<=>?

@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_

'abcdefghijklmnopqrstuvwxyz{|}~

Rimuovi lo spazio e tutti i caratteri necessari per gli identificatori, (che sono tutte lettere, tutti i numeri e il trattino basso,) e ci rimane questo:

!"#$%&'()*+,-./:;<=>?@[\]^{|}~

Rimuovi parentesi, parentesi graffe e parentesi graffe, così come il backtick ('), che non è sufficientemente dissimile dalla singola citazione, e ci rimane questo:

!"#$%&'*+,-./:;<=>?@\^|~

Rimuovi la punteggiatura che è fondamentale per la sintassi della lingua, ("# ',.:;?) e ci rimane questo:

!$%&*+-/<=>@\^|~

Rimuovi la e commerciale (&), che sta per "prendere l'indirizzo di" qualcosa, e che deve quindi essere nettamente diversa da qualsiasi cosa abbia a che fare con i puntatori, così come i confronti, (< = >) , esclamativo (!), più (+) e meno (-), che sono operazioni valide da eseguire sui puntatori e ci rimane questo:

$%*/@\^|~

Non sono sicuro del motivo per cui il simbolo del dollaro e l'at-sign non sono stati considerati; forse si pensava che fosse un po 'troppo voluminoso per fare un uso frequente del codice. Quindi, portali fuori e ci rimangono questi:

%*/\^|~

Come puoi vedere, tutti i caratteri precedenti sono usati come operatori per varie operazioni aritmetiche e logiche in C, il che significa che hanno dovuto riutilizzarne uno come carattere per i puntatori di dichiarazione e dereferenziamento.

Hanno scelto l'asterisco (*), che è una scelta decente perché in qualche modo porta la nozione di un "punto", e poiché la moltiplicazione non è una delle operazioni che sono valide per eseguire sui puntatori.

Potrebbero aver scelto anche il segno di omissione (^), (che sta per OR esclusivo), ma non penso che sia un grosso problema e, chiaramente, le loro opzioni erano molto limitate.

    
risposta data 08.06.2015 - 15:34
fonte
7

Ci sono due regole nel C BNF che rendono difficile la scrittura di un parser:

if-statement: "if" expression statement |
              "if" expression statement "else" statement

conduce al problema ciondolante , dove

if a if b c else d

può essere analizzato come uno dei

(if-statement (a) (if-statement (b) (c) (d)))
(if-statement (a) (if-statement (b) (c)) (d))

La risoluzione tipica per questo conflitto è di preferire shift su ridurre , allegando il ramo "else" alla istruzione if interna.

L'altro, problema più difficile è

typedef-name: identifier

che rende la lingua sensibile al contesto . Questo conflitto viene risolto omettendo la regola nel parser e creando un token separato per i nomi typedef; perché questo funzioni, lo scanner deve avere una tabella di nomi che sono stati dichiarati come typedef .

Per C ++, le regole sono molto più complesse e di solito è più semplice scrivere uno scanner / parser integrato che risolva tutti gli identificatori.

    
risposta data 12.12.2011 - 14:53
fonte
4

Sono risolti usando le tabelle dei simboli. Questa tabella dei simboli tiene traccia delle dichiarazioni che hai visto per risolvere se stai chiamando o meno una funzione che è già stata dichiarata. Quando il parser incontra un identificatore, effettua una ricerca nella tabella dei simboli per vedere di cosa si tratta. Troverai situazioni simili per i nomi dei tipi- specialmente poiché C ha più spazi dei nomi e puoi iniziare a ombreggiarli. Queste regole non sono banali.

typedef int MahInt;
MahInt * p; // declaration or multiplication?

Non è possibile analizzare C senza tabelle di simboli.

Per questo motivo è così, perché le tastiere al momento erano molto limitate - per esempio digrammi e trigrammi, che producono simboli che erano inusuali al momento. Ad esempio, si assiste all'operatore "WTF":

int x, y;
x ??!??! y;

che è veramente

x || y
    
risposta data 12.12.2011 - 12:03
fonte
3

Il typedef era un'aggiunta relativamente tardiva al linguaggio C.

Nelle versioni precedenti di C, la grammatica definiva il nome o il nome di un tipo abbastanza semplicemente. Molti nomi di tipi: int , char , double , ecc. Erano (e sono tuttora) singole parole chiave. Altri nomi di tipi includevano parole chiave o simboli: struct foo , char * , int[42] . Un identificatore senza parole chiave non potrebbe mai essere un nome di tipo.

Quando il costrutto typedef è stato aggiunto alla lingua esistente, non è stato possibile modificare la grammatica per poter trattare un identificatore senza parole chiave come nome di un tipo senza creare ambiguità o rompere il codice esistente. Ad esempio, in:

int foo() {
    x*y;
}

x*y potrebbe essere una dichiarazione di espressione che moltiplica x di y e scarta il risultato, o una dichiarazione di y come puntatore a digitare x .

Un modo per guardarlo è che typedef crea una nuova parola chiave, una che esiste solo fino alla fine dell'ambito in cui è definita. Ciò significa che il parser deve guardare la tabella dei simboli per sapere come interpretare le cose (qualcosa che non è vero per molti altri linguaggi, ad esempio, in Pascal un identificatore può essere un nome di tipo, e questo non introduce un ambiguità).

Ad esempio:

int x, y;
int foo() {
     x * y; /* x isn't type name, so this is an expression statement */
     {
         typedef int x;
         x *y; /* Now x is a type name (effectively a keyword),
                  so this is a declaration */
     }
     x * y; /* The type name x is now out of scope,
               so it's an expression statement again */
}

(Trattare i nomi typedef come parole chiave è solo un modo per vederlo, non intendo suggerire che i compilatori lo facciano effettivamente internamente.)

    
risposta data 13.12.2011 - 00:15
fonte
1

Considera di utilizzare un parsing GLR , ti permette di posticipare la scelta tra le interpretazioni ambigue della sintassi finché non hai digitare informazioni. Ecco come funzionano i parser come Elsa.

Un'altra alternativa è quella di raccogliere ed espandere i tipi durante l'analisi - ad esempio, funziona bene con una discesa ricorsiva ad hoc che analizza l'implementazione. Questo approccio è utilizzato sia in gcc che in Clang .

Ci sono più possibilità disponibili: puoi ancora usare un generatore di alto livello che produce un parser Packrat con le specifiche PEG - in questo modo puoi inserire più logica di effetti collaterali in un parsing senza dover implementare l'intera cosa manualmente, come con un approccio ad hoc.

    
risposta data 13.12.2011 - 11:14
fonte

Leggi altre domande sui tag