Mis à jour le mardi 25 juillet 2017
  • 40 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

Ce cours existe en livre papier.

Ce cours existe en eBook.

Vous pouvez obtenir un certificat de réussite à l'issue de ce cours.

Vous pouvez être accompagné et mentoré par un professeur particulier par visioconférence sur ce cours.

J'ai tout compris !

Le préprocesseur

Connectez-vous ou inscrivez-vous pour bénéficier de toutes les fonctionnalités de ce cours !

Après ces derniers chapitres harassants sur les pointeurs, tableaux et chaînes de caractères, nous allons faire une pause. Vous avez dû intégrer un certain nombre de nouveautés dans les chapitres précédents, je ne peux donc pas vous refuser de souffler un peu.

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 en revanche moins complexes que ce que vous avez eu à assimiler récemment.

Les include

Comme je vous l'ai expliqué dans les tout premiers chapitres du cours, on trouve dans les codes source 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.hcomme les fichiers.hdes bibliothèques (stdlib.h,stdio.h,string.h,math.h…) et vos propres fichiers.h.

Pour inclure un fichier.hse trouvant dans le dossier où est installé votre IDE, vous devez utiliser les chevrons< >:

#include <stdlib.h>

Pour inclure un fichier.hse trouvant dans le dossier de votre projet, vous devez en revanche utiliser les guillemets :

#include "monfichier.h"

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 insère littéralement le contenu du fichier indiqué à l'endroit du#include.

Supposons que j'aie unfichier.ccontenant le code de mes fonctions et unfichier.hcontenant les prototypes des fonctions defichier.c. On pourrait résumer la situation dans le schéma de la fig. suivante.

Inclusion de fichier

Tout le contenu defichier.hest mis à l'intérieur defichier.c, à l'endroit où il y a la directive#include fichier.h.

Imaginons qu'on ait dans lefichier.c:

#include "fichier.h"

int maFonction(int truc, double bidule)
{
    /* Code de la fonction */
}

void autreFonction(int valeur)
{
    /* Code de la fonction */
}

Et dans lefichier.h:

int maFonction(int truc, double bidule);
void autreFonction(int valeur);

Lorsque le préprocesseur passe par là, juste avant la compilation defichier.c, il insèrefichier.hdansfichier.c. Au final, le code source defichier.cjuste avant la compilation ressemble à ça :

int maFonction(int truc, double bidule);
void autreFonction(int valeur);

int maFonction(int truc, double bidule)
{
    /* Code de la fonction */
}

void autreFonction(int valeur)
{
    /* Code de la fonction */
}

Le contenu du.hest 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.
Avec ces explications supplémentaires, j'espère avoir mis tout le monde d'accord. Le#includene fait rien d'autre qu'insérer un fichier dans un autre, c'est important de bien le comprendre.

Les define

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 :

#define NOMBRE_VIES_INITIALES 3

Vous devez écrire 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), cela est très différent des constantes que nous avons étudiées jusqu'ici, telles que :

const int 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#defineremplace dans votre code source tous les mots par leur valeur correspondante. C'est un peu comme la fonction « Rechercher / Remplacer » de Word si vous voulez. Ainsi, la ligne :

#define NOMBRE_VIES_INITIALES 3

… remplace dans le fichier chaqueNOMBRE_VIES_INITIALESpar 3.

Voici un exemple de fichier.cavant passage du préprocesseur :

#define NOMBRE_VIES_INITIALES 3

int main(int argc, char *argv[])
{
    int vies = NOMBRE_VIES_INITIALES;

    /* Code ...*/

Après passage du préprocesseur :

int main(int argc, char *argv[])
{
    int vies = 3;

    /* Code ...*/

Avant la compilation, tous les#defineauront 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#defineen revanche s'appliquera à toutes les fonctions du fichier, ce qui peut s'avérer parfois pratique selon les besoins.

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 :

#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#definepuis de recompiler.

À noter : les#definesont généralement placés dans des.h, à côté des prototypes (vous pouvez d'ailleurs aller voir les.hdes bibliothèques commestdlib.h, vous verrez qu'il y a des#define!).
Les#definesont donc « faciles d'accès », vous pouvez changer les dimensions de la fenêtre en modifiant les#defineplutô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 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.

Undefinepour la taille des tableaux

On utilise souvent lesdefinepour définir la taille des tableaux. On écrit par exemple :

#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 ni de constante entre les crochets lors d'une définition de tableau ?

Oui, maisTAILLE_MAXn'est PAS une variable ni une constante. En effet je vous l'ai dit, le préprocesseur transforme le fichier avant compilation en :

int main(int argc, char *argv[])
{
    char chaine1[1000], chaine2[1000];
    // ...

… et cela est valide !

En définissantTAILLE_MAXainsi, vous pouvez vous en servir pour créer des tableaux d'une certaine taille. Si à l'avenir cela s'avère insuffisant, vous n'aurez qu'à modifier la ligne du#define, recompiler, et vos tableaux decharprendront tous la nouvelle taille que vous aurez indiquée.

Calculs dans lesdefine

Il est possible de faire quelques petits calculs dans lesdefine.
Par exemple, ce code crée une constanteLARGEUR_FENETRE, une autreHAUTEUR_FENETRE, puis une troisièmeNOMBRE_PIXELSqui contiendra le nombre de pixels affichés à l'intérieur de la fenêtre (le calcul est simple : largeur * hauteur) :

#define LARGEUR_FENETRE  800
#define HAUTEUR_FENETRE  600
#define NOMBRE_PIXELS    (LARGEUR_FENETRE * HAUTEUR_FENETRE)

La valeur deNOMBRE_PIXELSest remplacée avant la compilation par le code suivant : (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 par sécurité pour bien isoler l'opération.

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.

Chacune de ces constantes commence et se termine par deux symboles underscore_(vous trouverez ce symbole sous le chiffre 8, tout du moins si vous avez un clavier AZERTY français).

  • __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.

Ces constantes peuvent être utiles pour gérer des erreurs, en faisant par exemple ceci :

printf("Erreur a la ligne %d du fichier %s\n", __LINE__, __FILE__);
printf("Ce fichier a ete compile le %s a %s\n", __DATE__, __TIME__);
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 :

#define CONSTANTE

… sans préciser de valeur.
Cela veut dire pour le préprocesseur que le motCONSTANTEest défini, tout simplement. Il n'a pas de valeur, mais il « existe ».

Quel peut en être 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 très rapidement.

Les macros

Nous avons vu qu'avec le#defineon pouvait demander au préprocesseur de remplacer un mot par une valeur. Par exemple :

#define NOMBRE 9

… signifie que tous lesNOMBREde 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#defineest encore plus puissant que ça. Il permet de remplacer aussi par… un code source tout entier ! Quand on utilise#definepour 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 :

#define COUCOU() printf("Coucou");

Ce qui change ici, ce sont les parenthèses qu'on a ajoutées après le mot-clé (iciCOUCOU()). Nous verrons à quoi elles peuvent servir tout à l'heure.

Testons la macro dans un code source :

#define COUCOU() printf("Coucou");

int main(int argc, char *argv[])
{
    COUCOU()

    return 0;
}
Coucou

Je vous l'accorde, ce n'est pas original pour le moment. Ce qu'il faut déjà bien comprendre, 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 :

int main(int argc, char *argv[])
{
    printf("Coucou");

    return 0;
}

Si vous avez compris ça, vous avez compris le principe de base des macros.

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 placer un\avant chaque nouvelle ligne, comme ceci :

#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;
}
Coucou, je m'appelle Brice
J'habite a Nice
J'aime la glisse

Remarquez dans lemainque 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 des parenthèses vides. 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.

#define MAJEUR(age) if (age >= 18) \
                    printf("Vous etes majeur\n");

int main(int argc, char *argv[])
{
    MAJEUR(22)

    return 0;
}
Vous etes majeur

Le principe de notre macro est assez intuitif :

#define MAJEUR(age) if (age >= 18) \
                    printf("Vous etes majeur\n");

On met entre parenthèses le nom d'une « variable » qu'on nommeage. Dans tout notre code de macro,agesera remplacé par le nombre qui est indiqué lors de l'appel à la macro (ici, c'est 22).

Ainsi, notre code source précédent ressemblera à ceci juste après le passage du préprocesseur :

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 »agea été mise directement dans le code source de remplacement.

Il est possible aussi de créer une macro qui prend plusieurs paramètres :

#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à 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.

Les conditions

Tenez-vous bien : il est possible de réaliser des conditions en langage préprocesseur ! Voici comment cela fonctionne :

#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é#ifpermet d'insérer une condition de préprocesseur.#elifsignifieelse 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 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#defined'une constante sans préciser de valeur, comme je vous l'ai montré précédemment :

#define CONSTANTE

En effet, il est possible d'utiliser#ifdefpour dire « Si la constante est définie ».
#ifndef, lui, sert à dire « Si la constante n'est pas définie ».

On peut alors imaginer ceci :

#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 certains programmes multi-plates-formes pour s'adapter à l'OS par exemple.
Alors, bien entendu, il faut recompiler le programme pour chaque OS (ce n'est pas magique). Si vous êtes sous Windows, vous écrivez un#define WINDOWSen haut, puis vous compilez.
Si vous voulez compiler votre programme pour Linux (avec la partie du code source spécifique à Linux), vous devrez alors modifier ledefineet 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.

#ifndefpour éviter les inclusions infinies

#ifndefest très utilisé dans les.hpour éviter les « inclusions infinies ».

Une inclusion infinie ? C'est-à-dire ?

Imaginez, c'est très simple.
J'ai un fichierA.het un fichierB.h. Le fichierA.hcontient unincludedu fichierB.h. Le fichier B est donc inclus dans le fichier A.
Mais, et c'est là que ça commence à coincer, supposez que le fichierB.hcontienne à son tour unincludedu fichierA.h! Ça 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 un peu, on imagine vite ce qu'il va se passer :

  1. l'ordinateur litA.het voit qu'il faut inclureB.h;

  2. il litB.hpour l'inclure, et là il voit qu'il faut inclureA.h;

  3. il inclut doncA.hdansB.h, mais dansA.hon lui indique qu'il doit inclureB.h!

  4. rebelote, il va voirB.het voit à nouveau qu'il faut inclureA.h;

  5. etc.

Vous vous doutez bien que tout cela est sans fin !
En fait, à force de faire trop d'inclusions, le préprocesseur s'arrêtera en disant « J'en ai marre des inclusions ! » ce qui fera planter votre compilation.

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 :

#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 include, prototypes, define...) */

#endif

Vous mettrez en fait tout le contenu de votre fichier.h(à savoir vos autresinclude, vos prototypes, vosdefine…) entre le#ifndefet le#endif.

Comprenez-vous bien comment ce code fonctionne ? La première fois qu'on m'a présenté cette technique, j'étais assez désorienté : je vais essayer de vous l'expliquer.

Imaginez que le fichier.hest inclus pour la première fois. Le préprocesseur lit la condition « Si la constanteDEF_NOMDUFICHIERn'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 entre à l'intérieur duif.

La première instruction qu'il rencontre est justement :

#define DEF_NOMDUFICHIER

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 inclus à nouveau.

Bien entendu, vous appelez votre constante comme vous voulez. Moi, je l'appelleDEF_NOMDUFICHIERpar habitude.

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.hdifférent. Il ne faut pas que ça soit la même constante pour tous les fichiers.h, sinon seul le premier fichier.hserait lu et pas les autres !
Vous remplacerez doncNOMDUFICHIERpar le nom de votre fichier.h.

En résumé

  • Le préprocesseur est un programme qui analyse votre code source et y effectue des modifications avant la compilation.

  • L'instruction de préprocesseur#includeinsère le contenu d'un autre fichier.

  • L'instruction#definedéfinit une constante de préprocesseur. Elle permet de remplacer un mot-clé par une valeur dans le code source.

  • Les macros sont des morceaux de code tout prêts définis à l'aide d'un#define. Ils peuvent accepter des paramètres.

  • Il est possible d'écrire des conditions en langage préprocesseur pour choisir ce qui sera compilé. On utilise notamment les mots-clés#if,#elifet#endif.

  • Pour éviter qu'un fichier.hne soit inclus un nombre infini de fois, on le protège à l'aide d'une combinaison de constantes de préprocesseur et de conditions. Tous vos futurs fichiers.hdevront être protégés de cette manière.

Exemple de certificat de réussite
Exemple de certificat de réussite