5. Le préprocesseur C▲
5.1. Définition▲
Le préprocesseur est un programme qui analyse un fichier texte et qui lui fait subir certaines transformations. Ces transformations peuvent être l'inclusion d'un fichier, la suppression d'une zone de texte ou le remplacement d'une zone de texte.
Le préprocesseur effectue ces opérations en suivant des ordres qu'il lit dans le fichier en cours d'analyse.
Il est appelé automatiquement par le compilateur, avant la compilation, pour traiter les fichiers à compiler.
5.2. Les commandes du préprocesseur▲
Toutes les commandes du préprocesseur commencent :
- en début de ligne ;
- par un signe dièse (#).
Les commandes sont les suivantes :
5.2.1. Inclusion de fichier▲
L'inclusion de fichier permet de factoriser du texte commun à plusieurs autres fichiers (par exemple des déclarations de type, de constante, de fonction, etc.). Le texte commun est mis en général dans un fichier portant l'extension .h (pour « header », fichier d'en-tête de programme).
#include
"fichier"
ou :
#include
<fichier>
fichier est le nom du fichier à inclure. Lorsque son nom est entre guillemets, le fichier spécifié est recherché dans le répertoire courant (normalement le répertoire du programme). S'il est encadré de crochets, il est recherché d'abord dans les répertoires spécifiés en ligne de commande avec l'option -I, puis dans les répertoires du chemin de recherche des en-têtes du système (ces règles ne sont pas fixes, elles ne sont pas normalisées).
Le fichier inclus est traité lui aussi par le préprocesseur.
La signification de la ligne #include <stdio.h> au début de tous les programmes utilisant les fonctions scanf et printf devient alors claire. Si vous ouvrez le fichier stdio.h, vous y verrez la déclaration de toutes les fonctions et de tous les types de la bibliothèque d'entrée - sortie standard. De même, les fonctions malloc et free sont déclarées dans le fichier d'en-tête stdlib.h et définies dans la bibliothèque standard. L'inclusion de ces fichiers permet donc de déclarer ces fonctions afin de les utiliser.
5.2.2. Constantes de compilation et remplacement de texte▲
Le préprocesseur permet de définir des identificateurs qui, utilisés dans le programme, seront remplacés textuellement par leur valeur. La définition de ces identificateurs suit la syntaxe suivante :
#define identificateur texte
où identificateur est l'identificateur qui sera utilisé dans la suite du programme, et texte sera le texte de remplacement que le préprocesseur utilisera. Le texte de remplacement est facultatif (dans ce cas, c'est le texte vide). À chaque fois que l'identificateur identificateur sera rencontré par le préprocesseur, il sera remplacé par le texte texte dans toute la suite du programme.
Cette commande est couramment utilisée pour définir des constantes de compilation, c'est-à-dire des constantes qui décrivent les paramètres de la plate-forme pour laquelle le programme est compilé. Ces constantes permettent de réaliser des compilations conditionnelles, c'est-à-dire de modifier le comportement du programme en fonction de paramètres définis lors de sa compilation. Elle est également utilisée pour remplacer des identificateurs du programme par d'autres identificateurs, par exemple afin de tester plusieurs versions d'une même fonction sans modifier tout le programme.
#define UNIX_SOURCE
#define POSIX_VERSION 1001
Dans cet exemple, l'identificateur UNIX_SOURCE sera défini dans toute la suite du programme, et la constante de compilation POSIX_VERSION sera remplacée par 1001 partout où elle apparaîtra.
Note : On fera une distinction bien nette entre les constantes de compilation définies avec la directive #define du préprocesseur et les constantes définies avec le mot clé const. En effet, les constantes littérales ne réservent pas de mémoire. Ce sont des valeurs immédiates, définies par le compilateur. En revanche, les variables de classe de stockage const peuvent malgré tout avoir une place mémoire réservée. Ce peut par exemple être le cas si l'on manipule leur adresse ou s'il ne s'agit pas de vraies constantes, par exemple si elles peuvent être modifiées par l'environnement (dans ce cas, elles doivent être déclarées avec la classe de stockage volatile). Ce sont donc plus des variables accessibles en lecture seule que des constantes. On ne pourra jamais supposer qu'une variable ne change pas de valeur sous prétexte qu'elle a la classe de stockage const, alors qu'évidemment, une constante littérale déclarée avec la directive #define du préprocesseur conservera toujours sa valeur (pourvu qu'on ne la redéfinisse pas). Par ailleurs, les constantes littérales n'ont pas de type, ce qui peut être très gênant et source d'erreur. On réservera donc leur emploi uniquement pour les constantes de compilation, et on préférera le mot clé const pour toutes les autres constantes du programme.
Le préprocesseur définit un certain nombre de constantes de compilation automatiquement. Ce sont les suivantes :
- __LINE__ : donne le numéro de la ligne courante ;
- __FILE__ : donne le nom du fichier courant ;
- __DATE__ : renvoie la date du traitement du fichier par le préprocesseur ;
- __TIME__ : renvoie l'heure du traitement du fichier par le préprocesseur ;
- __cplusplus : définie uniquement dans le cas d'une compilation C++. Sa valeur doit être 199711L pour les compilateurs compatibles avec le projet de norme du 2 décembre 1996. En pratique, sa valeur est dépendante de l'implémentation utilisée, mais on pourra utiliser cette chaîne de remplacement pour distinguer les parties de code écrites en C++ de celles écrites en C.
Note : Si __FILE__, __DATE__, __TIME__ et __cplusplus sont bien des constantes pour un fichier donné, ce n'est pas le cas de __LINE__. En effet, cette dernière « constante » change bien évidemment de valeur à chaque ligne. On peut considérer qu'elle est redéfinie automatiquement par le préprocesseur à chaque début de ligne.
5.2.3. Compilation conditionnelle▲
La définition des identificateurs et des constantes de compilation est très utilisée pour effectuer ce que l'on appelle la compilation conditionnelle. La compilation conditionnelle consiste à remplacer certaines portions de code source par d'autres, en fonction de la présence ou de la valeur de constantes de compilation. Cela est réalisable à l'aide des directives de compilation conditionnelle, dont la plus courante est sans doute #ifdef :
#ifdef identificateur
&
vellip;
#endif
Dans l'exemple précédent, le texte compris entre le #ifdef (c'est-à-dire « if defined ») et le #endif est laissé tel quel si l'identificateur identificateur est connu du préprocesseur. Sinon, il est supprimé. L'identificateur peut être déclaré en utilisant simplement la commande #define vue précédemment.
Il existe d'autres directives de compilation conditionnelle :
#ifndef (if not defined ...)
#elif (sinon, si ... )
#if (si ... )
La directive #if attend en paramètre une expression constante. Le texte qui la suit est inclus dans le fichier si et seulement si cette expression est non nulle. Par exemple :
#if (__cplusplus==199711L)
&
vellip;
#endif
permet d'inclure un morceau de code C++ strictement conforme à la norme décrite dans le projet de norme du 2 décembre 1996.
Une autre application courante des directives de compilation est la protection des fichiers d'en-tête contre les inclusions multiples :
#ifndef DejaLa
#define DejaLa
Texte à n'inclure qu'une seule fois au plus.
#endif
Cela permet d'éviter que le texte soit inclus plusieurs fois, à la suite de plusieurs appels de #include. En effet, au premier appel, DejaLa n'est pas connu du préprocesseur. Il est donc déclaré et le texte est inclus. Lors de tout autre appel ultérieur, DejaLa existe, et le texte n'est pas inclus. Ce genre d'écriture se rencontre dans les fichiers d'en-tête, pour lesquels en général on ne veut pas qu'une inclusion multiple ait lieu.
5.2.4. Autres commandes▲
Le préprocesseur est capable d'effectuer d'autres actions que l'inclusion et la suppression de texte.
Les directives qui permettent d'effectuer ces actions sont indiquées ci-dessous :
- # : ne fait rien (directive nulle) ;
- #error message : permet de stopper la compilation en affichant le message d'erreur donné en paramètre ;
- #line numéro [fichier] : permet de changer le numéro de ligne courant et le nom du fichier courant lors de la compilation ;
- #pragma texte : permet de donner des ordres spécifiques à une l'implémentation du compilateur tout en conservant la portabilité du programme. Toute implémentation qui ne reconnaît pas un ordre donné dans une directive #pragma doit l'ignorer pour éviter des messages d'erreurs. Le format des ordres que l'on peut spécifier à l'aide de la directive #pragma n'est pas normalisé et dépend de chaque compilateur.
5.3. Les macros▲
Le préprocesseur peut, lors du mécanisme de remplacement de texte, utiliser des paramètres fournis à l'identificateur à remplacer. Ces paramètres sont alors replacés sans modification dans le texte de remplacement. Le texte de remplacement est alors appelé macro.
La syntaxe des macros est la suivante :
#define macro(paramètre[, paramètre [...]]) définition
#define MAX(x,y) ((x)>(y)?(x):(y))
#define MIN(x,y) ((x)<(y)?(x):(y))
Note : Pour poursuivre une définition sur la ligne suivante, terminez la ligne courante par le signe '\'.
Le mécanisme des macros permet de faire l'équivalent de fonctions générales, qui fonctionnent pour tous les types. Ainsi, la macro MAX renvoie le maximum de ses deux paramètres, qu'ils soient entiers, longs ou réels. Cependant, on prendra garde au fait que les paramètres passés à une macro sont évalués par celle-ci à chaque fois qu'ils sont utilisés dans la définition de la macro. Cela peut poser des problèmes de performances ou, pire, provoquer des effets de bords indésirables. Par exemple, l'utilisation suivante de la macro MIN :
MIN(f(3
), 5
)
provoque le remplacement suivant :
((f(3
))<
(5
))?(f(3
)):(5
))
soit deux appels de la fonction f si f(3) est inférieur à 5, et un seul appel sinon. Si la fonction f ainsi appelée modifie des variables globales, le résultat de la macro ne sera certainement pas celui attendu, puisque le nombre d'appels est variable pour une même expression. On évitera donc, autant que faire se peut, d'utiliser des expressions ayant des effets de bords en paramètres d'une macro. Les écritures du type :
MIN(++
i, j)
sont donc à prohiber.
On mettra toujours des parenthèses autour des paramètres de la macro. En effet, ces paramètres peuvent être des expressions composées, qui doivent être calculées complètement avant d'être utilisées dans la macro. Les parenthèses forcent ce calcul. Si on ne les met pas, les règles de priorités peuvent générer une erreur de logique dans la macro elle-même. De même, on entourera de parenthèses les macros renvoyant une valeur, afin de forcer leur évaluation complète avant toute utilisation dans une autre expression. Par exemple :
#define mul(x,y) x*y
est une macro fausse. La ligne :
mul(2
+
3
,5
+
9
)
sera remplacée par :
2+3*5+9
ce qui vaut 26, et non pas 70 comme on l'aurait attendu. La bonne macro est :
#define mul(x,y) ((x)*(y))
car elle donne le texte suivant :
((2+3)*(5+9))
et le résultat est correct. De même, la macro :
#define add(x,y) (x)+(y)
est fausse, car l'expression suivante :
add(2
,3
)*
5
est remplacée textuellement par :
(2)+(3)*5
dont le résultat est 17 et non 25 comme on l'aurait espéré. Cette macro doit donc se déclarer comme suit :
#define add(x,y) ((x)+(y))
Ainsi, les parenthèses assurent un comportement cohérent de la macro. Comme on le voit, les parenthèses peuvent alourdir les définitions des macros, mais elles sont absolument nécessaires.
Le résultat du remplacement d'une macro par sa définition est, lui aussi, soumis au préprocesseur. Par conséquent, une macro peut utiliser une autre macro ou une constante définie avec #define. Cependant, ce mécanisme est limité aux macros qui n'ont pas encore été remplacées afin d'éviter une récursion infinie du préprocesseur. Par exemple :
#define toto(x) toto((x)+1)
définit la macro toto. Si plus loin on utilise « toto(3) », le texte de remplacement final sera « toto((3)+1) » et non pas l'expression infinie « (...(((3)+1)+1...)+1 ».
Le préprocesseur définit automatiquement la macro defined, qui permet de tester si un identificateur est connu du préprocesseur. Sa syntaxe est la suivante :
defined(identificateur)
La valeur de cette macro est 1 si l'identificateur existe, 0 sinon. Elle est utilisée principalement avec la directive #if. Il est donc équivalent d'écrire :
#if defined(identificateur)
&
vellip;
#endif
et :
#ifdef identificateur
&
vellip;
#endif
Cependant, defined permet l'écriture d'expressions plus complexes que la directive #if.
5.4. Manipulation de chaînes de caractères dans les macros▲
Le préprocesseur permet d'effectuer des opérations sur les chaînes de caractères. Tout argument de macro peut être transformé en chaîne de caractères dans la définition de la macro s'il est précédé du signe #. Par exemple, la macro suivante :
#define CHAINE(s) #s
transforme son argument en chaîne de caractères. Par exemple :
CHAINE(2
+
3
)
devient :
"2+3"
Lors de la transformation de l'argument, toute occurrence des caractères " et \ est transformée respectivement en \" et \\ pour conserver ces caractères dans la chaîne de caractères de remplacement.
Le préprocesseur permet également la concaténation de texte grâce à l'opérateur ##. Les arguments de la macro qui sont séparés par cet opérateur sont concaténés (sans être transformés en chaînes de caractères cependant). Par exemple, la macro suivante :
#define NOMBRE(chiffre1,chiffre2) chiffre1##chiffre2
permet de construire un nombre à deux chiffres :
NOMBRE(2
,3
)
est remplacé par le nombre décimal 23. Le résultat de la concaténation est ensuite analysé pour d'éventuels remplacements additionnels par le préprocesseur.
5.5. Les trigraphes▲
Le jeu de caractères utilisé par le langage C++ comprend toutes les lettres en majuscules et en minuscules, tous les chiffres et les caractères suivants :
. , ; : ! ? " ' + - ^ * % = & | ~ _ # / \ { } [ ] () < >
Malheureusement, certains environnements sont incapables de gérer quelques-uns de ces caractères. C'est pour résoudre ce problème que les trigraphes ont été créés.
Les trigraphes sont des séquences de trois caractères commençant par deux points d'interrogations. Ils permettent de remplacer les caractères qui ne sont pas accessibles sur tous les environnements. Vous n'utiliserez donc sans doute jamais les trigraphes, à moins d'y être forcé. Les trigraphes disponibles sont définis ci-dessous :
Tableau 5-1. Trigraphes
Trigraphe |
Caractère de remplacement |
---|---|
??= |
# |
??/ |
\ |
??' |
^ |
??( |
^ |
??) |
[ |
??! |
] |
{ |
} |
??- |
~ |