Accès rapide :
Macro-génération de code
L'instruction #define
L'instruction #undef
Les macros prédéfinies
Inclusion de fichiers
L'instruction #include
Protection contre l'inclusion multiple
Compilation conditionnelle
Les instructions #if
#ifdef
#ifndef
et #endif
Les instructions #else
et #elif
Quelques autres instructions
L'instruction #line
L'instruction #error
L'instruction #pragma
Une particularité propre aux compilateurs C est qu'ils ne travaillent pas directement sur le code source fourni par le programmeur. En fait une phase, spéciale de réécriture du programme précède toute compilation. L'utilitaire se chargeant de cette phase de prétraitement se nomme le préprocesseur.
Comme nous allons le voir, ce préprocesseur permet, entre autre, de définir des macros, de réaliser l'inclusion de fichiers (afin de réaliser une approche, certes peut être un peu archaïque, de la modularité) ou encore des compilations de code C conditionnelles. Nous allons donc étudier un à un chacun de ces points et quelques autres encore.
Notons, dès à présent, que toutes les instructions du préprocesseur commencent par un caractère « # ». Si vous oubliez ce premier caractère l'instruction ne sera plus considérée comme
une instruction du préprocesseur, mais comme une instruction C prise en charge par le compilateur. Il en résultera très certainement une erreur de compilation. Autre remarque : l'écriture d'une
instruction du préprocesseur est « case sensitive » (sensible à la casse). C'est à dire qu'il ne faut pas se tromper entre minuscules et majuscules. Ainsi, le mot clé
#Define
ne sera pas compris par le préprocesseur alors que l'instruction #define
sera parfaitement prise en charge. Faites très attention sur ce dernier point.
La macro-génération de code permet de réaliser des substitutions de code avant la phase de compilation. Cela peut notamment être utile pour concentrer la définition d'une valeur constante en un seul et unique point de votre code source. Dans ce cas, la macro sera vue comme une définition d'une constante et chaque utilisation de cette macro constante sera remplacée par la valeur associée par le préprocesseur. Une autre utilisation des macros permet de simuler des fonctions : dans ce cas, on parlera de macros paramétrées. Contrairement à une fonction (pour laquelle un appel de fonction est généré) une macro paramétrée verra son code injecté en lieu et place de l'utilisation de cette macro. Ainsi, comme aucun appel de fonction ne sera généré dans ce cas, pour des petits traitements (calcule de minimum, de maximum, ...) les performances seront améliorées. Attention à ne pas trop utiliser les macros pour des traitements lourds car cela a tendance à faire grossir la taille de l'exécutable.
Remarque : dans le cas de l'utilisation d'une macro pour gérer une valeur constante, il aurait une autre possibilité équivalente : définir une constante via le mot clé const
.
La différence, c'est qu'avec le mot clé const
, la problématique est gérée par le compilateur et plus le préprocesseur. Les deux techniques sont équivalentes et chacune d'entre elles
est couramment utilisée.
#define
L'instruction #define
permet donc de définir une macro. Comme indiqué précédemment, deux types de macros peuvent utilisées : les macros constantes et les macros paramétrées.
Pour les distinguer, regarder si des paramètres sont spécifiés entre parenthèses à la suite du nom de la macro. Si tel est le cas, alors vous avez à faire à une macro paramétrée. Si ce n'est
pas le cas, alors ce sera une macro constante.
Pour ce qui est de la syntaxe, une macro commence donc par le mot clé #define
, suivi du nom de la macro (avec éventuellement une liste de paramètres) et enfin elle se termine par
son code de substitution. Les trois parties étant juste séparées par des espaces.
Attention : contrairement à une instruction du langage C, les instructions du préprocesseur ne doivent pas être terminées par un caractère « ; ». Si vous oubliez cette règle, il sera très probable que des erreurs de compilations soient produites (dans le meilleur des cas) ou que des disfonctionnement soient observés durant l'exécution de votre programme (dans le pire des cas).
#include <stdio.h> #include <stdlib.h> /* Définition de quelques macros */ #define FALSE 0 #define TRUE 1 #define BOOL int #define MINI( a, b ) ((a)<(b) ? (a) : (b)) #define MAXI( a, b ) ((a)>(b) ? (a) : (b)) /* Exemple d'utilisation de ces macros */ int main() { BOOL state = TRUE; if ( state == FALSE ) { printf( "Maximum(3, 5) == %d", MAXI( 3, 5 ) ); } else { printf( "Minimum(3, 5) == %d", MINI( 3, 5 ) ); } return EXIT_SUCCESS; }
Remarque 1 : nous cherchons dans cette exemple à simuler un type booléen à partir de macro. Cela est fait par de très nombreux développeurs C étant donné que le type booléen n'existe tout
simplement pas en C. Par autant, depuis la version 2011 du standard C, une nouvelle entête (<stdbool.h>
) prédéfinie une
fois pour toute des macros similaires aux nôtres.
Remarque 2 : assez couramment et par convention, les noms des macros constantes sont orthographiés en majuscules. Nous vous recommandons donc de respecter cette convention afin de faciliter la lecture de vos codes.
Si vous êtes curieux, il est possible de voir le résultat de la transformation du code par le préprocesseur. Pour ce faire exécutez la commande suivante (l'option -E demandant à interrompre le traitement après la phase de preprocessing) :
gcc -E -o Sample.temp Sample.c
Pour information, voici le code de la fonction main (le point d'entrée de votre programme C) après traitement par le préprocesseur.
int main() { int state = 1; if ( state == 0 ) { printf( "Maximum(3, 5) == %d", ((3)>(5) ? (3) : (5)) ); } else { printf( "Minimum(3, 5) == %d", ((3)<(5) ? (3) : (5)) ); } return 0; }
#undef
Cette instruction permet d'annuler la définition d'une macro : à partir de cette ligne, la macro n'existera plus et son emploie amènera donc très certainement à une erreur de compilation, sauf si un élément de même nom que la macro existe dans le programme (comme c'est le cas dans l'exemple ci-dessous). Le fait qu'une macro puisse avoir le même nom qu'une variable (par exemple), est possible, car le préprocesseur et le compilateur sont deux traitements s'exécutant à deux moments différents : ces deux éléments de même nom peuvent donc cohabiter.
#include <stdio.h> #include <stdlib.h> int main() { int a = 123; #define a 456 printf( "a == %d\n", a ); #undef a printf( "a == %d\n", a ); return EXIT_SUCCESS; }
Si vous compilez ce programme et que vous l'exécutez, voici les résultats qui vous seront retournés.
$> gcc -o Sample -Wall Sample.c $> ./Sample a == 456 a == 123 $>
Il existe un certain nombre de macros qui sont prédéfinies et qui vous permettent de connaître un certain nombre de choses. Ces macros ne peuvent en aucun cas être supprimées ou altérées. Elles sont donc immuables. Le tableau suivant vous donne le nom de ces macros ainsi que leur développement.
Nom des macros | Développement de la macro associée |
---|---|
__LINE__ |
Se développe en une valeur numérique associée au numéro de la ligne courante dans le code du programme. |
__FILE__ |
Cette macro sera remplacée par le nom du fichier en cours de traitement. |
__DATE__ |
Celle-ci se transformera en une chaîne de caractères contenant la date de traitement du fichier sous un format "Mmm jj aaaa". |
__TIME__ |
De même, se transformera en un chaîne représentant l'heure de traitement du fichier sous le format "hh:mm:ss". |
__STDC__ |
Cette macro n'est définie que si votre compilateur respecte les normes ANSI ou ISO. Si tel est le cas, elle est alors définie à la valeur 1. |
__STDC_VERSION__ |
Cette macro existe que depuis C99 (C ISO 19999) : elle n'est donc non définie pour les anciens compilateur.
En C99 elle renverra la valeur |
Outre la macro-génération de code, le préprocesseur permet d'autres possibilités. Parmi ces possibilités, nous retrouvons l'inclusion de fichiers. En C, c'est grâce à ce mécanisme que nous allons
pouvoir faire de la programmation modulaire. Effectivement, coder 2 million de lignes de code dans un seul est unique fichier, pour être très problématique (temps de compilation extrêmement long,
très mauvaise maintenabilité du programme, ...). Il est donc conseillé de répartir un tel programme dans plusieurs fichiers. Mais si tel est le cas, alors il va falloir donner un petit coup de pouce
au compilateur pour qu'il sache qu'est ce qui sera fourni par un module de code précis. La définition de ces éléments sera effectuée dans un fichier d'entête (un fichier d'extension .h
).
Souvent nous aurons un .h
par module de code. Ce fichier d'extension .h
devra être inclue dans les fichiers qui utiliserons les éléments de ce module.
Dans les faits, nous utilisons déjà cette possibilité depuis un petit moment. Effectivement, nos exemples précédents utilisent des modules (des librairies) de codes pour la gestion des I/O
(Input/Output) et les fonctions standards : nous avons donc régulièrement inclus <stdio.h>
et <stdlib.h>
via le mot clé #include
. Il est important
de comprendre qu'un .h
ne permet que de vérifier la bonne cohérence des appels de fonctions. Les liens entre les différents modules de codes étant définitivement résolus qu'à la phase
d'édition des liens.
#include
L'instruction #include
injecte, en lieu est place de cette instruction, le contenu du fichier spécifié. Si ce fichier inclut contient lui-même des instructions #include
alors ces fichiers seront eux aussi récursivement inclus. Théoriquement, il n'y a pas de limite, si ce n'est les chaînes d'inclusion circulaire (à éviter);
Notez qu'il existe deux formes d'utilisation de l'instruction #include
. Ces deux formes correspondent à deux localisations de recherche sur le système de fichiers.
La syntaxe « #include <filename>
» permet de rechercher le fichier d'entête parmi celles proposées de base avec votre compilateur.
La syntaxe « #include "filename"
» permet de rechercher le fichier d'entête parmi celles développées au sein de votre programme. Attention à correctement utiliser ces
deux syntaxes (pour une meilleur compréhension du code) en sachant qu'avec la seconde solution, si le fichier n'est pas trouvé dans les répertories de votre application, alors une recherche est
effectuée dans les répertoires du compilateur.
#include "monfichier.h"; /* Fichier local au projet */ #include <stdxxx.h> /* Fichier fourni par le compilateur */
Dernier point par rapport à ce sujet : un fichier d'entête (un .h
) ne doit pas être inclue plusieurs fois dans un même fichier à compiler. Or parfois un de vos fichiers inclue d'autres
fichiers qui eux-mêmes inclue encore de fichiers... Comment être sûr qu'à des niveaux 2, 3 ou supérieurs d'inclusions on ne retombe pas plusieurs fois sur le même fichier ? La réponse réside
dans le fait de sécuriser un .h
contre l'inclusion multiple en se servant du mécanisme de macro. L'exemple ci-dessous montre comment le fichier <stdio.h>
est
sécurisé.
#ifndef _STDIO_H #define _STDIO_H 1 ... #endif /* !_STDIO_H */
Quelques explications : la première ligne cherche à tester si la macro _STDIO_H
est définie ou non (#ifnef
=> if not defined). Si tel est le cas, alors le fichier est
inclus pour la première fois. Il définit alors la macro pour que le prochain coup, le test soit négatif. La séquence « ...
» symbolise le contenu du fichier. Enfin la dernière
ligne marque la fin de la protection. Lors d'une seconde tentative d'inclusion, comme la macro sera définie. Tout ce qui sera compris entre le #ifndef
et le #endif
sera
ignoré et il n'y aura donc pas plusieurs fois l'inclusion du code.
Après la macro-génération de code et l'inclusion de fichier, le troisième type d'utilisation classique du préprocesseur réside dans la compilation conditionnelle. La compilation conditionnelle, comme son nom l'indique, consiste à adapter le contenu d'un fichier de code C en fonction de telle ou telle condition. Il en résulte que le compilateur traitera un fichier dont le contenu sera dépendant des conditions considéré.
Deux cas classiques d'utilisation de la compilation conditionnelle me viennent à l'esprit. Le premier cas consiste à adapter le contenu d'un fichier selon qu'on soit en mode debug ou non. Le second cas, assez similaire, consiste à adapter le contenu d'un fichier en fonction que l'on compile pour une plate-forme Linux, ou pour une autre (ainsi seuls les codes relatifs à une plate-forme seront produits dans le fichier final). En changeant la condition, le code final produit sera différent.
Il faut savoir que pour définir une macro, il y a une autre façon que l'utilisation de l'instruction #define
. Effectivement, en spécifiant des paramètres sur la ligne de commande de
votre compilateur, vous pouvez spécifier et affecter une valeur à vos macros. L'exemple ci-dessus montre comment définir une macro NDEBUG, en mode ligne de commande, avec le compilateur gcc
(la même option fonctionne aussi avec MinGW).
gcc -c -Wall -DNDEBUG=1 Sample.c
Remarque : la macro NDEBUG
est standard en C. Elle signifie No DEBUG : tous les codes relatifs au debug devront donc être supprimés si cette macro est définie.
#if
#ifdef
#ifndef
et #endif
Tout d'abord, j'attire votre attention sur le fait que nous avons à faire à différentes syntaxes équivalente : du coup, selon les développeurs considérés, les choses pourront s'exprimer différemment. Dans l'exemple ci-dessous, les deux premières lignes sont strictement équivalentes et il en va de même pour les deux dernières.
#ifdef MACRO_NAME #if defined MACRO_NAME #ifndef MACRO_NAME #if ! defined MACRO_NAME
Remarque : le caractère « ! » correspond ici à l'opérateur de négation. On peut donc lire la dernière ligne ainsi : if not defined.
Voici un exemple concret qui cherche à afficher certains messages que si l'on compile en mode debug.
#include <stdio.h> #include <stdlib.h> int main() { #ifndef NDEBUG printf( "Mode DEBUG activé\n" ); #endif printf( "Code de votre application\n" ); return EXIT_SUCCESS; }
Voici maintenant un exemple montrant le même programme, mais compilé de deux manières différentes en fonction de si la macro NDEBUG
est définie ou non.
$> gcc -o Sample -Wall Sample.c $> ./Sample Mode DEBUG activé Code de votre application $> gcc -o Sample -Wall -DNDEBUG=1 Sample.c $> ./Sample Code de votre application $>
Remarque : la valeur à laquelle nous fixons la macro NDEBUG
n'est pas réellement nécessaire. Effectivement, l'instruction #ifndef
test la non existence de la macro.
Comme il est aussi possible de déclarer une macro sans lui fixer de valeur, on aurait pu lancer la compilation ainsi :
$> gcc -o Sample -Wall -DNDEBUG Sample.c
#else
et #elif
Ces instructions sont, par exemple, très pratique si l'on cherche à compiler du code en fonction d'une plate-forme donnée ou d'une autre. Dans l'exemple ci-dessous, du code spécifique à la
plate-forme Microsoft Windows sera produit si la macro WIN32
est définie, sinon du code relatif à la plate-forme Linux sera généré.
#include <stdio.h> #include <stdlib.h> #ifdef WIN32 #include <windows.h> #else #include <unistd.h> /* Just for example ; not used here */ #endif int main() { #ifdef WIN32 MessageBox( NULL, "Hello", "Title", MB_OK ); #else system( "notify-send 'title' 'message'" ); #endif return EXIT_SUCCESS; }
L'instruction #elif
étant, bien entendu, un moyen rapide d'enchainer un #else
avec un #if
.
Le préprocesseur C propose encore quelques autres instructions : elles sont moins importantes que celle déjà proposées, mais peuvent néanmoins être utile dans quelques cas.
#line
Normalement, le préprocesseur communique au compilateur C le numéro de ligne censé être associé à chaque élément du fichier. Cela est normal, car entre la version du code source que vous vous
fournissez en entrée de la chaîne de compilation et ce que le préprocesseur produit, il peut y avoir de très grosses différences. Si les numéros de lignes, en cas d'erreurs ou d'avertissements, vous
étaient communiqué par rapport au fichier temporaire (et non par rapport au fichier source), vous auriez de grosse difficultés pour localiser les problèmes dans votre code source (pensez notamment
au cas des inclusions (#include
) qui s'injecte en plein milieu de votre fichier en modifiant le nombre de lignes totales). Dans l'ensemble d'exemple proposés sur ce site, nous n'avons
pas utilisé l'instruction #line
: le préprocesseur gérant très bien ces aspects.
Néanmoins, vous aurez peut-être un jour besoin de contrôler ces numéros de lignes. L'instruction #line
est faite pour cela. Elle permet de spécifier un nouveau numéro de ligne ainsi
qu'un autre éventuel nom de fichier (pour signaler une erreur localisé dans un autre fichier inclus). Dès lors qu'un nouveau numéro de ligne est spécifié, il sera considéré pour la ligne suivante
et les lignes qui suivront seront incrémentés par rapport à ce nouveau numéro de ligne. Voici deux formes d'utilisation de cette instruction.
#line number #line number "file_name"
#error
L'instruction #error
permet de faire afficher un message d'erreur, spécifier à la suite de la ligne.
#error Specify here your error message
#pragma
Cette instruction permet de passer des informations additionnelles au compilateur. Par exemple, certains compilateurs acceptent de supprimer certains types d'avertissements qu'ils produisent.
Il est important de comprendre que le standard C99 impose la présence du mot clé #pragma
dans le langage, par contre il laisse libre les équipes de développement de ces compilateurs
d'utiliser cette syntaxe pour leur propre besoins. Si le pragma spécifié n'est pas reconnu par votre compilateur, il sera alors ignoré. L'exemple ci-dessous, montre comment supprimer un type
d'avertissement (dans cet exemple, les variables locales non utilisées) avec le compilateur C de la société Microsoft.
#pragma warning( disable : 4101 )
Améliorations / Corrections
Vous avez des améliorations (ou des corrections) à proposer pour ce document : je vous remerçie par avance de m'en faire part, cela m'aide à améliorer le site.
Emplacement :
Description des améliorations :