Programmation C++/Le préprocesseur
Avant de compiler le programme, il est possible d'effectuer certaines modifications sur le code source. Le programme effectuant ces modifications s'appelle le préprocesseur. Les commandes destinées au préprocesseur commencent toutes par # en début de ligne.
Inclusion de fichiers
modifierPour inclure un fichier à un certain endroit dans le fichier source, on écrit :
#include "nom_du_fichier"
Le contenu du fichier nom_du_fichier est alors inséré dans le fichier source.
Le nom du fichier peut être écrit entre guillemets "nom_du_fichier"
ou entre chevrons <nom_du_fichier>
. Dans le premier cas, cela signifie que le fichier se trouve dans le même dossier que le fichier source, tandis que dans le deuxième cas, il s'agit d'un fichier se situant dans un endroit différent (ce fichier pouvant être fournit par le compilateur ou une librairie externe par exemple).
Exemple :
#include <iostream>
Le fichier C++ standard iostream est inclus à cet endroit-là dans le code. Il contient la définition de certains objets standards notamment cin et cout.
#define, #undef
modifierLa directive #define
permet de remplacer toutes les occurrences d'un certain mot par un autre. Par exemple :
#define N 1143
Sur cet exemple toutes les occurrences de N seront remplacées par 1143
. Cela est parfois utilisé pour définir des constantes. On préférera toutefois utiliser le mot-clé const
.
On peut très bien ne pas fixer de valeur et écrire :
#define PLATEFORME_INTEL
La variable de compilation PLATEFORME_INTEL
est ici définie. Combiné à #ifdef
, on pourra compiler ou non certaines parties du code à certains endroits du programme.
De la même façon que l'on peut définir une variable, on peut arrêter une définition en utilisant #undef
. Son utilisation est rare, mais peut servir à ne plus définir une variable de compilation. Par exemple:
#undef PLATEFORME_INTEL
#ifdef, #ifndef, #if, #endif et #else
modifierPrésentation
modifierToutes ces directives permettent la compilation conditionnelle.
C'est-à-dire que la partie du code comprise entre la directive conditionnelle (#ifdef
, #ifndef
ou #if
) et la fin du bloc signalée par la directive #endif
n'est compilée que si la condition est remplie.
- La directive
#ifdef
permet de compiler toute une série de lignes du programme si une variable de compilation a précédemment été définie (par la directive#define
). La directive#endif
indique la fin de la partie de code conditionnelle. La partie du programme compilée sera toute la partie comprise entre le#ifdef
et le prochain#endif
.
- La directive
#ifndef
permet de compiler un bout de programme si une variable de compilation n'est pas définie. C'est donc l'inverse de#ifdef
. La fin de la partie à inclure est déterminée également par#endif
.
- La directive
#if
permet de tester qu'une expression est vraie. Cette expression ne peut utiliser que des constantes (éventuellement définies par une directive#define
), et la fonctiondefined
permettant de tester si une variable de compilation existe.
Il faut noter que les directives #ifdef et #ifndef, bien que très largement utilisées, sont considérées comme dépréciées ("deprecated"). On préférera donc la syntaxe :
#if defined(MA_VARIABLE)
à #ifdef MA_VARIABLE
et
#if !defined(MA_VARIABLE)
à #ifndef MA_VARIABLE
.
- Chacune de ces conditions peut être accompagné d'une directive
#else
qui permet d'inclure un bout de programme si la condition n'est pas vérifiée.
Il ne faut pas abuser de ces directives et elles sont surtout utilisées :
- pour gérer des problèmes de portabilité.
- au début des fichiers d'en-tête pour éviter une double compilation.
- dans les fichiers d'en-tête de DLL sous Windows.
Exemples
modifierExemple 1
modifier#include <iostream>
using namespace std;
#define FRENCH
int main()
{
#ifdef FRENCH
cout << "BONJOUR";
#else
cout << "HELLO";
#endif
return 0;
}
Dans ce programme, il suffit d'effacer #define FRENCH
et de recompiler le programme pour passer d'une version française à une version anglaise. Ceci pourrait être utile si le programme comporte 10 000 lignes (ce qui est faible pour un programme réel). Bien évidemment, il existe bien d'autres façons de gérer le multilinguisme en C++.
Exemple 2
modifierCet exemple montre l'utilisation d'expression dans les définitions de valeurs, et les précautions à prendre pour les expressions.
// Définir la taille d'un tableau contenant le prix :
// - de 5 variétés d'orange
#define N_ORANGES 5
// - de 3 variétés de pommes
#define N_POMMES 3
#define N_TOTAL_FRUITS N_ORANGES+N_POMMES
double prix_fruits[N_TOTAL_FRUITS];
#if N_TOTAL_FRUITS > 7
// ... plus de 7 variétés de fruits
#else
// ... moins de 7 ou égale à 7 variétés de fruits
#endif
cout << "Total de fruits : " << (N_TOTAL_FRUITS) << endl;
// 8
cout << "Double total de fruits : " << (N_TOTAL_FRUITS * 2) << endl;
// <!> 11 au lieu de 16 car l'opérateur * est prioritaire sur +
// dans l'expression étendue 5 + 3 * 2
// --> Utilisez des parenthèses :
// - autour du nom de la macro
cout << "Double total de fruits : " << ((N_TOTAL_FRUITS) * 2) << endl;
// 16
// - mieux : dans la définition
#define N_TOTAL_FRUITS_MIEUX (N_ORANGES+N_POMMES)
cout << "Double total de fruits : " << (N_TOTAL_FRUITS_MIEUX * 2) << endl;
// 16
Exemple 3
modifierFichier toto.h
#ifndef TOTO_H
#define TOTO_H
... écrire ici les prototypes ...
#endif
Le problème :
Imaginons qu'un fichier header toto.h contienne le prototype d'une certaine classe ou d'une fonction. Imaginons que le programme contiennent 3 autres fichiers headers nommés A.h, B.h et C.h qui ont tous les 3 besoin des prototypes inclus dans toto.h, ces 3 fichiers vont commencer par #include"toto.h". Imaginons également que C.h a besoin des prototypes inclus dans A.h et B.h. C.h va donc commence par #include"A.h" et #include"B.h". Le problème est que le fichier toto.h va être inclus plusieurs fois. Le compilateur va alors refuser de compiler le programme en indiquant que plusieurs prototypes d'une même fonction sont inclus.
Solution :
Pour résoudre l'inclusion multiple de fichier headers (inévitable), on va faire commencer le fichier header par #ifndef TOTO_H et il se termine par #endif. Si la variable de compilation TOTO_H n'est pas définie, alors le header sera inclus, sinon, il sera tout simplement vide. Juste après #ifndef TOTO_H, nous allons écrire #define TOTO_H définissant justement cette variable de compilation TOTO_H.
La première fois que le header sera inclus, TOTO_H n'est pas défini, le header normal sera donc inclus. #define TOTO_H définira alors la variable de compilation TOTO_H. La deuxième fois que ce même header sera inclus, et les fois suivantes, TOTO_H sera défini et par conséquent, le header sera vide. les prototypes n'auront donc été inclus qu'une seule fois. Le tour est joué. Il faut donc faire commencer systèmatiquement (c'est tout du moins conseillé) tous les fichiers header par les 2 lignes #ifndef ... et #define ... et les faire se terminer par #endif.
Autre solution
modifierToutefois, il existe une autre solution au problème précédent en utilisant la directive suivante en début de fichier .h
:
#pragma once
Cette directive indique au compilateur d'ignorer le fichier s'il a déjà été "visité", et ne fonctionne qu'avec certains compilateurs :
- Visual C++
- CodeWarrior
- GCC for Darwin
Les macros
modifierPrésentation
modifierLes macros sont des #define
particulier parce qu'ils contiennent des paramètres. Ainsi si vous écrivez :
#define AFFICHE(x) cout << x << endl;
Alors vous pouvez écrire AFFICHE("BONJOUR")
et le préprocesseur modifiera cette ligne et la transformera en cout << "BONJOUR" << endl;
. Il y aura substitution de x
par "BONJOUR"
. Il ne faut pas abuser des macros et très souvent l'utilisation de fonctions, notamment les fonctions inline, est préférable.
Exemple
modifier#include <iostream>
using namespace std;
#define AFFICHER(x) cout << x << endl;
int main()
{
AFFICHER("BONJOUR");
return 0;
}
Bonnes pratiques
modifierAfin d'utiliser correctement les macros, il est préférable de les afficher clairement et de les rendre suffisamment flexible à différentes utilisations.
Si la macros est constituée de plusieurs instructions séparés par des ;
, il est préférable d'écrire la macro sur plusieurs lignes afin d'accroître sa lisibilité. Pour indiquer à une macro que sa définition continue sur la ligne suivante, il suffit d'indiquer un antislash ('\') en dernier caractère le la ligne.
#define AFFICHER(x) \
cout << x; \
cout << endl;
L'utilisation la plus courante des macros est de ne pas mettre de ;
à la fin de celle-ci, mais de le mettre dans le code, là où elle est utilisée. En effet, on peut prévoir qu'une macro soit utilisable en tant qu'instruction simple, ou en tant que condition ou paramètre de fonction où l'on ne doit pas mettre de ;
. Pour les macros qui ne retournent rien (comme la macro AFFICHER dans l'exemple précédent), le placement du ;
n'est pas un problème car elles ne retournent rien et ne seront jamais utilisées dans une condition ou un appel de fonction.
#include <iostream>
using namespace std;
#define DIVISER(x, y) ((x) / (y))
int main()
{
int valeur = DIVISER(5, 3);
if (DIVISER(8, 2) == 4)
cout << DIVISER(1.0, 5.0) << endl;
return 0;
}
Il est fortement conseillé de toujours utiliser un paramètre de macro avec des parenthèses autour. Si l'on reprend l'exemple précédent sans parenthèses :
#include <iostream>
using namespace std;
#define DIVISER(x, y) x / y
int main()
{
int valeur = DIVISER(5, 3);
if (DIVISER(4 + 4, 2) == 4)
cout << DIVISER(1.0, 5.0) << endl;
return 0;
}
Ici, le résultat obtenu n'est pas forcement celui désiré. DIVISER(4 + 4, 2)
sera traduit après la précompilation par 4 + 4 / 2
. Ceci donne pour valeur 4 + 2, soit 6. Ajouter un maximum de parenthèses permet de s'assurer de la validité de la macro sous plusieurs utilisations différentes. Ainsi, dans l'exemple précédent, une utilisation de parenthèses dans la macro (#define DIVISER(x, y) ((x) / (y))
), aurait traduit DIVISER(4 + 4, 2)
en ((4 + 4) / (2))
. Ceci aurait donné comme valeur 8 / 2 = 4, la valeur attendue.