[Plan du site]
Vous êtes ici ---
> Le Site du Zér0
> Les tutoriels
> Officiels
> Programmation
> Lecture du tutoriel
Le préprocesseur
Après ces derniers chapitres harassants sur les pointeurs, tableaux et chaînes de caractères, nous allons faire
une pause.
Je veux dire par là que vous avez dû encaisser pas mal de chocs dans les chapitres précédents, et que je ne peux donc pas vous refuser de souffler un peu
Ceci étant, pas question de se reposer sans rien apprendre (ça va pas la tête ?

). On va donc voir ensemble un chapitre simple, contenant d'ailleurs quelques rappels. Ce chapitre va traiter du préprocesseur, ce programme qui s'exécute juste avant la compilation.
Ne vous y trompez pas : les informations contenues dans ce chapitre vous seront utiles. Elles sont juste faciles à comprendre... et ça nous arrange
Comme je vous l'ai expliqué dans les tous premiers chapitres du cours, on trouve dans les codes sources des lignes un peu particulières appelées
directives de préprocesseur.
Ces directives de préprocesseur ont la caractéristique suivante : elles commencent toujours par le symbole #. Elles sont donc faciles à reconnaître.
La première (et seule) directive que nous ayons vue pour l'instant est #include.
Cette directive permet d'inclure le contenu d'un fichier dans un autre, je vous l'ai dit plus tôt.
On s'en sert en particulier pour inclure des fichiers .h comme les fichiers .h des librairies (stdlib.h, stdio.h, string.h, math.h...) et vos propres fichiers .h.
Pour inclure un fichier .h se trouvant dans
le dossier où est installé votre IDE, vous devez utiliser les chevrons < > :
Code : C
Pour inclure un fichier .h se trouvant dans
le dossier de votre projet, vous devez utiliser les guillemets :
Code : C
Concrètement,
le préprocesseur est démarré avant la compilation. Il parcourt tous vos fichiers à la recherche de directives de préprocesseur, ces fameuses lignes qui commencent par un #
Lorsqu'il rencontre la directive #include, il met littéralement le contenu du fichier indiqué à l'endroit du #include.
Supposons que j'aie un "fichier.c" contenant le code de mes fonctions et un "fichier.h" contenant les prototypes des fonctions de fichier.c. On pourrait résumer la situation dans ce schéma tout simple :
Tout le contenu de fichier.h est mis à l'intérieur de fichier.c, à l'endroit où il y a la directive #include fichier.h
Imaginons qu'on ait dans le fichier.c :
Code : C 1
2
3
4
5
6
7
8
9
10
11 | #include "fichier.h"
long maFonction(int truc, double bidule)
{
/* Code de la fonction */
}
void autreFonction(long valeur)
{
/* Code de la fonction */
}
|
Et dans le fichier.h :
Code : C1
2 | long maFonction(int truc, double bidule);
void autreFonction(long valeur);
|
Lorsque le préprocesseur passe par là, juste avant la compilation de fichier.c, il met fichier.h dans fichier.c. Au final, le code source de fichier.c
juste avant la compilation ressemble à ça :
Code : C 1
2
3
4
5
6
7
8
9
10
11
12 | long maFonction(int truc, double bidule);
void autreFonction(long valeur);
long maFonction(int truc, double bidule)
{
/* Code de la fonction */
}
void autreFonction(long valeur)
{
/* Code de la fonction */
}
|
Le contenu du .h est venu se mettre à l'emplacement de la ligne #include
Ce n'est pas bien compliqué à comprendre, je pense d'ailleurs que bon nombre d'entre vous devaient se douter que ça fonctionnait comme ça.
Au moins là avec ces explications supplémentaires, j'espère avoir mis tout le monde d'accord

Le #include ne fait rien d'autre qu'insérer un fichier dans un autre, c'est important de bien le comprendre.
Si on a décidé de mettre les prototypes dans les .h, au lieu de tout mettre dans les .c, c'est principalement par principe. On pourrait à priori mettre les prototypes en haut des .c (d'ailleurs dans certains très petits programmes on le fait parfois), mais pour des questions d'organisation il est vivement conseillé de placer ses prototypes dans des .h.
Nous allons découvrir maintenant une nouvelle directive de préprocesseur : le #define.
Cette directive permet de définir une
constante de préprocesseur. Cela permet d'associer une valeur à un mot. Voici un exemple :
Code : C1 | #define NOMBRE_VIES_INITIALES 3
|
Vous mettez dans l'ordre :
- le #define
- le mot auquel la valeur va être associée
- la valeur du mot
Attention, malgré les apparences (notamment le nom que l'on a l'habitude de mettre en majuscules) c'est
très différent des constantes que nous avons étudiées jusqu'ici, telles que :
Code : C1 | const long NOMBRE_VIES_INITIALES = 3;
|
Les constantes occupaient de la place en mémoire. Même si la valeur ne changeait pas, votre nombre "3" était stocké quelque part dans la mémoire.
Ce n'est pas le cas des constantes de préprocesseur
Comment ça fonctionne ? En fait, le #define remplace dans votre code source tous les mots par leur valeur correspondante. C'est comme la fonction "rechercher / remplacer" de Word si vous voulez

Ainsi, la ligne :
Code : C1 | #define NOMBRE_VIES_INITIALES 3
|
... remplace dans le fichier chaque NOMBRE_VIES_INITIALES par 3.
Voici un exemple de fichier .c avant passage du préprocesseur :
Code : C1
2
3
4
5
6
7 | #define NOMBRE_VIES_INITIALES 3
int main(int argc, char *argv[])
{
long vies = NOMBRES_VIES_INITIALES;
/* Code ...*/
|
Après passage du préprocesseur :
Code : C1
2
3
4
5 | int main(int argc, char *argv[])
{
long vies = 3;
/* Code ...*/
|
Avant la compilation, tous les #define auront donc été remplacés par les valeurs correspondantes. Le compilateur "voit" le fichier après passage du préprocesseur, dans lequel tous les remplacements auront été effectués.
Quel intérêt par rapport à l'utilisation de constantes comme on l'a vu jusqu'ici ?
Eh bien, comme je vous l'ai dit ça
ne prend pas de place en mémoire. C'est logique, vu que lors de la compilation il ne reste plus que des nombres dans le code source.
Un autre intérêt est que le remplacement se fait dans
tout le fichier dans lequel se trouve le #define. Si vous aviez défini une constante en mémoire dans une fonction, celle-ci n'aurait été valable que dans la fonction puis aurait été supprimée. Le #define en revanche s'appliquera à toutes les fonctions du fichier, ce qui peut s'avérer vraiment pratique pour le programmeur.
Un exemple concret d'utilisation des #define ?
En voici un que vous ne tarderez pas à utiliser. Lorsque vous ouvrirez une fenêtre en C, vous aurez probablement envie de définir des constantes de préprocesseur pour indiquer les dimensions de la fenêtre :
Code : C1
2 | #define LARGEUR_FENETRE 800
#define HAUTEUR_FENETRE 600
|
L'avantage est que si plus tard vous décidez de changer la taille de la fenêtre (parce que ça vous semble trop petit), il vous suffira de modifier les #define puis de recompiler.
A noter : les #define sont généralement placés dans des .h, à côté des prototypes (vous pouvez d'ailleurs aller voir les .h des librairies comme stdlib.h, vous verrez qu'il y a des #define !).
Les #define sont donc "faciles d'accès", vous pouvez changer les dimensions de la fenêtre en modifiant les #define plutôt que d'aller chercher au fond de vos fonctions l'endroit où vous ouvrez la fenêtre pour modifier les dimensions. C'est donc du temps de gagné pour le programmeur
En résumé, les constantes de préprocesseur permettent de "configurer" votre programme avant sa compilation. C'est une sorte de mini-configuration en fait
Un define pour la taille des tableaux
On utilise souvent les defines pour définir la taille des tableaux. On fait par exemple :
Code : C1
2
3
4
5
6 | #define TAILLE_MAX 1000
int main(int argc, char *argv[])
{
char chaine1[TAILLE_MAX], chaine2[TAILLE_MAX];
// ...
|
Mais... Je croyais qu'on ne pouvait pas mettre de variable entre les crochets lors d'une définition de tableau ?
Oui, mais TAILLE_MAX n'est PAS une variable

En effet je vous l'ai dit, le préprocesseur transforme le fichier avant compilation en :
Code : C1
2
3
4 | int main(int argc, char *argv[])
{
char chaine1[1000], chaine2[1000];
// ...
|
... et cela est valide
En définissant TAILLE_MAX ainsi, vous pouvez vous en servir pour créer des tableaux d'une certaine taille. Si cela s'avère insuffisant par le futur, vous n'aurez qu'à modifier la ligne du #define, recompiler, et vos tableaux de char prendront tous la nouvelle taille que vous aurez indiquée
Calculs dans les defines
Il est possible de faire quelques petits calculs dans les defines.
Par exemple, ce code crée une constante LARGEUR_FENETRE, une autre HAUTEUR_FENETRE, puis une troisième NOMBRE_PIXELS qui contiendra le nombre de pixels affichés à l'intérieur de la fenêtre (le calcul est simple : largeur * hauteur) :
Code : C1
2
3 | #define LARGEUR_FENETRE 800
#define HAUTEUR_FENETRE 600
#define NOMBRE_PIXELS (LARGEUR_FENETRE * HAUTEUR_FENETRE)
|
La valeur de NOMBRE_PIXELS est remplacée avant la compilation par (LARGEUR_FENETRE * HAUTEUR_FENETRE), c'est-à-dire par (800 * 600), ce qui fait 480000

Mettez toujours votre calcul entre parenthèses comme je l'ai fait.
Vous pouvez faire toutes les opérations de base que vous connaissez : addition (+), soustraction (-), multiplication (*), division (/) et modulo (%)
Les constantes prédéfinies
En plus des constantes que vous pouvez définir vous-mêmes, il existe quelques constantes prédéfinies par le préprocesseur. A l'heure où j'écris ces lignes, je dois vous dire que je ne les ai encore jamais utilisées, mais il n'est pas impossible que vous leur en trouviez une utilité donc je vais vous les présenter
Chacune de ces constantes commence et se termine par 2 symboles underscore _ (que vous trouverez sous le chiffre 8, tout du moins si vous avez un clavier AZERTY).
- __LINE__ : donne le numéro de la ligne actuelle
- __FILE__ : donne le nom du fichier actuel
- __DATE__ : donne la date de la compilation
- __TIME__ : donne l'heure de la compilation
Je pense que ces constantes peuvent être utiles pour gérer des erreurs, en faisant par exemple ceci :
Code : C1
2 | printf("Erreur a la ligne %ld du fichier %s\n", __LINE__, __FILE__);
printf("Ce fichier a ete compile le %s a %s\n", __DATE__, __TIME__);
|
Code : Console | Erreur a la ligne 9 du fichier main.c
Ce fichier a ete compile le Jan 13 2006 a 19:21:10 |
Les définitions simples
Il est aussi possible de faire tout simplement :
Code : C
... sans préciser de valeur.
Cela veut dire pour le préprocesseur que le mot CONSTANTE est défini, tout simplement. Il n'a pas de valeur, mais il "existe".
J'en vois vraiment pas l'intérêt !?
L'intérêt est moins évident que tout à l'heure, mais il y en a un et nous allons le découvrir tout à l'heure
Nous avons vu qu'avec le #define on pouvait demander au préprocesseur de remplacer un mot par une valeur. Par exemple :
Code : C
... signifie que tous les "NOMBRE" de votre code seront remplacés par 9. Nous avons vu qu'il s'agissait en fait d'un simple rechercher / remplacer fait par le préprocesseur avant la compilation.
J'ai du nouveau !

En fait, le #define est encore plus puissant que ça. Il permet de remplacer aussi par... du code source tout entier ! Quand on utilise #define pour rechercher / remplacer un mot par un code source, on dit qu'on crée
une macro.
Macro sans paramètres
Voici un exemple de macro très simple :
Code : C1 | #define COUCOU() printf("Coucou");
|
Ce qui change ici, c'est les parenthèses qu'on a ajoutées après le mot-clé (ici
COUCOU()). Nous verrons à quoi elles peuvent servir tout à l'heure.
Testons la macro dans un code source :
Code : C1
2
3
4
5
6
7
8 | #define COUCOU() printf("Coucou");
int main(int argc, char *argv[])
{
COUCOU()
return 0;
}
|
Code : Console
Je vous l'accorde, ce n'est pas original pour le moment
Ce qu'il faut bien comprendre déjà, c'est que les macros ne sont en fait que des bouts de code qui sont directement remplacés dans votre code source juste avant la compilation.
Le code qu'on vient de voir ressemblera en fait à ça lors de la compilation :
Code : C1
2
3
4
5
6 | int main(int argc, char *argv[])
{
printf("Coucou");
return 0;
}
|
Si vous avez compris ça, vous avez compris le principe de base des macros déjà
Mais... On ne peut mettre qu'une seule ligne de code par macro ?
Non, heureusement il est possible de mettre plusieurs lignes de code à la fois. Il suffit de mettre un \ avant chaque nouvelle ligne, comme ceci :
Code : C 1
2
3
4
5
6
7
8
9
10 | #define RACONTER_SA_VIE() printf("Coucou, je m'appelle Brice\n"); \
printf("J'habite a Nice\n"); \
printf("J'aime la glisse\n");
int main(int argc, char *argv[])
{
RACONTER_SA_VIE()
return 0;
}
|
Code : Console | Coucou, je m'appelle Brice
J'habite a Nice
J'aime la glisse |
Remarquez dans le main que l'appel de la macro ne prend pas de point-virgule à la fin. En effet, c'est une ligne pour le préprocesseur, elle ne nécessite donc pas d'être terminée par un point-virgule
Macro avec paramètres
Pour le moment, on a vu comment faire une macro sans paramètre, c'est-à-dire avec rien entre les parenthèses. Le principal intérêt de ce type de macros, c'est de pouvoir "raccourcir" un code un peu long, surtout s'il est amené à être répété de nombreuses fois dans votre code source.
Cependant, les macros deviennent réellement intéressantes lorsqu'on leur met des paramètres. Cela marche quasiment comme avec les fonctions.
Code : C1
2
3
4
5
6
7
8
9 | #define MAJEUR(age) if (age >= 18) \
printf("Vous etes majeur\n");
int main(int argc, char *argv[])
{
MAJEUR(22)
return 0;
}
|
Code : Console
Notez qu'on aurait aussi pu rajouter un else pour afficher "Vous êtes mineur". Essayez de le faire pour vous entraîner, ce n'est pas bien difficile. N'oubliez pas de mettre un antislash \ avant chaque nouvelle ligne
Le principe de notre macro est assez intuitif je trouve :
Code : C1
2 | #define MAJEUR(age) if (age >= 18) \
printf("Vous etes majeur\n");
|
On met entre parenthèses le nom d'une "variable" qu'on nomme age. Dans tout notre code de remplacement, age sera remplacé par le nombre qui est indiqué lors de l'appel à la macro (ici c'est 22).
Ainsi, notre code source de tout à l'heure ressemblera à ceci juste après le passage du préprocesseur :
Code : C1
2
3
4
5
6
7 | int main(int argc, char *argv[])
{
if (22 >= 18)
printf("Vous etes majeur\n");
return 0;
}
|
Le code source a été mis à la place de l'appel de la macro, et la valeur de la "variable" age a été mise directement dans le code source de remplacement.
Il est possible aussi de créer une macro qui prend plusieurs paramètres :
Code : C1
2
3
4
5
6
7
8
9 | #define MAJEUR(age, nom) if (age >= 18) \
printf("Vous etes majeur %s\n", nom);
int main(int argc, char *argv[])
{
MAJEUR(22, "Maxime")
return 0;
}
|
Voilà en gros tout ce qu'on peut dire sur les macros. Il faut donc retenir que c'est un simple remplacement de code source qui a l'avantage de pouvoir prendre des paramètres.
Tenez-vous bien : il est possible de réaliser des conditions en langage préprocesseur

Voici comment cela fonctionne :
Code : C1
2
3
4
5 | #if condition
/* Code source à compiler si la condition est vraie */
#elif condition2
/* Sinon` si la condition 2 est vraie` compiler ce code source */
#endif
|
Le mot-clé #if permet d'insérer une condition de préprocesseur. #elif signifie "else if" (sinon si).
La condition s'arrête lorsque vous insérez un #endif. Vous noterez qu'il n'y a pas d'accolades en préprocesseur.
L'intérêt, c'est qu'on peut ainsi faire des
compilations conditionnelles.
En effet, si la condition est vraie, le code qui suit sera compilé. Sinon, il sera tout simplement supprimé du fichier pour le temps de la compilation. Il n'apparaîtra donc pas dans le programme final.
#ifdef, #ifndef
Nous allons voir maintenant l'intérêt de faire un #define d'une constante sans préciser de valeur, comme je vous l'ai montré tout à l'heure :
Code : C
En effet, il est possible d'utiliser #ifdef pour dire "Si la constante est définie".
#ifndef, lui, sert à dire "Si la constante n'est pas définie".
On peut alors imaginer ceci :
Code : C 1
2
3
4
5
6
7
8
9
10
11
12
13 | #define WINDOWS
#ifdef WINDOWS
/* Code source pour Windows */
#endif
#ifdef LINUX
/* Code source pour Linux */
#endif
#ifdef MAC
/* Code source pour Mac */
#endif
|
C'est comme ça que font les programmes multi plateformes pour s'adapter à l'OS par exemple

Alors, bien entendu, il faut recompiler le programme pour chaque OS (ce n'est pas magique :D). Si vous êtes sous Windows, vous mettez un
#define WINDOWS en haut, puis vous compilez.
Si vous voulez compiler votre programme pour Linux (avec la partie du code source spécifique à Linux), alors vous devrez modifier le define et mettre à la place :
#define LINUX. Recompilez, et cette fois c'est la portion de code source pour Linux qui sera compilée, les autres parties étant ignorées.
#ifndef pour éviter les inclusions infinies
#ifndef est très utilisé dans les .h pour éviter les "inclusions infinies".
Comment ça une inclusion infinie ? ?
Imaginez, c'est très simple.
J'ai un fichier A.h et un fichier B.h. Le fichier A.h contient un include du fichier B.h. Le fichier B est donc inclus dans le fichier A.
Mais, et c'est là le hic, supposez que le fichier B.h contienne à son tour un include du fichier A.h ? Ca arrive quelques fois en programmation ! Le premier fichier a besoin du second pour fonctionner, et le second a besoin du premier.
Si on y réfléchit 10 petites secondes, on imagine vite ce qu'il va se passer :
- L'ordinateur lit A.h et voit qu'il faut inclure B.h
- Il lit B.h pour l'inclure, et là il voit qu'il faut inclure A.h
- Donc il inclut A.h dans B.h, mais dans A.h on lui indique qu'il doit inclure B.h !
- Rebelote, il va voir B.h et voit à nouveau qu'il faut inclure A.h
- etc etc.
Pas besoin d'être un pro pour comprendre que ça ne s'arrêtera jamais !
En fait, à force de faire trop d'inclusions, le préprocesseur s'arrêtera en disant "
y'en a marre des inclusions !" et du coup bah votre compilation plantera
Comment diable faire pour éviter cet affreux cauchemar ?
Voici l'astuce. Désormais, je vous demande de faire comme ça
dans TOUS vos fichiers .h sans exception :
Code : C1
2
3
4
5
6 | #ifndef DEF_NOMDUFICHIER // Si la constante n'a pas été définie` le fichier n'a jamais été inclus
#define DEF_NOMDUFICHIER // On définit la constante pour que la prochaine fois le fichier ne soit plus inclus
/* Contenu de votre fichier .h (autres includes` prototypes de vos fonctions` defines...) */
#endif
|
Vous mettrez en fait tout le contenu de votre fichier .h (à savoir vos autres includes, vos prototypes, vos defines...)
entre le #ifndef et le #endif.
Comprenez-vous bien comment ce code fonctionne ? Je ne l'ai pas compris du premier coup quand on me l'a expliqué moi pour être tout à fait franc
Imaginez que le fichier .h est inclus pour la première fois. Il lit la condition "Si la constante DEF_NOMDUFICHIER n'a pas été définie". Comme c'est la première fois que le fichier est lu, la constante n'est pas définie, donc le préprocesseur rentre à l'intérieur du if.
La première instruction qu'il rencontre est justement :
Code : C
Maintenant, la constante est définie. La prochaine fois que le fichier sera inclus, la condition ne sera plus vraie, et donc le fichier ne risque plus d'être réinclus.
Bien entendu, vous appelez votre constante comme vous voulez. Moi je l'appelle DEF_NOMDUFICHIER par habitude, maintenant c'est chacun ses petites manies

Ce qui compte en revanche, et j'espère que vous l'aviez bien compris, c'est de changer de nom de constante à chaque fichier .h différent. Il ne faut pas que ça soit la même constante pour tous les fichiers .h, sinon seul le premier fichier .h serait lu et pas les autres

Vous remplacerez donc NOMDUFICHIER par le nom de votre fichier .h.
Si vous voulez vérifier que je ne suis pas en train de vous raconter des bêtises, je vous invite à aller consulter les .h des librairies standard sur votre disque dur. Vous verrez qu'ils sont TOUS construits sur le même principe (un ifndef au début et un endif à la fin). Ils s'assurent ainsi qu'il ne pourra pas y avoir d'inclusions infinies.
C'est marrant, j'ai presque l'impression de vous avoir enseigné un nouveau langage de programmation là

Et c'est un peu vrai d'ailleurs, car le préprocesseur, ce fameux programme qui lit vos codes sources juste avant de les envoyer au compilateur, a son propre langage à lui.
On peut faire 2-3 autres petites choses dont je ne vous ai pas parlé ici, mais globalement on en a fait le tour. On utilise beaucoup les directives de préprocesseur dans les .h comme vous l'avez vu.
Ah, aussi une petite remarque qui ne mange pas de pain avant de terminer : je vous conseille vivement de mettre quelques retours à la ligne après le #endif à la fin de vos fichiers .h.
Evitez que #endif soit la dernière ligne du fichier, j'ai déjà eu des erreurs de compilation à cause de ça et j'ai eu du mal à comprendre au début d'où ça venait. J'avais l'erreur "No new ligne at the end of file bidule"
Mettez donc 2-3 retours à la ligne après le #endif comme ceci :
Code : C
Cette remarque vaut d'ailleurs aussi pour la fin des fichiers .c. Mettez toujours quelques retours à la ligne vides à la fin, ça ne coûte rien et ça évite des prises de tête inutiles.
Je n'ai jamais dit que la programmation était une science exacte hein

Parfois, on tombe sur des erreurs tellement bizarres qu'on en vient à se demander s'il ne faudrait pas faire d'incantations vaudoues
Vous inquiétez pas si ça vous arrive, c'est l'métier qui rentre