Compilation et directives de préprocesseur
Description détaillée du processus de compilation
Le processus de compilation se décompose en différentes phases conduisant à la construction d’un fichier binaire exécutable. Ces phases ne sont, en général, pas spécifiques au C++ et quand bien même les différents outils de programmation peuvent les cacher, le processus de génération des exécutables se déroule toujours selon les principes suivant.
La première étape appelée précompilation, consiste à “traiter” les fichiers
sources (fichiers d’en-tête: *.h
ou *.hh
et fichiers contenant le code à
proprement dit: *.cc *.cpp
) avant compilation. Dans le cas du C et du C++, il
s’agit des opérations effectuées par le préprocesseur (remplacement de macros,
suppression de texte, inclusion de fichiers…). Le résultat de la
précompilation peut s’obtenir via la commande suivante
g++ -E fichier_source.cc
À la précompilation succède la compilation séparée qui est le fait de compiler
séparément les fichiers sources. Le résultat de la compilation d’un fichier
source est généralement un fichier en assembleur, soit le langage décrivant les
instructions du microprocesseur de la machine cible pour laquelle le programme
est destiné. Cette étape conduit à la création de fichiers objets (d’extension
.o
) contenant la traduction du code assembleur en langage machine. Les données
initialisées, par exemple, sont également comprises dans les fichiers
objets. Pour ce qui concerne le C++, la commande nécessaire à la compilation
séparée s’écrit :
g++ -c fichier_source.cc
Cette opération créera, par conséquent, un fichier objet nommé
fichier_source.o
. L’ensemble des fichiers objets relatifs à un projet ou code
donné peuvent être regroupés en une librairie (statique ou dynamique) afin de
rassembler dans un même fichier les fonctionnalités développées.
L’étape finale du processus de compilation consiste à regrouper la totalité des données de même que les fichiers objets et les bibliothèques (fonctions de la bibliothèque standard et des autres bibliothèques externes) ainsi qu’à résoudre les références inter-fichiers. Cette étape est appelée édition de liens (linking en anglais). Le résultat de l’édition de liens est un fichier image qui pourra être chargé en mémoire par le système d’exploitation. Les fichiers exécutables et les bibliothèques dynamiques sont des exemples de fichiers images. La commande relative à ce mécanisme est la suivante:
g++ fichier_source1.o fichier_source2.o … -o fichier_executable
Comme nous le soulignions en introduction, certains compilateurs peuvent réaliser l’ensemble de ces étapes en une seule opération. Le compilateur C++ peut ainsi compiler séparément l’ensemble des fichiers sources et réaliser l’édition de liens via la commande
g++ fichier_source1.cc fichier_source2.cc … -o fichier_executable
Options de compilation
Le compilateur C++ autorise l’utilisation d’une multitude d’options concernant
aussi bien l’optimisation du processus de compilation que la compréhension et la
résolution des éventuels conflits. L’ensemble des options disponibles pour la
commande g++
peut être obtenu à l’aide de l’instruction man g++
1. Parmi
les multiples options disponibles2, la liste ci-dessous recense les plus
couramment utilisées:
- l’option
-Ichemin
indique au compilateur où sont localisés les fichiers d’entêtes nécessaires à la compilation séparée du programme. Par défaut, le compilateur recherche ces fichiers localement et dans le répertoire/usr/include
, - l’option
-Lchemin
est essentiel lors du processus d’édition de liens afin d’indiquer au compilateur où se situent les librairies externes. Soit le chemin d’accès est complet et pointe donc vers la librairie concernée, soit le chemin d’accès indique uniquement le répertoire où sont localisées les librairies, auquel cas il est nécessaire de préciser le nom de la librairie à utiliser via l’option-lnom_librairie
, - l’option
-W
permet l’activation des messages de mise en garde (leW
se référant à l’expression warning). Ces warnings n’empècheront pas l’exécution du programme final mais fournissent des indications voir des conseils en relation avec certaines parties du code. Différents niveaux de mise en garde sont accessibles, le plus complet étant-Wall -Wextra
, - l’option
-Didentificateur
permet la définition d’identificateur à fournir lors du processus de précompilation. L’activation des directives de préprocesseur peut se faire par ce biais ce qui permettra au compilateur d’insérer, de supprimer ou de modifier les portions de code relatives à cette directive. À titre d’exemple, il pourra s’avérer utile lors d’une phase de débogage d’un programme, de définir un identificateur__DEBUG__
qui affichera des informations complémentaires lors de l’exécution du programme. Afin d’inclure ces instructions supplémentaires au sein du code, la commande de compilation deviendrag++ -D__DEBUG__ ...
, - afin de profiter des processeurs multi-cœurs présents dans les machines
récentes, l’option
-On
permet l’optimisation de la compilation en répartissant la charge de compilation surn
cœurs.
Directives de préprocesseur
Comme nous l’avons rappelé en introduction à cette annexe, la compilation d’un
programme se déroule en trois phases. La première est exécutée par le
préprocesseur et vise à remplacer les directives de compilation par des
instructions C++. Dans les faits, le préprocesseur recherche des directives de
compilation repérées en début de ligne par le symbole #
et se terminant avec
la fin de la ligne. La syntaxe est ainsi
#directive [paramètres]
Il est possible de placer des espaces blancs avant et après la directive mais, contrairement au compilateur, les sauts de lignes et les commentaires ne sont pas considérés comme des blancs par le préprocesseur. Par conséquent, on ne doit pas couper une ligne de directive, ni y placer un commentaire qui pourrait entrer en conflit avec la directive. Notons qu’il ne faut pas de point virgule en fin de ligne.
Si la directive ne tient pas sur une seule ligne, il suffit d’écrire le
caractère \
juste avant le saut de ligne; dans ce cas, la ligne courante est
considérée comme la suite de la précédente. Rappelons que ceci est également
vrai pour le compilateur qui ignore les paires \
+ saut de ligne.
Parmi les différents types de directives de préprocesseur, la plus fréquemment
utilisée demeure la directive d’inclusion #include
. Elle indique au
préprocesseur de remplacer la ligne courante par l’ensemble des lignes du
fichier donné en paramètre. En pratique, cette directive est essentiellement
utilisée pour inclure les fichiers d’entête de librairies (fichiers
*.h
ou *.hh
).
La directive #define identificateur
permet de définir un paramètre
“identificateur” qui pourra être utilisé dans une clause conditionnelle (#if
,
#ifdef
, #ifndef
voir ci-après). L’identificateur ne doit pas commencer par
un chiffre.
Par ailleurs, il est possible de contrôler ce qui sera compilé effectivement ou non, avec les clauses de condition. Si l’on écrit
#if condition ... #endif
la condition, qui doit être une constante numérique au format C++, est évaluée
par le préprocesseur; si la condition est non nulle, la clause #if
est ignorée
et le code compris dans la séquence #if
– #endif
est considéré par le
compilateur. En revanche, si la condition est nulle, tout ce qui se trouve entre
les directives #if
et #endif
est ignoré et donc non compilé. La clause #if
peut avoir une clause #else
voir #elif
(pour else if
). Dans le cas
d’identificateur tel que défini via la directive #define
, on utilisera
l’écriture suivante
#ifdef identificateur ... #endif
ou sa négation
#ifndef identificateur ... #endif
Enfin, les clauses de compilation conditionnelles peuvent être imbriquées.
La directive #define
sert également à la définition des macros. Il s’agit
d’abréviations ou de noms symboliques, traditionnellement en majuscules, se
référant à d’autres objets tels qu’une constante numérique ou une chaîne de
caractères. Les macros suivantes constituent des exemples classiques utilisés
notamment en C:
#define PI 3.141592 #define ERRMSG "Une erreur s'est produite.\n" #define CARRE(x) ((x) * (x))
Le préprocesseur examine chaque ligne de code à la recherche du nom des macros
préalablement définies; si l’un des noms est rencontré, son expression est
remplacée par sa valeur. Si la macro a plusieurs paramètres telle que CARRE
dans l’exemple ci-dessus, chacun des paramètres est remplacé littéralement par
sa valeur effective. Ainsi, l’écriture suivante
if (CARRE(d) > PI)
printf(ERRMSG);
deviendra après précompilation
if (((d) * (d)) > 3.141592) printf("Une erreur s'est produite.\n");
Les occurences de x
ont été remplacées littéralement par d
.
Une macro a donc une syntaxe similaire à celle d’une fonction bien que leurs
traitements par le compilateur obéissent à des mécanismes sensiblement
différents : les macros résultent de la précompilation alors que les fonctions
sont compilées et donc soumises aux règles du C++. En ce sens, les macros sont
la source de nombreuses erreurs d’autant plus difficiles à repérer qu’on ne
dispose pas de la version étendue du code. À titre d’exemple, l’utilisation de
la macro CARRE
suivant les instructions
int i = 3; int j = CARRE(i++);
provoquera la double incrémentation de la variable i
en raison du remplacement
de l’instruction CARRE(i++)
par l’expression ((i++) * (i++))
(et non une
simple incrémentation comme visiblement suggéré par le code). Aussi, les macros
sont généralement bannies du C++ et remplacées au profit de déclarations ne
faisant pas intervenir le précompilateur.
Ainsi, en C++, les macros qui définissent des constantes seront avantageusement remplacées par des déclarations de la forme
const double Pi = 3.141592; const std::string ErrMsg = "Une erreur s'est produite";
De même, les macros telles que CARRE
c’est-à-dire définissant de simples
actions deviendront des fonctions en ligne (cf. "Spécificités du C++")
s’écrivant
inline int Carre(const int i) { return i*i; } inline double Carre(const double d) { return d*d; }
qui vérifient, en outre, les types de leurs paramètres.
Notes :
man
, pour manual, constitue l’instruction
unix permettant de connaître les différents modes de fonctionnement
d’une commande donnée
pour plus d’informations, le site internet
http://www.antoarts.com/the-most-useful-gcc-options-and-extensions/ propose une
liste des options de g++
les plus utiles.