Pointeurs, références & allocation dynamique Rappels sur les fonctions Les spécificités du C++ Structures et classes Encapsulation des données Notions de constructeur et de destructeur Fonctions et classes amies Surcharge d'opérateur Héritage Notions de patrons de fonctions et de classes Introduction à la librairie standard STL
Compilation et directives de préprocesseur Convention d'écriture et organisation des programmes Écriture/lecture sur l'entrée/sortie standard Les membres données statiques Utilisation de enum et de typedef
Retour menu principal

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 (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.

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 :

1

man, pour manual, constitue l’instruction unix permettant de connaître les différents modes de fonctionnement d’une commande donnée

2

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.