Toujours dans le cadre d’un de mes TP de programmation en C, je travail sur la réalisation d’un modèle de skip-list et dans l’élaboration des mes différents fichiers, j’ai pas mal de soucis quant à la dépendance des fichiers .h ainsi que leur compilation.
N’ayant pas encore tout compris sur ce système de dépendance qui me parait particulier encore selon certains cas, j’ai pensé que mon problème ne venait pas seulement de la déclaration des différents #include dans mes fichiers .c ou .h mais de ma conception de mon Makefile et particulièrement l’ordre des dépendances déclaré.
C’est pour cela que je vais tenter d’effectuer une recherche sur les dépendances d’inclusions et de compilation dans ce billet.
Vous êtes de la partie ?
1. Test d’un programme basique
Pour commencer, rien de mieux qu’un petit « Hello world! » standard. Hop, lancez un Terminal et déterminez un répertoire de projet pour notre article :
~/Prog% mkdir rand0 ~/Prog% cd rand0 ~/Prog/rand0% vim main.c Makefile
Code source
Code source du fichier main.c :
1 2 3 4 5 6 7 8 9 10 11 12 13 | #include <stdio.h> #include <stdlib.h> void says_hello() { printf("Hello world!\n"); } int main () { says_hello(); return EXIT_SUCCESS; } |
Voici rien de bien compliqué, juste une petite fonction qui va afficher un simple « Hello world! » dans la console.
Passons au Makefile.
Makefile
Code source du fichier Makefile :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | # Définition du compilateur à utiliser CC ?= gcc # Définition des paramètres de compilation FLAGS ?= -W -Wall -ansi -pedantic -ggdb # Tout notre programme se résumera à rand0 (nom de notre binaire final) all: rand0 # rand0 dépend de main.o et de hello.o rand0: main.o $(CC) -o rand0 main.o # main.o dépend de main.c (c'est sa propre compilation) main.o: main.c $(CC) -o main.o -c main.c $(FLAGS) # Commande pour effacer les fichiers compilés clean: rm -rf *.o *~ rand0 |
Compilons notre programme et exécutons-le :
~/Prog/rand0% make cc -o main.o -c main.c -W -Wall -ansi -pedantic -ggdb cc -o rand0 main.o ~/Prog/rand0% ./rand0 Hello world! ~/Prog/rand0%
Pas d’erreur ni de warning théoriquement. On constate tout de même que la compilation de main.o est précédée de celle de rand0. Cela paraît logique puisque rand0 a besoin de main.o afin d’être compilé (c’est plus de l’assemblage à ce niveau-là).
Pour vérifier cela, nous allons maintenant procéder à la décomposition de notre programme.
2. Décomposition de notre programme
Pour faire ceci, nous allons transférer tout ce qui concerne notre « Hello world! » dans un jeu de fichier hello.c et hello.h et seulement se contenter d’appeler says_hello() dans main.c.
Aller, hop, c’est parti :
~/Prog/rand0% vim hello.h hello.c
Code source
Code source du fichier hello.h :
1 2 3 4 5 6 | #ifndef _HELLO #define _HELLO void says_hello(); #endif |
Code source du fichier hello.c :
1 2 3 4 5 6 7 8 | #include <stdio.h> #include <stdlib.h> #include "hello.h" void says_hello() { printf("Hello world!\n"); } |
Code source du fichier main.c :
1 2 3 4 5 6 7 8 9 | #include <stdio.h> #include <stdlib.h> #include "hello.h" int main () { says_hello(); return EXIT_SUCCESS; } |
A ce stade, si nous compilons, le compilateur nous envoie une erreur car il ne parvient pas à trouver la fonction says_hello() :
~/Prog/rand0% make cc -o main.o -c main.c -W -Wall -ansi -pedantic -ggdb cc -o rand0 main.o Undefined symbols: "_says_hello", referenced from: _main in main.o ld: symbol(s) not found collect2: ld returned 1 exit status make: *** [rand0] Error 1 zsh: exit 2 make ~/Prog/rand0%
Pour palier à ce souci de compilation, nous allons ajouter la dépendance dans notre Makefile.
Makefile
Code source du fichier Makefile :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | # Définition du compilateur à utiliser CC ?= gcc # Définition des paramètres de compilation FLAGS ?= -W -Wall -ansi -pedantic -ggdb # Tout notre programme se résumera à rand0 (nom de notre binaire final) all: rand0 # rand0 dépend de main.o et de hello.o rand0: main.o hello.o $(CC) -o rand0 main.o hello.o # main.o dépend de main.c (c'est sa propre compilation) main.o: main.c $(CC) -o main.o -c main.c $(FLAGS) # hello.o dépend de hello.c (c'est sa propre compilation) hello.o: hello.c $(CC) -o hello.o -c hello.c $(FLAGS) # Commande pour effacer les fichiers compilés clean: rm -rf *.o *~ rand0 |
On compile et on exécute :
~/Prog/rand0% make cc -o main.o -c main.c -W -Wall -ansi -pedantic -ggdb cc -o hello.o -c hello.c -W -Wall -ansi -pedantic -ggdb cc -o rand0 main.o hello.o ~/Prog/rand0% ./rand0 Hello world! ~/Prog/rand0%
On constate que main.o est compilé avant hello.o mais que cela n’affecte pas la compilation globale de notre programme. Seulement, lorsque je compile, j’aime garder les notions de dépendances le plus faible en début de compilation. Et puis cela ne paraît pas logique.
On verra comment inverser l’ordre plus bas…
3. Tests sur les dépendances d’inclusion
Avant de bidouiller avec nos fichiers .h, nous allons les supprimer de nos en-têtes de fichiers .c ainsi :
Code source
Code source du fichier main.c :
1 2 3 4 5 6 7 8 | #include <stdio.h> #include <stdlib.h> int main () { says_hello(); return EXIT_SUCCESS; } |
Code source du fichier hello.c :
1 2 3 4 5 6 7 | #include <stdio.h> #include <stdlib.h> void says_hello() { printf("Hello world!\n"); } |
On nettoie, on compile et on exécute :
~/Prog/rand0% make clean rm -rf *.o *~ rand0 ~/Prog/rand0% make cc -o main.o -c main.c -W -Wall -ansi -pedantic -ggdb main.c: In function ‘main’: main.c:6: warning: implicit declaration of function ‘says_hello’ cc -o hello.o -c hello.c -W -Wall -ansi -pedantic -ggdb cc -o rand0 main.o hello.o ~/Prog/rand0% ./rand0 Hello world! ~/Prog/rand0%
Nous constatons que le compilateur n’a pas besoin du fichier header .h pour compiler, assembler et exécuter le programme. Un simple « warning » est levé donc rien de bloquant.
Étudions un cas bloquant maintenant…
Ce qui va être intéressant, c’est de savoir à quel endroit notre fichier hello.h aura-t’il besoin d’être inclus. Pour cela, nous allons ajouter la définition d’un type Hello qui correspondra à une structure et nous allons l’initialiser et l’utiliser sans notre fonction says_hello() :
Code source
Code source du fichier hello.h :
1 2 3 4 5 6 7 8 9 10 11 12 | #ifndef _HELLO #define _HELLO typedef struct Hello Hello; struct Hello { int foo; }; void says_hello(); #endif |
Code source du fichier hello.c :
1 2 3 4 5 6 7 8 9 | #include <stdio.h> #include <stdlib.h> void says_hello() { Hello entier; entier.foo = 3; printf("Hello world!\nHere is my number: %d\n", entier.foo); } |
Vous avez déjà deviné ce qui allait se passer… Faisons tout de même le test : compilons :
~/Prog/rand0% make clean rm -rf *.o *~ rand0 ~/Prog/rand0% make cc -o main.o -c main.c -W -Wall -ansi -pedantic -ggdb main.c: In function ‘main’: main.c:6: warning: implicit declaration of function ‘says_hello’ cc -o hello.o -c hello.c -W -Wall -ansi -pedantic -ggdb hello.c: In function ‘says_hello’: hello.c:6: error: ‘Hello’ undeclared (first use in this function) hello.c:6: error: (Each undeclared identifier is reported only once hello.c:6: error: for each function it appears in.) hello.c:6: error: syntax error before ‘entier’ hello.c:7: error: ‘entier’ undeclared (first use in this function) make: *** [hello.o] Error 1 zsh: exit 2 make ~/Prog/rand0%
C’est clairement écrit : le type Hello n’est pas déclaré donc impossible de l’utiliser, cela nous lève donc une erreur de syntaxe juste avant « entier » dans la compilation de notre fichier hello.o.
Si maintenant nous ajoutons la ligne nécessaire (#include "hello.h"), et que nous compilons puis exécutons :
~/Prog/rand0% make cc -o main.o -c main.c -W -Wall -ansi -pedantic -ggdb main.c: In function ‘main’: main.c:6: warning: implicit declaration of function ‘says_hello’ cc -o hello.o -c hello.c -W -Wall -ansi -pedantic -ggdb cc -o rand0 main.o hello.o ~/Prog/rand0% ./rand0 Hello world! Here is my number: 3 ~/Prog/rand0%
Ok, ça marche mais toujours avec le warning donc il faut aussi le rajouter dans main.c !
Il faut comprendre que les fichiers d’en-têtes sont utiles lorsque que nous avons besoin des déclarations de prototypes ou lorsque vous faites appel à une fonction qui elle-même en appelle une autre déclarée plus bas dans le fichier.
4. Tests sur les dépendances de compilation
Comme écrit plus haut, nous allons tenter d’inverser la compilation de nos fichiers .o et comprendre comment cela se fait.
Makefile
Fragment du code source du fichier Makefile :
10 11 12 | # rand0 dépend de main.o et de hello.o rand0: main.o hello.o $(CC) -o rand0 hello.o main.o |
Si on compile, (faire un make clean préalablement), on constate que l’ordre de compilation n’a pas changé.
Maintenant, changeons l’ordre des dépendances de compilation de « rand0 » :
Fragment du code source du fichier Makefile :
10 11 12 | # rand0 dépend de main.o et de hello.o rand0: hello.o main.o $(CC) -o rand0 hello.o main.o |
Après sauvegarde et compilation, nous obtenons bien un changement d’ordre :
~/Prog/rand0% make clean rm -rf *.o *~ rand0 ~/Prog/rand0% make cc -o hello.o -c hello.c -W -Wall -ansi -pedantic -ggdb cc -o main.o -c main.c -W -Wall -ansi -pedantic -ggdb cc -o rand0 hello.o main.o ~/Prog/rand0%
Donc on comprend bien que la ligne du dessus (rand0: hello.o main.o) correspond en fait aux dépendances de fichiers .o afin d’assembler notre programme alors que la ligne du dessous ($(CC) -o rand0 hello.o main.o) correspond à la commande qui sera passée au shell afin de d’assembler les fichiers .o.
Dans ce cas de dépendance, quelque soit l’ordre de compilation des objets, cela ne semble pas avoir d’influence sur l’assemblage final de notre programme. Cependant, il se pourrait que cela ait une importance dans certains cas de dépendance extrême mais je n’ai pas encore trouvé de cas alors si vous avez un retour d’expérience dessus, merci de m’en faire part
.
5. Conclusion
Pour avoir galéré avec pas plus de 4 paires de fichiers .c et .h contenant des dépendances d’inclusions de types, il a fallu que j’effectue plusieurs tests afin de bien comprendre comment et pourquoi cela ne fonctionnait pas.
La seule chose que vous devez retenir, c’est qu’à chaque fois que vous utilisez une fonction, un type ou quoique ce soit d’autre dans votre fichier et qui n’est pas déclaré dedans ou dans votre en-tête, vous devez inclure son en-tête.
Téléchargez les sources de l’exemple complet : rand0_dependances.tgz (1 Ko).

sympa
Il faut savoir que les fichiers en-têtes ne sont que des bouts de fichiers sources qui ne contiennent que des déclarations de structure et fonctions, ainsi que des directives de préprocesseurs (ajout de bibliothèques standard, constantes de programme, etc. ). En gros, tu peux faire un programme sans aucun fichier en-tête. Il suffit d’inclure le fichier source correspondant : #include « hello.c ».
Personne ne le fait parce que c’est sale et généralement, le fichier source est en binaire pour ne pas qu’on pique le code
De plus, peu de gens commentent le fichier en-tête, ce qui casse un peu l’utilité du fichier, mais enfin, ce n’est pas grave
Pour ma part, j’adore commenter un fichier en-tête dans tous les sens, car c’est un peu l’interface du programmeur qui va utiliser mon code (quand on distribue le code, on donne le fichier .h et le fichier .dll). Sinon, il n’y a aucun intérêt de faire un fichier d’en-tête