Préprocesseur

Écrit le 27/11/2004 par Wikibooks
Dernière mise à jour : 02/02/2006

Le préprocesseur est un langage de macro qui est analysé, comme son nom l'indique, avant la compilation. En fait, c'est un langage complètement indépendant, il est même théoriquement possible de l'utiliser par dessus un autre langage que le C. Cette indépendance fait que le préprocesseur ignore totalement la structure de votre programme, les directives seront toujours évaluées de haut en bas.

Ces directives commencent toutes par le symbole dièse ('#'), suivi d'un nombre quelconque de blanc (espace ou tabulation), suivi du nom de la directive en minuscule. Les directives doivent être déclarées sur une ligne dédiée. Les noms standards de directives sont :

Déclarations de constantes

#define CONSTANTE valeur

Permet de substituer dans un code source C la suite de caractères « CONSTANTE » par la valeur (à l'exception des caractères et des chaines). Par exemple :

#define TAILLE 100

/* La substitution se fera sur la deuxième occurence de TAILLE, mais pas la première */
printf("La constante TAILLE vaut %d\n", TAILLE);

remplacera les occurences de « TAILLE » par « 100 ». Historiquement, les programmeurs avaient pour habitude d'utiliser des majuscules pour distinguer les déclarations du préprocesseur et les minuscules pour les noms de symboles (fonctions, variables, champs, etc.) du compilateur. Ce n'est pas une règle à suivre impérativement, mais elle améliore la lisibilité des programmes.

Il est possible de définir plusieurs fois la même « CONSTANTE ». Le compilateur n'émettera un avertissement que si les deux valeurs ne concordent pas.

Une définition de constantes peut s'étendre sur plusieurs lignes. Pour cela, il faut que le dernier caractère de la ligne soit une barre oblique inverse ('\').

Déclarations automatiques

Le langage C impose que le compilateur définisse un certain nombre de constantes. Sans énumérer toutes celles spécifiques à chaque compilateur, on peut néanmoins compter sur :

Déclaration de macros

Une macro est en fait une constante qui peut prendre un certain nombre d'arguments.
Les arguments sont placés entre parenthèses après le nom de la macro sans espaces, par exemple :

#define MAX(x,y)    x > y ? x : y
#define SWAP(x,y)   x ^= y, y ^= x, x ^= y

La première macro prend deux arguments et « retourne » le maximum entre les deux. La deuxième est plus subtile, elle échange la valeur des deux arguments (qui doivent être des variables entières), sans passer par une variable temporaire, et ce avec le même nombre d'opérations.

La macro va en fait remplacer toutes les occurences de la chaîne « MAX » et de ses arguments par « x > y ? x : y ». Ainsi, si on appelle la macro de cette façon :

printf("%d\n", MAX(4,6));

Elle sera remplacée par :

printf("%d\n", 4 > 6 ? 4 : 6);

Il faut bien comprendre qu'il ne s'agit que d'une substitution de texte, qui ne tient pas compte de la structure du programme. Considérez l'exemple suivant, qui illustre une erreur très classique dans l'utilisation des macros :

#define MAX(x,y) x > y ? x : y

i = 2 * MAX(4,6); /* Sera remplacé par : i = 2 * 4 > 6 ? 4 : 6; */

L'effet n'est pas du tout ce à quoi on s'attendait. Il est donc important de bien parenthéser les expressions, justement pour éviter ce genre d'effet de bord. Il aurait mieux fallu écrire la macro MAX de la manière suivante :

#define MAX(x,y) ((x) > (y) ? (x) : (y))

En fait dès qu'une macro est composée d'autre chose qu'un élément atomique (un lexème, ou un token) il est bon de le mettre entre parenthèses, notamment les arguments qui peuvent être des expressions utilisant des opérateurs ayant des priorités arbitraires.

Suppression d'une définition

Il arrive qu'une macro/constante soit déjà définie, mais qu'on aimerait quand même utiliser ce nom avec une autre valeur. Pour éviter un avertissement du préprocesseur, on doit d'abord supprimer l'ancienne définition, puis déclarer la nouvelle :

#undef symbole
#define symbole       nouvelle_valeur

Cette directive supprime le symbole spécifié. À noter que même pour les macros avec arguments, il suffit juste de spécifier le nom de cette macro.

Transformation en chaîne de caractères

Le préprocesseur permet de transformer une expression en chaîne de caractères. Cette technique ne fonctionne donc qu'avec des macros ayant au moins un argument. Pour transformer n'importe quel argument de la macro en chaîne de caractères, il suffit de préfixer le nom de l'argument par le caractère dièse ('#'). Cela peut être utile pour afficher des messages de diagnostique très précis, comme dans l'exemple suivant :

#define assert(condition)     \
if( (condition) == 0 )        \
{                             \
    puts( "La condition '" #condition "' a échoué" ); \
    exit( 1 );            \
}

À noter qu'il n'existe pas de mécanisme simple pour transformer la valeur de la macro en chaîne de caractères. C'est le cas classique des constantes numériques qu'on aimerait souvent transformer en chaîne de caractères : le préprocesseur C n'offre malheureusement rien de vraiment pratique pour effectuer ce genre d'opération.

Concaténation d'arguments

Il s'agit d'une facilité d'écriture pour simplifier les tâches répétitives. En utilisant l'opérateur ##, on peut concaténer deux expressions :

#define version(symbole)    symbole ## _v123

int version(variable);   /* Déclare "int variable_v123;" */

Déclaration de macros à nombre variable d'arguments

Ceci est une extension ISO C99. La déclaration d'une macro à nombre variable d'arguments est en fait identique à une fonction, sauf qu'avec une macro on ne pourra pas traiter les arguments supplémentaires un à un. Ces paramètres sont en fait traités comme un tout, via le symbole __VA_ARGS__. Exemple :

#define debug(message, ...)   fprintf( stderr, __FILE__ ":%d:" message "\n", __LINE__, __VA_ARGS__ )

Il y a une restriction qui ne saute pas vraiment aux yeux dans cet exemple, c'est que les points de suspension doivent obligatoirement être remplacés par au moins un argument, ce qui n'est pas toujours très pratique. Malheureusement le langage C n'offre aucun moyen pour contourner ce problème pourtant assez répandu.

À noter, une extension du compilateur gcc, qui permet de s'affranchir de cette limitation en rajoutant l'opérateur ## à __VA_ARGS__ :

/* Extension de gcc pour utiliser la macro sans argument */
#define debug(message, ...)   fprintf( stderr, __FILE__ ":%d:" message "\n", __LINE__, ##__VA_ARGS__ )

Tests

Les tests permettent d'effectuer de la compilation conditionnelle. La directive #ifdef permet de savoir si une constante ou une macro est définie. Chaque déclaration #ifdef doit obligatoirement se terminer par un #endif, avec éventuellement une clause #else entre. Un petit exemple classique :

#ifdef DEBUG
/* S'utilise : debug( ("Variable x = %d\n", x) ); (double parenthésage) */
#define debug(x)  printf x
#else
#define debug(x)
#endif

L'argument de la directive #ifdef doit obligatoirement être un symbole du préprocesseur. Pour utiliser des expressions un peu plus complexe, il y a la directive #if (et #elif, contraction de else if). Cette directive utilise des expressions semblable à l'instruction if() du compilateur : si l'expression évaluée est différente de zéro, elle sera considéré comme vraie, et faux si l'expression s'évalue à 0.

On peut utiliser l'addition, la soustraction, la multiplication, la division, les opérateurs binaires (&, |, ^, ~, <<, >>), les comparaisons et les connecteurs logiques (&& et ||). Ces derniers sont évalués en circuit court comme leur équivalent C. Les opérandes possibles sont les nombres entiers et les caractères, ainsi que les macros elle-mêmes, qui seront remplacés par leur valeur.

À noter l'opérateur spécial defined qui permet de tester si une macro est définie (renvoit donc un booléen). Exemple :

#if defined(DEBUG) && defined(NDEBUG)
#error DEBUG et NDEBUG sont définis !
#endif

En fait, il y a un cas particulier où cette directive est très pratique : c'est pour désactiver des pans entier de code sans rien y modifier. Il suffit de mettre une expression qui vaudra toujours zéro, comme :

#if 0
/* Vous pouvez mettre ce que vous voulez ici, tout sera ignoré, même du code invalide */
:-)
#endif

À noter qu'au niveau du préprocesseur il est impossible d'utiliser les opérateurs du compilateur. Notamment l'opérateur sizeof, dont le manque aura fait grincer les dents à des générations de programmeur, sera en fait interprété comme étant une macro. Il faut bien garder à l'esprit que le préprocesseur C est totalement indépendant du langage C.

Inclusion de fichiers

Il s'agit d'une autre fonctionnalité massivement employée dans toute application C qui se respecte. Comme son nom l'indique, la directive #include permet d'inclure in extenso le contenu d'un autre fichier, comme s'il avait été écrit en lieu et place de la directive. On peut donc voir ça comme de la factorisation de code, ou plutôt de déclarations. On appelle de tels fichiers, des fichiers en-têtes (header files) et on leur donne en général l'extension .h.

Il est rare d'inclure du code dans ces fichiers (déclarations de variables ou de fonctions), principalement parce que ces fichiers sont destinés à être inclus dans plusieurs endroits du programme. Ces déclarations seraient donc définies plusieurs fois, ce qui, dans le meilleur des cas, seraient du code supperflu, et dans le pire, pourraient poser des problèmes à l'édition des liens.

On y place surtout des définitions de type, macros, prototypes de fonctions relatif à un module. On en profite aussi pour documenter toutes les fonctions publiques, leurs paramètres, les valeurs renvoyées, les effets de bords, la signification des champs de structures et les pré/post conditions (quel état doit respecter la fonction avant/après son appel). Dans l'idéal on devrait pouvoir comprendre le fonctionnement d'un module, simplement en lisant son fichier en-tête.

La directive #include prend en fait un argument : le nom du fichier que vous voulez inclure. Ce nom doit être soit mis entre guillemets doubles ou entre balises (< >). Cette différence affecte simplement l'ordre de recherche du fichier. Dans le premier cas, le fichier est recherché dans le répertoire où le fichier contenant la directive se trouve, puis le préprocesseur regarde à des endroits préconfigurés. Dans le second cas, il regardera seulement dans les endroits préconfigurés. Les endroits préconfigurés sont le répertoire include par défaut (par exemple /usr/include/, sous Unix) et ceux passés explicitement en paramètre au compilateur.

== fichier.h ==
extern void affiche( void );

== fichier.c ==

#include "fichier.h"

int main( void )
{
    affiche();
    return;
}

C'est comme si fichier.c avait été écrit :

extern void affiche( void );

int main( void )
{
    affiche();
    return;
}

En fait l'argument de la directive #include peut aussi être une macro, dont la valeur est un argument valide (soit une chaîne de caractères, soit un nom entre balises < et >) :

#define  FICHIER_A_INCLURE        <stdio.h>
#include FICHIER_A_INCLURE

À noter un problème relativement récurrent avec les fichiers en-têtes : il s'agit des inclusions multiples. À mesure qu'une application grandit, il arrive fréquemment qu'un fichier soit inclus plusieurs fois à la suite d'une directive #include. Quand bien même les déclarations sont identiques, définir deux types avec le même nom n'est pas permis en C. On peut néanmoins s'en sortir avec cette technique issue de la nuit des temps :

#ifndef __MON_FICHIER_H__
#define __MON_FICHIER_H__

/* Mettre toutes les déclarations ici */

#endif

C'est un problème tellement classique, que le premier réflexe lors de l'écriture d'un tel fichier est de rajouter ces directives. __MON_FICHIER_H__ est bien-sûr à adapter à chaque fichier, l'essentiel est que le nom soit unique dans toute l'application. Habituellement on utilise le nom du fichier, mis entre double souligné et le tout en majuscule.

Avertissement et message d'erreur personnalisés

Il peut être parfois utile d'avertir l'utilisateur que certaines combinaisons d'options sont dangeureuses ou carrément invalides. Le préprocesseur dispose de deux directives pour effectuer cela :

#warning message
#error message

Ces deux directives afficheront en fait tel quelle la ligne, y compris la directive pour bien indiquer qu'il s'agisse d'un message personnalisé et non venant du préprocesseur lui même. message peut évidemment contenir n'importe quoi, y compris des caractères réservés, sans qu'on ait besoin de le mettre entre guillement ("). Dans le cas de #warning la compilation continuera quand même, alors avec qu'avec #error la compilation s'arrêtera sitôt le message affiché.

Cet article provient de Wikibooks et est sous licence GNU Free Documentation License. Il a été écrit par plusieurs personnes et est constamment mis à jour. Cet article est la version du 6 mai 2005 à 04:25. L'article d'origine se trouve à http://fr.wikibooks.org/wiki/Programmation_C_Préprocesseur.