Posez votre question »

La compilation et les modules en C et en C++

Mai 2015

Cet article a pour vocation d'introduire les notions de bases de la compilation en C et en C++ et de la programmation modulaire.

Il permet de mieux comprendre les messages d'erreur du compilateur. Les notions abordées ici sont indépendantes du fait que l'on utilise windows ou linux. Toutefois on se placera plutôt dans le cadre de linux afin de voir plus en détail tout se qui se passe lors de la compilation d'un programme C ou C++.




Introduction


De manière générale, un compilateur a pour objectif de convertir un fichier texte (contenant un code source) en un fichier binaire (par exemple un exécutable). Une fois l'exécutable construit, on le lance comme n'importe quel programme. Ce programme peut se lancer sans que l'on dispose du code source.

Un langage compilé (comme le C ou le C++) est à opposer à un langage interprété (par exemple un script shell) ou pseudo-interprété (par exemple un programme python).

Dans le cadre du C, la compilation va transformer le code C d'un programme en code natif, à savoir une suite d'instructions binaires directement compréhensibles par le processeur.

Installation d'un compilateur C et C++


Sous linux


On utilise gcc et g++ en général. Pour l'installer on passe par son gestionnaire de paquet habituel. Par exemple sous debian (ou sous une distribution basée sur debian) il suffit de taper en root ou avec un sudo :
aptitude update
aptitude safe-upgrade
aptitude install gcc g++

On peut également installer de la même façon un environnement de développement comme par exemple kdevelop (sous KDE) ou anjuta (sous gnome).

Sous windows


On peut utiliser dev-cpp ou code::blocks: deux environnements de développement libres qui reposent sur gcc et g++ :
www.bloodshed.net
www.codeblocks.org

Article connexe : ou trouver un compilateur c c

Les phases de compilation


La première étape consiste à écrire le code source en langage C ou C++ (fichiers .c et .h en C et .cpp et .hpp en C++). Ensuite on lance une compilation, par exemple avec gcc (en C) ou g++ (en C++). La compilation (au sens vague du terme) se déroule en trois grandes phases.

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 #define présents dans un fichier source (.c ou .cpp) ou dans un header (.h ou .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


Ensuite, le compilateur compile chaque fichier source (.c et .cpp), c'est-à-dire qu'il crée un fichier binaire (.o) par fichier source, excepté pour le fichier contenant la fonction main. Cette phase constitue la compilation proprement dite.

Ces deux premières étapes sont assurées par cc lorsqu'on utilise gcc/g++. Enfin le compilateur passe au fichier contenant le main.

3) Le linkage


Enfin, le compilateur agrège chaque fichier .o avec les éventuels fichiers binaires des librairies qui sont utilisées (fichiers .a et .so sous linux, fichiers .dll et .lib sous 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...

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). Il vérifie aussi qu'une fonction n'est pas implémentée dans plusieurs fichiers .o.

Cette phase, appelée aussi édition de lien, constitue la phase finale pour obtenir un exécutable (noté .exe sous windows, en général pas d'extension sous linux).

Cette étape est assurée par ld lorsqu'on utilise gcc/g++.

Warnings et erreurs


Évidemment, dans un environnement de développement, il suffit de cliquer sur "build" et ces trois phases se déroulent 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.

Je rappelle qu'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éé.

A contrario, une erreur signifie que le code n'a pas pu être compilé complètement et que l'exécutable 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


Ici je suis sous linux donc j'appelle directement gcc (-W et -Wall permettent d'afficher plus de messages pour vérifier si le code est propre, le -o plop.exe permet de dire que l'exécutable à créer doit s'appeler plop.exe) :
gcc -W -Wall -o plop.exe plop.c

Implicitement le compilateur fait les trois étapes que j'ai décrites juste avant.

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 printf car 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 :
ldd plop.exe

.. qui donne :
        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 lib c. Puis il crée plop.exe. On vérifie en outre qu'il n'y a ni erreur, ni warning.

Exécution


Il ne reste plus qu'à le lancer :
./plop.exe

... ce qui donne comme espéré :
plop !

Si une erreur se produit à ce moment là (erreur de segmentation, pas assez de mémoire etc...) il faut souvent recourir à un debuggeur (par exemple gdb ou ddd), revoir le code source etc... Dans tous les cas, ce n'est pas un problème de compilation.

Article connexe : langage c c c erreur de segmentation

Attention, gros piège sous windows

Pour exécuter un programme sous windows, deux méthodes sont possibles.

Méthode 1 : On peut lancer un programme via les commandes ms-dos (en cliquant sur démarrer exécuter et en tapant "cmd"). On se place ensuite dans le bon répertoire avec la commande cd et 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 mon code source oublie d'inclure le fichier <stdio.h> (dans lequel est déclarée la fonction printf), ou un ";", j'aurai alors un message d'erreur de compilation (syntax error, parse error...). Ce sont les erreurs les plus classiques.

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 programme. Elles sont faciles à reconnaître quand on utilise gcc ou g++ puisque les messages d'erreurs correspondant parlent de ld (le linker).

Premier exemple : 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 mon programme est écrit au travers de 3 fichiers : a.h, a.c, et main.c. Le header a.h est inclu par les deux fichiers source main.c et a.c. Le fichier main.c contient la fonction main().

1) Si je compile juste a.c, le fichier de contenant pas de main, il faut le préciser au compilateur (option -c dans gcc) sinon celui-ci ne sait pas comment créer un exécutable, vu qu'il n'y a pas de point de départ. 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.exe main.c a.o

Les options -W et -Wall permettent d'afficher plus de messages de warning.
- La première commande construit a.o à partir de a.c.
- La seconde génère le binaire associé à main.c, l'assemble avec a.o, et produit ainsi un exécutable (plop.exe)

On voit tout de suite que 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 (c'est à dire que la fonction n'est pas juste déclarée dans a.h). En d'autre termes, le fichier a.h contient
#include <stdio.h>

void plop(){
  printf("plop !\n");
}

... et le fichier a.c contient par exemple :
#include "a.h"

void f(){
  plop();
}

Le fichier a.h est inclu par main.c et a.c. Ainsi le contenu de a.h est recopié dans a.c et dans main.c. 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

Ceci justifie pourquoi l'implémentation d'une fonction doit se faire en général dans un fichier source (.c ou .cpp) et non dans un header (.h et .hpp). On retiendra juste deux exceptions à cette règle en C++ : les fonctions inline et les fonctions templates (ou les méthodes d'une classe template). Pour plus de détails on pourra se référer à ces deux articles :
http://www.commentcamarche.net/faq/sujet 11194 les templates en c
http://www.commentcamarche.net/faq/sujet 11250 les inlines en c

Programmation modulaire : multi définitions, boucles d'inclusion...


Supposons que j'ai 3 fichiers : main.c et les fichiers a.h, et b.h. Supposons que a.h et b.h s'incluent mutuellement. Ces deux fichiers s'incluent mutuellement indéfiniment ! C'est là que le pré compilateur vient à notre secours, car on va pouvoir éviter que les #include ne se fassent indéfiniment en mettant dans a.h :
#ifndef A_H
#define A_H
#include "b.h"
/* Le contenu du header a.h */

#endif

et dans b.h :
#ifndef B_H
#define B_H
#include "a.h"
/* Le contenu du header b.h */

#endif

Que va-t'il se passer concrètement ? Le compilateur va commencer par un fichier (par exemple a.h). Comme à ce stade A_H n'est pas défini, il avance, puis il définit a.h et continue à lire le header a.h, en incluant b.h. À ce stade B_H n'est pas défini, donc de la même façon on rentre dans le header b.h et on active A_H.

On lit à présent le contenu de b.h qui veut inclure a.h. On rentre à nouveau dans a.h mais cette fois-ci, A_H est défini donc on ignore le contenu du header. On finit de lire le header b.h, ce qui résout le #include "b.h" que l'on était en train de traiter dans a.h. Puis on termine le header b.h.

Ce cas peut paraître tordu, mais il faut bien voir que lorsqu'un header est inclus plusieurs fois, des multidéfinitions peuvent apparaître (en particulier si des structures sont déclarées dans les headers). C'est pourquoi on met systématiquement ce mécanisme de verrou dans tous les headers.

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


Si un environnement de développement permet au travers d'un "nouveau projet" de gérer ton code de sorte à le compiler entièrement, tu te doutes qu'il doit compiler chaque fichier .c et assembler le tout. Lorsqu'on ne dispose pas d'environnement de développement, afin d'éviter de taper manuellement un "gcc" par fichier source, on fait un fichier "makefile" qui s'occupe de décrire comment construire l'exécutable.

En pratique, on est sensé en écrire un en plus de tes fichiers C/C++ afin que le code soit simple à compiler. En effet, si ce fichier est correctement écrit, il suffit de lancer le makefile pour construire intégralement le programme. Sous linux on tape alors simplement la commande :
make
Pour une lecture illimitée hors ligne, vous avez la possibilité de télécharger gratuitement cet article au format PDF :
La-compilation-et-les-modules-en-c-et-en-c.pdf

Réalisé sous la direction de , fondateur de CommentCaMarche.net.

A voir également

Dans la même catégorie

La compilación y los módulos en C y C++
Par Carlos-vialfa le 29 mai 2009
A compilação e os módulos em C e em C++
Par pintuda le 7 janvier 2012
Publié par mamiemando. - Dernière mise à jour par christelle.b
Ce document intitulé «  La compilation et les modules en C et en C++  » issu de CommentCaMarche (www.commentcamarche.net) est mis à disposition sous les termes de la licence Creative Commons. Vous pouvez copier, modifier des copies de cette page, dans les conditions fixées par la licence, tant que cette note apparaît clairement.