Comment fonctionnent les fichiers en-tête et source dans C?

J’ai parcouru les possibles doublons, mais aucune des réponses ne s’enfonce.

tl; dr: Comment les fichiers source et en-tête sont-ils associés en C ? Les projets sortingent-ils implicitement les dépendances de déclaration / définition au moment de la construction?

J’essaie de comprendre comment le compilateur comprend la relation entre les fichiers .h et .h .

Compte tenu de ces fichiers:

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; } 

Est-ce que ce gâchis se comstackra? Je travaille actuellement sur NetBeans 7.0 avec gcc de Cygwin qui automatise une grande partie de la tâche de compilation. Quand un projet est compilé, les fichiers de projet impliqués tiendront-ils cette inclusion implicite de source.c basant sur les déclarations de header.h ?

La conversion des fichiers de code source C en un programme exécutable se fait généralement en deux étapes: la compilation et la liaison .

Tout d’abord, le compilateur convertit le code source en fichiers objects ( *.o ). Ensuite, l’éditeur de liens prend ces fichiers objects, ainsi que les bibliothèques liées de manière statique, et crée un programme exécutable.

Dans la première étape, le compilateur prend une unité de compilation , qui est normalement un fichier source prétraité (donc un fichier source contenant le contenu de tous les en-têtes s #include ) et le convertit en un fichier object.

Dans chaque unité de compilation, toutes les fonctions utilisées doivent être déclarées pour que le compilateur sache que la fonction existe et quels sont ses arguments. Dans votre exemple, la déclaration de la fonction returnSeven trouve dans le fichier d’en-tête header.h . Lorsque vous comstackz main.c , vous incluez l’en-tête avec la déclaration afin que le compilateur sache que returnSeven existe lorsqu’il comstack main.c

Lorsque l’éditeur de liens fait son travail, il doit trouver la définition de chaque fonction. Chaque fonction doit être définie exactement une fois dans l’un des fichiers d’object. S’il existe plusieurs fichiers d’object contenant la définition de la même fonction, l’éditeur de liens s’arrête avec une erreur.

Votre fonction returnSeven est définie dans source.c (et la fonction main est définie dans main.c ).

Donc, pour résumer, vous avez deux unités de compilation: source.c et main.c (avec les fichiers d’en-tête qu’il inclut). Vous les comstackz en deux fichiers objects: source.o et main.o Le premier contiendra la définition de returnSeven , le second la définition de main . Ensuite, l’éditeur de liens les collera dans un programme exécutable.

À propos de la liaison:

Il y a des liens externes et des liens internes . Par défaut, les fonctions ont un lien externe, ce qui signifie que le compilateur rend ces fonctions visibles à l’éditeur de liens. Si vous faites une fonction static , elle a un lien interne – elle n’est visible qu’à l’intérieur de l’unité de compilation dans laquelle elle est définie (l’éditeur de liens ne saura pas qu’il existe). Cela peut être utile pour les fonctions qui font quelque chose en interne dans un fichier source et que vous souhaitez masquer du rest du programme.

Le langage C n’a pas de concept de fichiers source et de fichiers d’en-tête (et le compilateur non plus). Ceci est simplement une convention. rappelez-vous qu’un fichier d’en-tête est toujours #include d dans un fichier source; le préprocesseur se contente littéralement de copier les contenus avant que la compilation proprement dite commence.

Votre exemple devrait comstackr (nonobstant les erreurs de syntaxe stupides). Par exemple, en utilisant GCC, vous pouvez commencer par:

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

Cela comstack chaque fichier source séparément, créant des fichiers objects indépendants. A ce stade, returnSeven() n’a pas été résolu dans main.c ; le compilateur a simplement marqué le fichier object d’une manière qui indique qu’il doit être résolu dans le futur. Donc, à ce stade, main.c ne peut pas voir une définition de returnSeven() . (Remarque: ceci est distinct du fait que main.c doit être capable de voir une déclaration de returnSeven() pour comstackr; il doit savoir que c’est bien une fonction, et quel est son prototype. C’est pourquoi vous devez #include "source.h" dans main.c )

Vous faites alors:

 gcc -o my_prog source.o main.o 

Cela lie les deux fichiers d’object en un binary exécutable et effectue la résolution des symboles. Dans notre exemple, cela est possible, car main.o requirejs returnSeven() , ce qui est exposé par source.o . Dans les cas où tout ne correspond pas, une erreur de l’éditeur de liens en résulterait.

La compilation n’a rien de magique. Ni automatique!

Les fichiers d’en-tête fournissent essentiellement des informations au compilateur, pratiquement jamais de code.
Cette information seule n’est généralement pas suffisante pour créer un programme complet.

Considérons le programme “hello world” (avec la fonction de mise simple):

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

sans en-tête, le compilateur ne sait pas comment gérer les put puts() (ce n’est pas un mot clé C). L’en-tête permet au compilateur de savoir comment gérer les arguments et la valeur de retour.

Le fonctionnement de la fonction n’est toutefois spécifié nulle part dans ce code simple. Quelqu’un d’autre a écrit le code pour puts() et a inclus le code compilé dans une bibliothèque. Le code de cette bibliothèque est inclus avec le code compilé pour votre source dans le cadre du processus de compilation.

Maintenant, considérez que vous vouliez votre propre version de puts()

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

Comstackr simplement ce code donne une erreur car le compilateur n’a aucune information sur la fonction. Vous pouvez fournir cette information

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

et le code comstack maintenant — mais ne lie pas, c’est-à-dire qu’il ne produit pas d’exécutable, car il n’y a pas de code pour myputs() . Donc, vous écrivez le code pour myputs() dans un fichier appelé “myputs.c”

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

et vous devez vous rappeler de comstackr à la fois votre premier fichier source et “myputs.c” ensemble.

Après un certain temps, votre fichier “myputs.c” a été étendu à une main pleine de fonctions et vous devez inclure les informations sur toutes les fonctions (leurs prototypes) dans les fichiers source qui veulent les utiliser.
Il est plus pratique d’écrire tous les prototypes dans un seul fichier et d’ #include ce fichier. Avec l’inclusion, vous ne risquez pas de faire une erreur en tapant le prototype.

Vous devez toujours comstackr et lier tous les fichiers de code ensemble.


Quand ils grandissent encore plus, vous mettez tout le code déjà compilé dans une bibliothèque … et c’est une autre histoire 🙂

Les fichiers d’en-tête permettent de séparer les déclarations d’interface correspondant aux implémentations des fichiers source. Ils sont maltraités par d’autres moyens, mais c’est le cas commun. Ce n’est pas pour le compilateur, c’est pour les humains qui écrivent le code.

La plupart des compilateurs ne voient pas les deux fichiers séparément, ils sont combinés par le préprocesseur.

Le compilateur lui-même n’a pas de “connaissance” spécifique des relations entre les fichiers source et les fichiers d’en-tête. Ces types de relations sont généralement définis par des fichiers de projet (par exemple, makefile, solution, etc.).

L’exemple donné apparaît comme s’il se comstackrait correctement. Vous devrez comstackr les deux fichiers source, puis l’éditeur de liens aura besoin des deux fichiers objects pour produire l’exécutable.