Domanda 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 collegati i file di origine e di intestazione C? I progetti risolvono le dipendenze di dichiarazione / definizione implicitamente al momento della compilazione?

Sto cercando di capire come il compilatore capisce la relazione tra .c e .h File.

Dati questi file:

header.h:

int returnSeven(void);

source.c:

int returnSeven(void){
    return 7;
}

main.c:

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

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


44
2018-05-05 21:52


origine


risposte:


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 oggetto (*.o). Quindi, il linker acquisisce questi file oggetto, insieme a librerie collegate staticamente e crea un programma eseguibile.

Nel primo passaggio, il compilatore accetta a unità di compilazione, che normalmente è un file sorgente preelaborato (quindi, un file sorgente con il contenuto di tutte le intestazioni che lo contiene #includes) e lo converte in un file oggetto.

In ogni unità di compilazione, devono essere tutte le funzioni utilizzate dichiarato, per far sapere al compilatore che la funzione esiste e quali sono i suoi argomenti. Nel tuo esempio, la dichiarazione della funzione returnSeven è nel file di intestazione header.h. Quando compili main.c, includi l'intestazione con la dichiarazione in modo che il compilatore lo sappia returnSeven esiste quando compila main.c.

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

La tua funzione returnSeven è definito in source.c (e il main la funzione è 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 oggetto: source.o e main.o. Il primo conterrà la definizione di returnSeven, il secondo la definizione di main. Quindi il linker li incollerà insieme in un programma eseguibile.

Informazioni sul collegamento:

C'è collegamento esterno e collegamento interno. Per impostazione predefinita, le funzioni hanno un collegamento esterno, il che significa che il compilatore rende queste funzioni visibili al linker. Se fai una funzione static, ha un collegamento interno - è visibile solo all'interno dell'unità di compilazione in cui è definito (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 nascondere dal resto del programma.


61
2018-05-05 22:16



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 #included in un file sorgente; il preprocessore letteralmente copia e incolla il contenuto, prima che inizi la corretta compilazione.

Il tuo esempio dovrebbero compilare (nonostante gli errori di sintassi 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 oggetto indipendenti. In questa fase, returnSeven() non è stato risolto all'interno main.c; il compilatore ha semplicemente contrassegnato il file oggetto in un modo che afferma che deve essere risolto in futuro. Quindi, in questa fase, non è un problema main.c non posso vedere a definizione di returnSeven(). (Nota: questo è distinto dal fatto che main.c deve essere in grado di vedere a dichiarazione di returnSeven() per compilare; deve sapere che è davvero una funzione e qual è il suo prototipo. Questo è il motivo per cui devi #include "source.h" in main.c.)

Quindi fai:

gcc -o my_prog source.o main.o

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


25
2018-05-05 21:57



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 il più semplice puts funzione):

#include <stdio.h>
int main(void) {
    puts("Hello, World!");
    return 0;
}

senza l'intestazione, il compilatore non sa come comportarsi 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 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. È possibile 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 myputs() in un file chiamato "myputs.c"

#include <stdio.h>
int myputs(const char *line) {
    while (*line) putchar(*line++);
    return 0;
}

e devi ricordarti di compilare entrambi il tuo primo file sorgente e "myputs.c" insieme.

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 collegare tutti i file di codice.


Quando crescono ancora di più, metti tutto il codice già compilato in una libreria ... e questa è un'altra storia :)


11
2018-05-05 22:25



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.


4
2018-05-05 22:01



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 oggetto per produrre l'eseguibile.


2
2018-05-05 21:58