Come funzionano i file di intestazione e di origine in C?

Ho esaminato i possibili duplicati, ma nessuna delle risposte ci sta affondando.

tl; dr: come sono correlati i file di origine e di intestazione in C ? I progetti risolvono le dipendenze di dichiarazione / definizione implicitamente al momento della compilazione?

Sto cercando di capire come il compilatore capisca la relazione tra file .c e .h .

Dati questi file:

header.h :

     int returnSeven(void); 

    source.c :

     int returnSeven(void){ return 7; } 

    main.c :

     #include  #include  #include "header.h" int main(void){ printf("%d", returnSeven()); return 0; } 

    Questo pasticcio sarà compilato? Attualmente sto facendo il mio lavoro in NetBeans 7.0 con gcc da Cygwin che automatizza gran parte dell’attività di compilazione. Quando un progetto viene compilato, i file di progetto coinvolti source.c questa inclusione implicita di source.c base alle dichiarazioni in header.h ?

    La conversione di file di codice sorgente C in un programma eseguibile viene normalmente eseguita in due passaggi: compilazione e collegamento .

    Innanzitutto, il compilatore converte il codice sorgente in file object ( *.o ). Quindi, il linker acquisisce questi file object, insieme a librerie collegate staticamente e crea un programma eseguibile.

    Nel primo passo, il compilatore prende un’unità di compilazione , che normalmente è un file sorgente preelaborato (quindi, un file sorgente con il contenuto di tutte le intestazioni che #include ) e lo converte in un file object.

    In ogni unità di compilazione, tutte le funzioni utilizzate devono essere dichiarate , per consentire al compilatore di sapere che la funzione esiste e quali sono i suoi argomenti. Nel tuo esempio, la dichiarazione della funzione returnSeven trova nel file header header.h . Quando compilate main.c , includete l’intestazione con la dichiarazione in modo che il compilatore sappia che returnSeven esiste quando compila main.c

    Quando il linker fa il suo lavoro, ha bisogno di trovare la definizione di ogni funzione. Ogni funzione deve essere definita esattamente una volta in uno dei file object – se ci sono più file object che contengono la definizione della stessa funzione, il linker si fermerà con un errore.

    La tua funzione returnSeven è definita in source.c (e la funzione main è definita in main.c ).

    Quindi, per riassumere, hai due unità di compilazione: source.c e main.c (con i file di intestazione che include). Si compila questi in due file object: source.o e main.o Il primo conterrà la definizione di returnSeven , la seconda la definizione di main . Quindi il linker li incollerà insieme in un programma eseguibile.

    Informazioni sul collegamento:

    Esiste un collegamento esterno e un collegamento interno . Per impostazione predefinita, le funzioni hanno un collegamento esterno, il che significa che il compilatore rende queste funzioni visibili al linker. Se si esegue una funzione static , ha un collegamento interno: è visibile solo all’interno dell’unità di compilazione in cui è definita (il linker non saprà che esiste). Questo può essere utile per le funzioni che eseguono qualcosa internamente in un file sorgente e che si desidera hide dal resto del programma.

    Il linguaggio C non ha alcun concetto di file sorgente e di header (e nemmeno il compilatore). Questa è solo una convenzione; ricorda che un file di intestazione è sempre #include d in un file sorgente; il preprocessore letteralmente copia e incolla il contenuto, prima che inizi la corretta compilazione.

    Il tuo esempio dovrebbe essere compilato (nonostante gli errori di syntax insensati). Ad esempio, utilizzando GCC, potresti prima fare:

     gcc -c -o source.o source.c gcc -c -o main.o main.c 

    Questo compila separatamente ciascun file sorgente, creando file object indipendenti. In questa fase, returnSeven() non è stato risolto all’interno di main.c ; il compilatore ha semplicemente contrassegnato il file object in un modo che afferma che deve essere risolto in futuro. Quindi, in questa fase, non è un problema che main.c non possa vedere una definizione di returnSeven() . (Nota: questo è distinto dal fatto che main.c deve essere in grado di vedere una dichiarazione di returnSeven() per compilare, deve sapere che è davvero una funzione e che cos’è il suo prototipo. Ecco perché è necessario #include "source.h" in #include "source.h" )

    Quindi fai:

     gcc -o my_prog source.o main.o 

    Questo collega i due file object insieme in un eseguibile binario ed esegue la risoluzione dei simboli. Nel nostro esempio, questo è ansible, perché main.o richiede returnSeven() , e questo è esposto da source.o . Nei casi in cui tutto non corrisponde, si otterrebbe un errore del linker.

    Non c’è nulla di magico nella compilazione. Né automatico!

    I file di intestazione forniscono fondamentalmente informazioni al compilatore, quasi mai codice.
    Quella informazione da sola, di solito non è sufficiente per creare un programma completo.

    Considera il programma “ciao mondo” (con la funzione puts più semplice):

     #include  int main(void) { puts("Hello, World!"); return 0; } 

    senza l’intestazione, il compilatore non sa come gestire puts() (non è una parola chiave C). L’intestazione consente al compilatore di sapere come gestire gli argomenti e restituire il valore.

    Come funziona la funzione, tuttavia, non è specificata da nessuna parte in questo semplice codice. Qualcun altro ha scritto il codice per puts() e ha incluso il codice compilato in una libreria. Il codice in quella libreria è incluso con il codice compilato per la tua fonte come parte del processo di compilazione.

    Ora considera di volere la tua versione di puts()

     int main(void) { myputs("Hello, World!"); return 0; } 

    Compilare solo questo codice dà un errore perché il compilatore non ha informazioni sulla funzione. È ansible fornire tali informazioni

     int myputs(const char *line); int main(void) { myputs("Hello, World!"); return 0; } 

    e il codice ora compila — ma non collega, cioè non produce un eseguibile, perché non esiste un codice per myputs() . Quindi scrivi il codice per myputs() in un file chiamato “myputs.c”

     #include  int myputs(const char *line) { while (*line) putchar(*line++); return 0; } 

    e devi ricordare di compilare insieme il tuo primo file sorgente e “myputs.c”.

    Dopo un po ‘il tuo file “myputs.c” si è espanso in una mano piena di funzioni e devi includere le informazioni su tutte le funzioni (i loro prototipi) nei file sorgente che vogliono usarle.
    È più conveniente scrivere tutti i prototipi in un singolo file e #include quel file. Con l’inclusione non corri alcun rischio di commettere un errore durante la digitazione del prototipo.

    Devi comunque compilare e colbind tutti i file di codice.


    Quando crescono ancora di più, metti tutto il codice già compilato in una libreria … e questa è un’altra storia 🙂

    I file di intestazione vengono utilizzati per separare le dichiarazioni dell’interfaccia che corrispondono alle implementazioni nei file di origine. Sono abusati in altri modi, ma questo è il caso comune. Questo non è per il compilatore, è per gli umani che scrivono il codice.

    La maggior parte dei compilatori in realtà non vede i due file separatamente, sono combinati dal preprocessore.

    Lo stesso compilatore non ha una “conoscenza” specifica delle relazioni tra i file di origine e i file di intestazione. Questi tipi di relazioni sono in genere definiti dai file di progetto (ad es. Makefile, soluzione, ecc.).

    L’esempio dato appare come se fosse compilato correttamente. Avresti bisogno di compilare entrambi i file sorgente e quindi il linker avrebbe bisogno di entrambi i file object per produrre l’eseguibile.