La compilation et les modules en C et en C++
Cet article introduit les notions de bases liées à la compilation en C et en C++, notamment en cas de la programmation modulaire.
Il permet de mieux comprendre les messages d'erreur du compilateur. Les notions abordées ici sont indépendantes du système d'exploitation (Windows, Linux, ...), même si les exemples considèrent que l'on est sous Linux avec le compilateur
Une fois le binaire généré, celui-ci est directement utilisable sans qu'on ait besoin du code source. Un langage compilé (comme le C ou le C++) se distingue donc d'un langage interprété (shell, PHP, ...) ou pseudo-interprété (python, ...).
Exemple : Sous Debian ou les distributions qui en dérivent
On peut éventuellement installer un environnement de développement comme par exemple
Article connexe : ou trouver un compilateur c c
Par convention :
Ensuite, on compile le programme :
La compilation (au sens vague du terme) se déroule en trois grandes phases détaillées ci-dessous.
C'est notamment à ce moment là que les
Cette étape est assurée par
Au cours de cette étape, le linker vérifie en particulier que chaque fonction appelée dans le programme n'est pas seulement déclarée (ceci est fait lors de la compilation), mais aussi implémentée (chose qu'il n'avait pas vérifié à ce stade) une et une seule fois.
Cette phase, appelée aussi édition de lien, constitue la phase finale pour obtenir un exécutable (pas d'extension sous linux, extension
Cette étape est assurée par
Définitions :
Cette commence enchaîne les trois étapes décrites précédemment.
1) Précompilation
2) Compilation (il trouve bien le
3) Édition de lien (il trouve bien le printf dans le binaire de la lib c). On peut d'ailleurs le vérifier sous linux avec
Sur la deuxième ligne on voit bien qu'il utilise la librairie c. À la fin du linkage, l'exécutable
Résultat :
Si une erreur se produit à ce moment là (langage erreur de segmentation, pas assez de mémoire etc...) il faut souvent recourir à un débogeur (par exemple
Méthode 1 : via les commandes ms-dos (en cliquant sur démarrer exécuter (Win+4) et en tapant
Méthode 2 : Si on lance le programme depuis l'explorateur, windows lance les commandes ms-dos, qui lance le programme, celui-ci se termine, rend la main aux commandes ms-dos, et windows ferme les commandes ms-dos. Concrètement on n'a alors rien le temps de voir. Il faut donc mettre une "pause" juste avant la fin du programme si on veut lancer ton exécutable de cette manière :
Supposons que :
1) Si l'on compile juste
C'est pourquoi le fichier contenant le main (pas d'option -c) et les autres fichiers sources se compilent différemment. En l'occurrence :
Si le programme comporte une erreur dans
2) Rappelons qu'en temps normal, on déclare la fonction dans le header (par exemple
... et qu'on l'implémente dans le fichier source (par exemple
Supposons à présent que j'implémente la fonction
... alors que
Le fichier
Cela explique pourquoi l'implémentation d'une fonction doit se faire en général dans un fichier source (
Si on ne prend pas de précautions, le fichier
C'est pourquoi pour s'en prémunir, on protège les headers ainsi.
Exemple pour a.h :
(même principe pour
Ainsi, quelque soit l'ordre dans lequel le compilateur traite les fichiers, chaque définition ne sera considérée qu'une et une seule fois.
Ce fichier peut être écrit manuellement ou au travers de générateurs de makefile comme
Une fois le fichier
Il permet de mieux comprendre les messages d'erreur du compilateur. Les notions abordées ici sont indépendantes du système d'exploitation (Windows, Linux, ...), même si les exemples considèrent que l'on est sous Linux avec le compilateur
gccou
g++.

- Introduction
- Installation d'un compilateur C et C++
- Les phases de compilation
- Warnings et erreurs
- Les grandes étapes pour écrire un programme en C ou en C++
- Les erreurs classiques
- Plus loin avec la compilation : makefile
Introduction
De manière générale, un compilateur convertit un fichier texte (contenant un code source) en binaire (par exemple un exécutable ou une librairie) directement compréhensibles par le micro-processeur.Une fois le binaire généré, celui-ci est directement utilisable sans qu'on ait besoin du code source. Un langage compilé (comme le C ou le C++) se distingue donc d'un langage interprété (shell, PHP, ...) ou pseudo-interprété (python, ...).
Installation d'un compilateur C et C++
Linux
En général, on utilisegcc(pour le C) et
g++(pour le C++).
Exemple : Sous Debian ou les distributions qui en dérivent
sudo update sudo apt install gcc g++
On peut éventuellement installer un environnement de développement comme par exemple
kdevelop(KDE) ou
anjuta(gnome).
Windows
On peut utiliserdev-cppou www.codeblocks.org
code::blocks, reposant tous les deux sur
gccet
g++:
Article connexe : ou trouver un compilateur c c
Les phases de compilation
On commence par écrire ou récupérer le code source (écrit en langage C ou C++) à compiler.Par convention :
- en C : les fichiers ont pour extension
.c
(source) et.h
(header) ; - en C++ : les fichiers ont pour extension
.cpp
(source) et.hpp
(header) ;
Ensuite, on compile le programme :
- en C : avec
gcc
; - en C++ : avec
g++
.
La compilation (au sens vague du terme) se déroule en trois grandes phases détaillées ci-dessous.
1) La précompilation (pré-processeur)
Le compilateur commence par appliquer chaque instruction passée au préprocesseur (toutes les lignes qui commencent par un#, dont les
#define). Ces instructions sont en fait très simples car elles ne consistent en gros qu'à recopier ou supprimer des sections de codes sans chercher à les compiler. C'est de la substitution de texte, ni plus ni moins.
C'est notamment à ce moment là que les
#defineprésents dans un fichier source (
.cou
.cpp) ou dans un header (
.hou
.hpp) sont remplacés par du code C/C++. À l'issue de cette étape, il n'y a donc plus, pour le compilateur, d'instructions commençant par un
#.
2) La compilation
Le compilateur compile chaque fichier source (.cet
.cpp) individuellement. Chacun engendre un fichier binaire (
.o).
Cette étape est assurée par
cclorsqu'on utilise
gcc/
g++.
3) Le linkage
Enfin, le compilateur agrège et lie les fichiers.oproduits, les éventuelles librairies tierces (fichiers
.aet
.sosous linux, fichiers
.dllet
.libsous windows).
- Une librairie dynamique (.dll et .so) n'est pas recopiée dans l'exécutable final (ce qui signifie que le programme est plus petit et bénéficiera des mises à jour de ladite librairie). En contrepartie, la librairie doit être présente sur le système sur lequel tourne le programme.
- Une librairie statique (.a) est recopiée dans l'exécutable final ce qui fait que celui-ci est complètement indépendant des librairies installées du le système sur lequel il sera recopié. En contrepartie, l'exécutable est plus gros, il ne bénéficie pas des mises à jour de cette librairie etc...
Au cours de cette étape, le linker vérifie en particulier que chaque fonction appelée dans le programme n'est pas seulement déclarée (ceci est fait lors de la compilation), mais aussi implémentée (chose qu'il n'avait pas vérifié à ce stade) une et une seule fois.
Cette phase, appelée aussi édition de lien, constitue la phase finale pour obtenir un exécutable (pas d'extension sous linux, extension
.exesous windows).
Cette étape est assurée par
ldlorsqu'on utilise
gcc/
g++.
Warnings et erreurs
Dans un environnement de développement, il suffit de cliquer sur "build" et ces trois phases s'enchaînent de manière transparente. Toutefois, il est important de les avoir en tête pour interpréter correctement les messages d'erreur ou de warning d'un compilateur lorsqu'il y en a.Définitions :
- Un warning signifie que le code est ambigu et que le code peut être interprété différemment d'un compilateur à l'autre, mais l'exécutable peut être créé.
- Une erreur signifie que le code n'a pas pu être compilé complètement et que le binaire n'a pas pu être (re)créé. Si un code peut compiler avec des warnings et doit compiler sans erreurs, il vaut mieux essayer de coder de sorte à n'avoir ni erreur, ni warning.
Les grandes étapes pour écrire un programme en C ou en C++
Écrire le code source
Un simple bloc note peut suffire, par exemple on peut écrire dans le fichier plop.c :#include <stdio.h> int main(){ printf("plop !\n"); return 0; }
Compiler
Sous linux on peut appeler directementgcc(les options
-Wet
-Wallpermettent d'afficher plus de messages pour vérifier si le code est propre, le
-o ploppermet de dire que l'exécutable à créer doit s'appeler
plop) :
gcc -W -Wall -o plop plop.c
Cette commence enchaîne les trois étapes décrites précédemment.
1) Précompilation
/* Tout ce qui est défini par <stdio.h>, y compris printf() */ int main(){ printf("plop !\n"); return 0; }
2) Compilation (il trouve bien le
printfcar celui-ci est déclaré dans <stdio.h>)
3) Édition de lien (il trouve bien le printf dans le binaire de la lib c). On peut d'ailleurs le vérifier sous linux avec
ldd plop:
linux-gate.so.1 => (0xb7f2b000)
libc.so.6 => /lib/i686/cmov/libc.so.6 (0xb7dbb000)
/lib/ld-linux.so.2 (0xb7f2c000)
Sur la deuxième ligne on voit bien qu'il utilise la librairie c. À la fin du linkage, l'exécutable
plopest créé.
Exécution
Sous Linux
Il ne reste plus qu'à le lancer le programme :./plop
Résultat :
plop !
Si une erreur se produit à ce moment là (langage erreur de segmentation, pas assez de mémoire etc...) il faut souvent recourir à un débogeur (par exemple
gdbou
ddd), revoir le code source, etc... Dans tous les cas, ce n'est plus un problème de compilation mais une erreur de programmation.
Sous Windows
Pour exécuter un programme sous windows, deux méthodes sont possibles.Méthode 1 : via les commandes ms-dos (en cliquant sur démarrer exécuter (Win+4) et en tapant
cmd). On se place ensuite dans le bon répertoire avec la commande
cdet on lance le programme. Dans ce cas tout se passera bien.
Méthode 2 : Si on lance le programme depuis l'explorateur, windows lance les commandes ms-dos, qui lance le programme, celui-ci se termine, rend la main aux commandes ms-dos, et windows ferme les commandes ms-dos. Concrètement on n'a alors rien le temps de voir. Il faut donc mettre une "pause" juste avant la fin du programme si on veut lancer ton exécutable de cette manière :
#include <stdio.h> int main(){ printf("plop !\n"); getchar(); /* le programme n'avance plus tant qu'on appuie pas sur une touche */ return 0; }
Les erreurs classiques
Erreur de compilation
Supposons que le code source oublie d'inclure le fichier<stdio.h>(dans lequel est déclarée la fonction
printf), ou qu'un
;soit manquant. Dans ce cas, le compilateur renverra un message d'erreur (syntax error, parse error...).
Erreur de linkage
Ces erreurs sont plus subtiles, car elles ne concernent pas la syntaxe, mais la manière dont est structuré et compilé le projet. Elles sont faciles à reconnaître quand on utilisegccou
g++puisque les messages d'erreurs correspondant parlent explicitement de
ld(le linker).
Mutlidéfinition
Une erreur de linkage peut survenir dès que l'on écrit le code d'un programme à l'aide de plusieurs fichiers. Nous allons à présent illustrer ce genre d'erreur.Supposons que :
- le projet soit implémenté au travers de 3 fichiers :
a.h
,a.c
, etmain.c
; - le header
a.h
soit inclus par les deux fichiers sourcemain.c
eta.c
; - le fichier
main.c
contienne la fonctionmain()
.
1) Si l'on compile juste
a.c, le fichier ne contenant pas de fonction
main, il faut le préciser au compilateur (option
-cdans
gcc), sans quoi celui-ci tentera d'en faire un exécutable et plantera faute de trouver la fonction
main.
C'est pourquoi le fichier contenant le main (pas d'option -c) et les autres fichiers sources se compilent différemment. En l'occurrence :
gcc -W -Wall -c a.c gcc -W -Wall -o plop main.c a.o
- La première commande construit
a.o
à partir dea.c
. - La seconde génère l'exécutable
plop
à partirmain.c
eta.o
.
Si le programme comporte une erreur dans
a.c, le compilateur déclenchera une erreur au moment de compiler
a.c. Ceci provoquera des erreurs en cascade dans les autres fichiers. C'est pourquoi lorsqu'un programme ne compile pas, on commence par les premiers messages d'erreurs, on les résout, on recompile etc... jusqu'à ce que toutes les erreurs soient résolues.
2) Rappelons qu'en temps normal, on déclare la fonction dans le header (par exemple
a.h) :
void plop();
... et qu'on l'implémente dans le fichier source (par exemple
a.c) :
#include "a.h" #include <stdio.h> void plop(){ printf("plop !\n"); }
Supposons à présent que j'implémente la fonction
plop()dans
a.h, et que ce dernier contienne :
#include <stdio.h> void plop(){ printf("plop !\n"); }
... alors que
a.ccontient :
#include "a.h" void f(){ plop(); }
Le fichier
a.hest inclus par
main.cet
a.c. Ainsi, le contenu de
a.hest recopié dans
a.cet dans
main.c(précompilation). Ainsi, ces deux fichiers sources disposeront chacun d'une implémentation de la fonction
plop()(la même certes !), mais le compilateur ne saura pas laquelle prendre et déclenchera une erreur de multi définition au moment du linkage:
(mando@aldur) (~) $ gcc -W -Wall main.c a.o
a.o: In function `plop':
a.c:(.text+0x0): multiple definition of `plop'
/tmp/ccmRKAvQ.o:main.c:(.text+0x0): first defined here
collect2: ld returned 1 exit status
Cela explique pourquoi l'implémentation d'une fonction doit se faire en général dans un fichier source (
.cou
.cpp) et non dans un header (
.het
.hpp). On retiendra juste deux exceptions à cette règle en C++ : les fonctions/méthodes
inlineet les fonctions/méthodes
templates.
Boucles d'inclusion et inclusion multiples.
Supposons que :- le projet comporte les fichiers suivant :
main.c
,a.h
,b.h
etc.h
(et leurs fichiers source correspondantsa.c
,b.c
,c.c
; -
a.h
etb.h
incluent tout deuxc.h
.
Si on ne prend pas de précautions, le fichier
c.hétant inclus à différents endroit, ses déclarations vont apparaître dans
a.o,
b.oet
c.oet engendrer une erreur au moment du linkage.
C'est pourquoi pour s'en prémunir, on protège les headers ainsi.
Exemple pour a.h :
#ifndef A_H #define A_H // Contenu de a.h #endif
(même principe pour
b.het
c.h)
Ainsi, quelque soit l'ordre dans lequel le compilateur traite les fichiers, chaque définition ne sera considérée qu'une et une seule fois.
Fonction déclarée ... mais pas trouvée
Si une fonction est déclarée, utilisée, mais pas implémentée, une erreur de linkage se produit également. Ceci survient typiquement dans deux cas :- on a déclaré une fonction mais on ne l'a pas encore implémentée
- on veut utiliser une fonction d'une librairie, on a correctement inclu les headers correspondant, mais on a oublié de passer en paramètre du compilateur lesdites librairies.
Plus loin avec la compilation : makefile
Afin d'éviter de taper à chaque fois les commandesgccpour chaque fichier source (ce qui est très fastidieux, surtout quand le projet devient gros), on scripte souvent la compilation à l'aide d'un fichier
Makefile.
Ce fichier peut être écrit manuellement ou au travers de générateurs de makefile comme
cmake.
Une fois le fichier
Makefileprêt et correct, et sous réserve que la commande
makesoit installée, il suffit alors de taper la commande suivante pour compiler le projet :
make