web-dev-qa-db-fra.com

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

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

tl; dr: Comment les fichiers source et d'en-tête sont-ils liés dans C? Les projets trient-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 .c et .h des dossiers.

Compte tenu de ces fichiers:

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

Ce désordre se compilera-t-il? Je fais actuellement mon travail dans NetBeans 7. avec gcc de Cygwin qui automatise une grande partie de la tâche de construction. Lors de la compilation d'un projet, les fichiers de projet concernés trieront-ils cette inclusion implicite de source.c basé sur les déclarations dans header.h?

50
Dan Lugg

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

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

Dans la première étape, le compilateur prend un nité de compilation, qui est normalement un fichier source prétraité (donc, un fichier source avec le contenu de tous les en-têtes qu'il #includes) et le convertit en fichier objet.

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 se trouve dans le fichier d'en-tête header.h. Lorsque vous compilez main.c, vous incluez l'en-tête avec la déclaration afin que le compilateur sache que returnSeven existe lorsqu'il compile 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 objet - s'il existe plusieurs fichiers objet 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 contient). Vous les compilez en deux fichiers objets: 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 collera ces deux éléments dans un programme exécutable.

À propos de la liaison:

Il y a liaison externe et liaison interne. Par défaut, les fonctions ont une liaison externe, ce qui signifie que le compilateur rend ces fonctions visibles à l'éditeur de liens. Si vous créez 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'elle existe). Cela peut être utile pour les fonctions qui font quelque chose en interne dans un fichier source et que vous souhaitez masquer du reste du programme.

72
Jesper

Le langage C n'a pas de concept de fichiers source et de fichiers d'en-tête (et le compilateur non plus). Ce n'est qu'une convention; rappelez-vous qu'un fichier d'en-tête est toujours #include d dans un fichier source; le préprocesseur ne fait que copier-coller littéralement le contenu, avant le début de la compilation.

Votre exemple devrait compiler (malgré les erreurs de syntaxe stupides). En utilisant GCC, par exemple, vous pourriez d'abord faire:

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

Cela compile chaque fichier source séparément, créant des fichiers objets indépendants. À ce stade, returnSeven() n'a pas été résolu dans main.c; le compilateur a simplement marqué le fichier objet d'une manière qui indique qu'il doit être résolu à l'avenir. Donc, à ce stade, ce n'est pas un problème si main.c Ne peut pas voir un définition de returnSeven(). (Remarque: ceci est distinct du fait que main.c Doit être capable de voir une déclaration de returnSeven() pour pouvoir compiler; il doit savoir qu'il est bien une fonction et quel est son prototype. C'est pourquoi vous devez #include "source.h" dans main.c.)

Vous faites ensuite:

gcc -o my_prog source.o main.o

Cette liens les deux fichiers objets ensemble dans un binaire exécutable, et effectue la résolution des symboles. Dans notre exemple, cela est possible, car main.o Nécessite returnSeven(), et cela est exposé par source.o. Dans les cas où tout ne correspond pas, une erreur de l'éditeur de liens en résulterait.

28

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

Les fichiers d'en-tête fournissent essentiellement des informations au compilateur, presque jamais de code.
Ces informations seules ne sont généralement pas suffisantes pour créer un programme complet.

Considérez le programme "hello world" (avec la fonction puts plus simple):

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

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

Le fonctionnement de la fonction n'est cependant spécifié nulle part dans ce code simple. Quelqu'un d'autre a écrit le code de puts() et 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.

Considérez maintenant que vous vouliez votre propre version de puts()

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

Compiler uniquement ce code donne une erreur car le compilateur n'a aucune information sur la fonction. Vous pouvez fournir ces informations

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

et le code compile maintenant --- mais ne lie pas, c'est-à-dire ne produit pas d'exécutable, car il n'y a pas de code pour myputs(). Vous écrivez donc le code de myputs() dans un fichier appelé "myputs.c"

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

et vous devez vous rappeler de compiler les deux votre premier fichier source et "myputs.c" ensemble.

Après un certain temps, votre fichier "myputs.c" s'est développé en une poignée de fonctions et vous devez inclure les informations sur toutes les fonctions (leurs prototypes) dans les fichiers source qui souhaitent les utiliser.
Il est plus pratique d'écrire tous les prototypes dans un seul fichier et #include Ce fichier. Avec l'inclusion, vous ne risquez pas de faire une erreur lors de la frappe du prototype.

Cependant, vous devez toujours compiler et lier tous les fichiers de code.


Quand ils grandissent encore plus, vous mettez tout le code déjà compilé dans une bibliothèque ... et c'est une autre histoire :)

13
pmg

Les fichiers d'en-tête sont utilisés pour séparer les déclarations d'interface qui correspondent aux implémentations dans les 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 réellement les deux fichiers séparément, ils sont combinés par le préprocesseur.

4
Hack Saw

Le compilateur lui-même n'a aucune "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 compilerait correctement. Vous devrez compiler les deux fichiers source, puis l'éditeur de liens aura besoin des deux fichiers objet pour produire l'exécutable.

2
Mark Wilkins