Compilation et directives de préprocesseur

publicité
Fiche
1
Compilation et directives de
préprocesseur
1.1
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’entê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.cpp
À 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.cpp
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.cpp fichier_source2.cpp … -o fichier_executable
1.2
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++a .
Parmi les multiples options disponiblesb , 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 (le W 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 deviendra g++ -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 sur n cœurs.
1.3
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]
a man, pour manual, constitue l’instruction unix permettant de connaître les différents modes de fonctionnement
d’une commande donnée
b 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.
2
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).
Lorsque l’activation des directives de préprocesseur se fait au moyen de l’option de compilation
-Didentificateur (cf plus haut), on aura une syntaxe telle que l’illustre l’exemple suivant
• test_debug.cpp
#include <iostream>
using namespace std;
int main()
{
#if (DEBUG == 1)
cout << "DEBUG: "
<< "Mode debug du programme" << endl;
#else
cout << "NOTICE: "
<< "Mode normal du programme" << endl;
#endif
}
• compilation :
$ g++ -DDEBUG=1 test_debug.cpp -o test_debug
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
3
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";
4
Téléchargement