Perché * dichiarazione * di dati e funzioni è necessaria in linguaggio C, quando la definizione è scritta alla fine del codice sorgente?


15

Considera il seguente codice "C":

#include<stdio.h>
main()
{   
  printf("func:%d",Func_i());   
}

Func_i()
{
  int i=3;
  return i;
}

Func_i()è definito alla fine del codice sorgente e non viene fornita alcuna dichiarazione prima del suo utilizzo in main(). Nel momento in cui il compilatore vede Func_i()dentro main(), esce da main()e lo scopre Func_i(). Il compilatore in qualche modo trova il valore restituito Func_i()e lo fornisce printf(). So anche che il compilatore non riesce a trovare il tipo di ritorno di Func_i(). Di default prende (indovina?) Il tipo di ritorno di Func_i()essere int. Cioè se il codice avesse float Func_i()quindi il compilatore darebbe l'errore: tipi in conflitto perFunc_i() .

Dalla discussione sopra vediamo che:

  1. Il compilatore può trovare il valore restituito da Func_i().

    • Se il compilatore riesce a trovare il valore restituito Func_i()uscendo da main()e cercando nel codice sorgente, perché non riesce a trovare il tipo di Func_i (), che è esplicitamente menzionato.
  2. Il compilatore deve sapere che Func_i()è di tipo float - ecco perché dà l'errore di tipi in conflitto.

  • Se il compilatore sa che Func_iè di tipo float, allora perché assume ancora Func_i()di essere di tipo int e dà l'errore di tipi in conflitto? Perché non forzatamente rendere Func_i()di tipo float.

Ho lo stesso dubbio con la dichiarazione variabile . Considera il seguente codice "C":

#include<stdio.h>
main()
{
  /* [extern int Data_i;]--omitted the declaration */
  printf("func:%d and Var:%d",Func_i(),Data_i);
}

 Func_i()
{
  int i=3;
  return i;
}
int Data_i=4;

Il compilatore restituisce l'errore: "Data_i" non dichiarato (primo utilizzo in questa funzione).

  • Quando il compilatore vede Func_i(), scende al codice sorgente per trovare il valore restituito da Func_ (). Perché il compilatore non può fare lo stesso per la variabile Data_i?

Modificare:

Non conosco i dettagli del funzionamento interno di compilatore, assemblatore, processore ecc. L'idea di base della mia domanda è che se dico (scrivo) il valore di ritorno della funzione nel codice sorgente finalmente, dopo l'uso di quella funzione quindi il linguaggio "C" consente al computer di trovare quel valore senza dare alcun errore. Ora perché il computer non riesce a trovare il tipo in modo simile. Perché non è stato possibile trovare il tipo di Data_i poiché è stato trovato il valore restituito di Func_i (). Anche se uso l' extern data-type identifier;istruzione, non sto dicendo il valore che deve essere restituito da quell'identificatore (funzione / variabile). Se il computer riesce a trovare quel valore, perché non riesce a trovare il tipo. Perché abbiamo bisogno della dichiarazione anticipata?

Grazie.


7
Il compilatore non "trova" il valore restituito da Func_i. questo viene fatto al momento dell'esecuzione.
James McLeod,

26
Non ho espresso il mio voto negativo, ma la domanda si basa su alcuni gravi equivoci su come funzionano i compilatori e le vostre risposte nei commenti suggeriscono che avete ancora alcuni ostacoli concettuali da superare.
James McLeod,

4
Si noti che il primo codice di esempio non è stato un codice valido e conforme agli standard negli ultimi quindici anni; C99 ha reso l'assenza di un tipo restituito nelle definizioni delle funzioni e la dichiarazione implicita di Func_inon valido. Non c'è mai stata una regola per dichiarare implicitamente variabili indefinite, quindi il secondo frammento era sempre malformato. (Sì, i compilatori accettano ancora il primo campione perché era valido, se sciatto, sotto C89 / C90.)
Jonathan Leffler,

19
@ user31782: In conclusione alla domanda: perché la lingua X fa / richiede Y? Perché questa è la scelta fatta dai designer. Sembra che tu stia sostenendo che i progettisti di uno dei linguaggi di maggior successo di sempre avrebbero dovuto fare scelte diverse decenni fa piuttosto che cercare di capire quelle scelte nel contesto in cui sono state fatte. La risposta alla tua domanda: perché abbiamo bisogno di una dichiarazione anticipata? è stato fornito: poiché C utilizza un compilatore a passaggio singolo. La risposta più semplice alla maggior parte delle domande di follow-up è Perché allora non sarebbe un compilatore one-pass.
Mr.Mindor,

4
@ user31782 Vuoi davvero leggere il libro del drago per capire come funzionano effettivamente compilatori e processori: è semplicemente impossibile distillare tutte le conoscenze richieste in una singola risposta SO (o anche 100 in quella). Ottimo libro per chiunque abbia interesse per i compilatori.
Voo,

Risposte:


26

A causa C è un singolo passaggio , staticamente tipizzato , debolmente tipizzato , compilato linguaggio.

  1. Single-pass significa che il compilatore non guarda avanti per vedere la definizione di una funzione o variabile. Dal momento che il compilatore non guarda avanti, la dichiarazione di una funzione deve precedere l'uso della funzione, altrimenti il ​​compilatore non sa quale sia la sua firma di tipo. Tuttavia, la definizione della funzione può essere successivamente nello stesso file o addirittura in un altro file. Vedi punto 4.

    L'unica eccezione è il manufatto storico che si presume che funzioni e variabili non dichiarate siano di tipo "int". La pratica moderna consiste nell'evitare la tipizzazione implicita dichiarando sempre esplicitamente funzioni e variabili.

  2. Digitato staticamente significa che tutte le informazioni sul tipo vengono calcolate al momento della compilazione. Tali informazioni vengono quindi utilizzate per generare il codice macchina che viene eseguito in fase di esecuzione. Non esiste alcun concetto in C di tipizzazione runtime. Una volta un int, sempre un int, una volta un float, sempre un float. Tuttavia, questo fatto è in qualche modo oscurato dal punto successivo.

  3. Digitato in modo debole significa che il compilatore C genera automaticamente il codice per la conversione tra tipi numerici senza richiedere al programmatore di specificare esplicitamente le operazioni di conversione. A causa della digitazione statica, la stessa conversione verrà sempre eseguita allo stesso modo ogni volta attraverso il programma. Se un valore float viene convertito in un valore int in un determinato punto del codice, un valore float verrà sempre convertito in un valore int in quel punto del codice. Questo non può essere modificato in fase di esecuzione. Il valore stesso può cambiare da un'esecuzione del programma alla successiva, ovviamente, e le istruzioni condizionali possono cambiare quali sezioni di codice vengono eseguite in quale ordine, ma una determinata singola sezione di codice senza chiamate di funzione o condizionali eseguirà sempre l'esatto stesse operazioni ogni volta che viene eseguito.

  4. Compilato significa che il processo di analisi del codice sorgente leggibile dall'uomo e di trasformarlo in istruzioni leggibili dalla macchina viene eseguito completamente prima dell'esecuzione del programma. Quando il compilatore sta compilando una funzione, non ha conoscenza di ciò che incontrerà più in basso in un determinato file di origine. Tuttavia, una volta completata la compilazione (e assembly, collegamento, ecc.), Ciascuna funzione nell'eseguibile finito contiene puntatori numerici alle funzioni che chiamerà quando viene eseguita. Ecco perché main () può chiamare una funzione più in basso nel file sorgente. Quando main () viene effettivamente eseguito, conterrà un puntatore all'indirizzo di Func_i ().

    Il codice macchina è molto, molto specifico. Il codice per l'aggiunta di due numeri interi (3 + 2) è diverso da quello per l'aggiunta di due float (3.0 + 2.0). Entrambi sono diversi dall'aggiunta di un int a un float (3 + 2.0) e così via. Il compilatore determina per ogni punto in una funzione quale operazione esatta deve essere eseguita in quel punto e genera codice che esegue quell'operazione esatta. Una volta fatto ciò, non può essere modificato senza ricompilare la funzione.

Mettendo insieme tutti questi concetti, la ragione per cui main () non può "vedere" più in basso per determinare il tipo di Func_i () è che l'analisi del tipo avviene all'inizio del processo di compilazione. A quel punto, solo la parte del file sorgente fino alla definizione di main () è stata letta e analizzata e la definizione di Func_i () non è ancora nota al compilatore.

Il motivo per cui main () può "vedere" dove Func_i () deve chiamarlo è che la chiamata avviene in fase di esecuzione, dopo che la compilazione ha già risolto tutti i nomi e i tipi di tutti gli identificatori, assembly ha già convertito tutti i funzioni al codice macchina e il collegamento ha già inserito l'indirizzo corretto di ciascuna funzione in ogni posizione in cui viene chiamata.

Naturalmente, ho lasciato fuori la maggior parte dei dettagli cruenti. Il processo attuale è molto, molto più complicato. Spero di aver fornito una panoramica di alto livello per rispondere alle tue domande.

Inoltre, ricorda, ciò che ho scritto sopra si applica specificamente a C.

In altre lingue, il compilatore può effettuare più passaggi attraverso il codice sorgente, quindi il compilatore può raccogliere la definizione di Func_i () senza che sia stato dichiarato.

In altre lingue, funzioni e / o variabili possono essere tipizzate in modo dinamico, quindi una singola variabile può contenere o una singola funzione può essere passata o restituita, un numero intero, un float, una stringa, una matrice o un oggetto in momenti diversi.

In altre lingue, la digitazione può essere più forte, richiedendo che la conversione da virgola mobile a intero sia specificata in modo esplicito. In ancora altre lingue, la digitazione può essere più debole, consentendo di eseguire automaticamente la conversione dalla stringa "3.0" al float 3.0 all'intero 3.

E in altre lingue, il codice può essere interpretato una riga alla volta, oppure compilato in byte-code e quindi interpretato, o compilato just-in-time o sottoposto a una vasta gamma di altri schemi di esecuzione.


1
Grazie per una risposta all-in-one. La tua risposta e nikie è ciò che volevo sapere. Ad esempio Func_()+1: qui al momento della compilazione il compilatore deve conoscere il tipo di in Func_i()modo da generare il codice macchina appropriato. Forse o non è possibile gestire l'assembly Func_()+1chiamando il tipo in fase di esecuzione, oppure è possibile, ma in tal modo il programma rallenterà in fase di esecuzione. Penso, per ora è abbastanza per me.
user106313

1
Particolare importante delle funzioni implicitamente dichiarate di C: si presume che siano di tipo int func(...)... cioè prendono una lista di argomenti vari. Ciò significa che se si definisce una funzione come int putc(char)ma si dimentica di dichiararla, verrà invece chiamata come int putc(int)(perché viene promosso char passato attraverso un elenco di argomenti variadici int). Pertanto, mentre l'esempio del PO funzionava perché la sua firma corrispondeva alla dichiarazione implicita, è comprensibile il motivo per cui questo comportamento è stato scoraggiato (e sono stati aggiunti opportuni avvertimenti).
uliwitness

37

Un vincolo progettuale del linguaggio C era che doveva essere compilato da un compilatore single-pass, il che lo rende adatto a sistemi molto limitati dalla memoria. Pertanto, il compilatore conosce in qualsiasi momento solo le cose menzionate in precedenza. Il compilatore non può saltare avanti nel sorgente per trovare una dichiarazione di funzione e poi tornare indietro per compilare una chiamata a quella funzione. Pertanto, tutti i simboli devono essere dichiarati prima di essere utilizzati. Puoi pre-dichiarare una funzione come

int Func_i();

nella parte superiore o in un file di intestazione per aiutare il compilatore.

Nei tuoi esempi, usi due caratteristiche dubbie del linguaggio C che dovrebbero essere evitate:

  1. Se una funzione viene utilizzata prima che fosse dichiarata correttamente, questa viene utilizzata come una "dichiarazione implicita". Il compilatore utilizza il contesto immediato per capire la firma della funzione. Il compilatore non eseguirà la scansione del resto del codice per capire qual è la vera dichiarazione.

  2. Se qualcosa viene dichiarato senza un tipo, si considera che il tipo sia int. Questo è ad esempio il caso di variabili statiche o tipi di ritorno di funzione.

Quindi printf("func:%d",Func_i()), abbiamo una dichiarazione implicita int Func_i(). Quando il compilatore raggiunge la definizione della funzione Func_i() { ... }, questo è compatibile con il tipo. Ma se hai scritto float Func_i() { ... }a questo punto, hai l'implicita dichiarata int Func_i()e dichiarata esplicitamente float Func_i(). Poiché le due dichiarazioni non corrispondono, il compilatore ti dà un errore.

Chiarire alcune idee sbagliate

  • Il compilatore non trova il valore restituito da Func_i. L'assenza di un tipo esplicito significa che il tipo restituito è intpredefinito. Anche se lo fai:

    Func_i() {
        float f = 42.3;
        return f;
    }

    quindi il tipo sarà int Func_i()e il valore restituito verrà troncato silenziosamente!

  • Il compilatore alla fine arriva a conoscere il tipo reale di Func_i, ma non conosce il tipo reale durante la dichiarazione implicita. Solo quando in seguito raggiunge la dichiarazione reale può scoprire se il tipo dichiarato implicitamente era corretto. Ma a quel punto, l'assembly per la chiamata di funzione potrebbe essere già stato scritto e non può essere modificato nel modello di compilazione C.


3
@ user31782: l'ordine del codice è importante in fase di compilazione, ma non in fase di esecuzione. Il compilatore non viene visualizzato quando il programma viene eseguito. Per tempo di esecuzione, la funzione sarà stata assemblata e collegata, il suo indirizzo sarà stato risolto e bloccato nel segnaposto dell'indirizzo della chiamata. (È un po 'più complesso di così, ma questa è l'idea di base.) Il processore può diramarsi in avanti o indietro.
Blrfl,

20
@ user31782: il compilatore non stampa il valore. Il tuo compilatore non esegue il programma !!
Lightness Races con Monica il

1
@LightnessRacesinOrbit Lo so. Ho erroneamente scritto compilatore nel mio commento sopra perché ho dimenticato il processore dei nomi .
user106313

3
@Carcigenicato C è stato fortemente influenzato dal linguaggio B, che aveva un solo tipo: un tipo numerico integrale di larghezza di parola che poteva essere usato anche per i puntatori. C originariamente ha copiato questo comportamento, ma ora è completamente messo fuori legge dallo standard C99. Unitcrea un bel tipo di default dal punto di vista della teoria dei tipi, ma fallisce nelle praticità della programmazione dei sistemi vicini al metallo per cui B e C sono stati progettati.
amon,

2
@ user31782: Il compilatore deve conoscere il tipo di variabile per generare l'assemblaggio corretto per il processore. Quando il compilatore trova l'implicito Func_i(), genera immediatamente e salva il codice affinché il processore salti in un'altra posizione, quindi riceva un numero intero e quindi continui. Quando in seguito il compilatore trova la Func_idefinizione, si assicura che le firme corrispondano e, in tal caso, posiziona l'assembly per Func_i()quell'indirizzo e gli dice di restituire un numero intero. Quando si esegue il programma, il processore quindi segue quelle istruzioni con il valore 3.
Mooing Duck il

10

Innanzitutto, i programmi sono validi per lo standard C90, ma non per quelli seguenti. int implicito (che consente di dichiarare una funzione senza specificarne il tipo restituito) e dichiarazione implicita di funzioni (che consente di utilizzare una funzione senza dichiararla) non sono più validi.

Secondo, non funziona come pensi.

  1. Il tipo di risultato è facoltativo in C90, non dare uno significa un intrisultato. È vero anche per la dichiarazione di variabili (ma è necessario fornire una classe di archiviazione statico extern).

  2. Ciò che il compilatore fa quando vede il Func_iviene chiamato senza una precedente dichiarazione, presuppone che ci sia una dichiarazione

    extern int Func_i();

    non guarda oltre nel codice per vedere quanto Func_iè dichiarato efficacemente . Se Func_inon fosse dichiarato o definito, il compilatore non cambierebbe il suo comportamento durante la compilazione main. La dichiarazione implicita è solo per la funzione, non ce n'è per la variabile.

    Si noti che l'elenco di parametri vuoto nella dichiarazione non significa che la funzione non accetta parametri (è necessario specificarlo (void)), significa che il compilatore non deve controllare i tipi di parametri e sarà lo stesso conversioni implicite che vengono applicate ad argomenti passati a funzioni variadiche.


Se il compilatore riesce a trovare il valore restituito da Func_i () uscendo da main () e cercando nel codice sorgente, perché non riesce a trovare il tipo di Func_i (), che è esplicitamente menzionato.
user106313

1
@ user31782 Se non vi era alcuna dichiarazione precedente di Func_i, quando si vede che Func_i viene utilizzato in un'espressione di chiamata, si comporta come se ce ne fosse una extern int Func_i(). Non guarda da nessuna parte.
Programmatore il

1
@ user31782, il compilatore non salta da nessuna parte. Emetterà un codice per chiamare quella funzione; il valore restituito verrà determinato in fase di esecuzione. Bene, nel caso di una funzione così semplice che è presente nella stessa unità di compilazione, la fase di ottimizzazione può incorporare la funzione, ma non è qualcosa a cui dovresti pensare quando si considerano le regole del linguaggio, è un'ottimizzazione.
Programmatore il

10
@ user31782, hai gravi equivoci su come funzionano i programmi. Così serio che non credo che p.se sia un buon posto per correggerli (forse la chat, ma non proverò a farlo).
Programmatore il

1
@ user31782: scrivere un piccolo frammento e compilarlo con -S(se lo si sta utilizzando gcc) consentirà di esaminare il codice assembly generato dal compilatore. Quindi puoi avere un'idea di come vengono gestiti i valori di ritorno in fase di esecuzione (normalmente utilizzando un registro del processore o uno spazio nello stack del programma).
Giorgio,

7

Hai scritto in un commento:

L'esecuzione viene eseguita riga per riga. L'unico modo per trovare il valore restituito da Func_i () è saltare fuori dal main

È un'idea sbagliata: l'esecuzione non è completa. La compilazione viene eseguita riga per riga e la risoluzione dei nomi viene eseguita durante la compilazione e risolve solo i nomi, non restituisce valori.

Un modello concettuale utile è questo: quando il compilatore legge la riga:

  printf("func:%d",Func_i());

emette codice equivalente a:

  1. call "function #2" and put the return value on the stack
  2. put the constant string "func:%d" on the stack
  3. call "function #1"

Il compilatore prende nota anche in alcune tabelle interne che function #2è una funzione non ancora dichiarata Func_i, che accetta un numero non specificato di argomenti e restituisce un int (impostazione predefinita).

Più tardi, quando analizza questo:

 int Func_i() { ...

il compilatore cerca Func_inella tabella sopra menzionata e verifica se i parametri e il tipo restituito corrispondono. In caso contrario, si interrompe con un messaggio di errore. In tal caso, aggiunge l'indirizzo corrente alla tabella delle funzioni interne e passa alla riga successiva.

Quindi, il compilatore non "ha cercato" Func_iquando ha analizzato il primo riferimento. Ha semplicemente preso nota in qualche tavolo, ha continuato ad analizzare la riga successiva. E alla fine del file, ha un file oggetto e un elenco di indirizzi jump.

Successivamente, il linker prende tutto questo e sostituisce tutti i puntatori a "funzione # 2" con l'indirizzo di salto effettivo, quindi emette qualcosa del tipo:

  call 0x0001215 and put the result on the stack
  put constant ... on the stack
  call ...
...
[at offset 0x0001215 in the file, compiled result of Func_i]:
  put 3 on the stack
  return top of the stack

Molto più tardi, quando viene eseguito il file eseguibile, l'indirizzo di salto è già risolto e il computer può semplicemente saltare all'indirizzo 0x1215. Non è richiesta la ricerca del nome.

Disclaimer : come ho detto, questo è un modello concettuale, e il mondo reale è più complicato. Compilatori e linker fanno oggi tutti i tipi di ottimizzazioni pazze. Hanno anche potrebbero "saltare su un down" per cercare Func_i, anche se ne dubito. Ma i linguaggi C sono definiti in modo da poter scrivere un compilatore semplicissimo come quello. Quindi la maggior parte delle volte è un modello molto utile.


La ringrazio per la risposta. Il compilatore non può emettere il codice:1. call "function #2", put the return-type onto the stack and put the return value on the stack?
user106313

1
(Cont.) Inoltre: cosa succede se si scrive printf(..., Func_i()+1);: il compilatore deve conoscere il tipo di Func_i, in modo che possa decidere se deve emettere un'istruzione add integero add float. Potresti trovare alcuni casi speciali in cui il compilatore potrebbe continuare senza le informazioni sul tipo, ma il compilatore deve funzionare per tutti i casi.
Nikie,

4
@ user31782: Le istruzioni della macchina, di regola, sono molto semplici: aggiungi due registri interi a 32 bit. Carica un indirizzo di memoria in un registro intero a 16 bit. Vai a un indirizzo. Inoltre, non ci sono tipi : puoi felicemente caricare una posizione di memoria che rappresenta un numero float a 32 bit in un registro intero a 32 bit e fare un po 'di aritmetica con esso. (Raramente ha senso.) Quindi no, non è possibile emettere direttamente un codice macchina del genere. Potresti scrivere un compilatore che fa tutte quelle cose con controlli di runtime e dati di tipo extra nello stack. Ma non sarebbe un compilatore C.
Nikie,

1
@ user31782: dipende, IIRC. floati valori possono vivere in un registro FPU - quindi non ci sarebbe alcuna istruzione. Il compilatore tiene semplicemente traccia del valore memorizzato in quale registro durante la compilazione ed emette elementi come "aggiungi la costante 1 al registro FP X". Oppure potrebbe vivere nello stack, se non ci sono registri gratuiti. Quindi ci sarebbe "aumenta il puntatore dello stack di 4" e il valore verrebbe "referenziato" come qualcosa come "puntatore dello stack - 4". Ma tutte queste cose funzionano solo se le dimensioni di tutte le variabili (prima e dopo) nello stack sono note al momento della compilazione.
nikie il

1
Da tutta la discussione che ho raggiunto a questa comprensione: Per il compilatore di creare un codice assembly plausibile per qualsiasi istruzione incluso Func_i()o / e Data_i, deve determinare i loro tipi; nel linguaggio assembly non è possibile effettuare una chiamata al tipo di dati. Ho bisogno di studiare le cose in dettaglio per essere sicuro.
user106313

5

C e una serie di altre lingue che richiedono dichiarazioni sono state progettate in un'epoca in cui il tempo e la memoria del processore erano costosi. Lo sviluppo di C e Unix è andato di pari passo per un po 'di tempo, e quest'ultimo non aveva memoria virtuale fino alla comparsa di 3BSD nel 1979. Senza lo spazio extra per lavorare, i compilatori tendevano ad essere affari single-pass perché non lo facevano richiede la possibilità di conservare una parte dell'intero file in memoria contemporaneamente.

I compilatori single-pass sono, come noi, sellati dall'incapacità di guardare al futuro. Ciò significa che le uniche cose che possono sapere con certezza sono ciò che è stato loro detto esplicitamente prima che la riga di codice fosse compilata. È chiaro a nessuno di noi che Func_i()viene dichiarato più avanti nel file sorgente, ma il compilatore, che opera su una piccola porzione di codice alla volta, non ha idea che stia arrivando.

All'inizio C (AT&T, K&R, C89), l'uso di una funzione foo()prima della dichiarazione ha comportato una dichiarazione di fatto o implicita di int foo(). Il tuo esempio funziona quando Func_i()viene dichiarato intperché corrisponde a ciò che il compilatore ha dichiarato per tuo conto. La modifica in qualsiasi altro tipo comporterà un conflitto perché non corrisponde più a ciò che il compilatore ha scelto in assenza di una dichiarazione esplicita. Questo comportamento è stato rimosso in C99, dove l'uso di una funzione non dichiarata è diventato un errore.

E i tipi di restituzione?

La convenzione di chiamata per il codice oggetto nella maggior parte degli ambienti richiede di conoscere solo l'indirizzo della funzione chiamata, che è relativamente facile da gestire per compilatori e linker. L'esecuzione salta all'inizio della funzione e ritorna quando ritorna. Qualsiasi altra cosa, in particolare le disposizioni per passare argomenti e un valore di ritorno, è determinata interamente dal chiamante e chiamata in una disposizione chiamata convenzione chiamante . Finché entrambi condividono lo stesso insieme di convenzioni, diventa possibile per un programma richiamare funzioni in altri file oggetto indipendentemente dal fatto che siano state compilate in qualsiasi linguaggio che condivida tali convenzioni. (Nel calcolo scientifico, ti imbatti in un sacco di C che chiama FORTRAN e viceversa, e la capacità di farlo deriva dall'avere una convenzione di chiamata.)

Un'altra caratteristica dei primi C era che i prototipi come li conosciamo ora non esistevano. È possibile dichiarare il tipo restituito di una funzione (ad esempio, int foo()), ma non i suoi argomenti (ovvero, int foo(int bar)non era un'opzione). Ciò esisteva perché, come indicato sopra, il programma si è sempre attaccato a una convenzione di chiamata che potrebbe essere determinata dagli argomenti. Se hai chiamato una funzione con il tipo sbagliato di argomenti, era una situazione di immondizia, immondizia.

Poiché il codice oggetto ha la nozione di un ritorno ma non un tipo di ritorno, un compilatore deve conoscere il tipo di ritorno per gestire il valore restituito. Quando esegui le istruzioni della macchina, è tutto solo bit e al processore non importa se la memoria in cui stai provando a confrontare ne doubleha effettivamente una int. Fa solo quello che chiedi, e se lo rompi, possiedi entrambi i pezzi.

Considera questi bit di codice:

double foo();         double foo();
double x;             int x;
x = foo();            x = foo();

Il codice a sinistra viene compilato fino a una chiamata a foo()seguita copiando il risultato fornito tramite la convenzione di chiamata / ritorno in qualsiasi punto xvenga archiviato. Questo è il caso semplice.

Il codice a destra mostra una conversione del tipo ed è per questo che i compilatori devono conoscere il tipo di ritorno di una funzione. I numeri in virgola mobile non possono essere scaricati in memoria dove altri codici si aspettano di vederne uno intperché non c'è conversione magica che ha luogo. Se il risultato finale deve essere un numero intero, devono essere presenti istruzioni che guidano il processore a effettuare la conversione prima della memorizzazione. Senza conoscere in foo()anticipo il tipo di ritorno , il compilatore non avrebbe idea che il codice di conversione fosse necessario.

I compilatori multi-pass consentono ogni sorta di cose, una delle quali è la capacità di dichiarare variabili, funzioni e metodi dopo il loro primo utilizzo. Ciò significa che quando il compilatore si appresta a compilare il codice, ha già visto il futuro e sa cosa fare. Java, ad esempio, impone il multi-pass in virtù del fatto che la sua sintassi consente la dichiarazione dopo l'uso.


Grazie per la tua risposta (+1). Non conosco i dettagli del funzionamento interno di compilatore, assemblatore, processore ecc. L'idea di base della mia domanda è che se dico (scrivo) il valore di ritorno della funzione nel codice sorgente finalmente, dopo l'uso di quella funzione quindi la lingua consente al computer di trovare quel valore senza dare alcun errore. Ora perché il computer non riesce a trovare il tipo in modo simile. Perché non è stato possibile trovare il tipo di Data_i poiché Func_i()è stato trovato il valore di ritorno.
user106313

Non sono ancora soddisfatto. double foo(); int x; x = foo();dà semplicemente l'errore. So che non possiamo farlo. La mia domanda è che nella chiamata di funzione il processore trova solo il valore restituito; perché non riesce a trovare anche il tipo restituito?
user106313

1
@ user31782: non dovrebbe. C'è un prototipo per foo(), quindi il compilatore sa cosa farne.
Blrfl,

2
@ user31782: i processori non hanno alcuna nozione di tipo restituito.
Blrfl,

1
@ user31782 Per la domanda in fase di compilazione: è possibile scrivere una lingua in cui è possibile eseguire tutte queste analisi del tipo in fase di compilazione. C non è un linguaggio del genere. Il compilatore C non può farlo perché non è progettato per farlo. Potrebbe essere stato progettato diversamente? Certo, ma ci vorrebbe molta più potenza di elaborazione e memoria per farlo. In conclusione, non lo era. È stato progettato in un modo che i computer del giorno erano in grado di gestire al meglio.
Mr.Mindor,
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.