Apprenez à programmer en C !
Last updated on Thursday, September 25, 2014
  • 4 semaines
  • Moyen

Ce cours est visible gratuitement en ligne.

Paperback available in this course

Ce cours existe en eBook.

Certificate of achievement available at the end this course

Got it!

Le préprocesseur

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 .h comme les fichiers .h des bibliothèques (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 < > :

#include <stdlib.h>

Pour inclure un fichier .h se 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 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 le schéma de la fig. suivante.

Inclusion de fichier

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 :

#include "fichier.h"

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

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

Et dans le fichier.h :

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

Lorsque le préprocesseur passe par là, juste avant la compilation de fichier.c, il insère fichier.h dans fichier.c. Au final, le code source de fichier.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 .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.
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.

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 #define remplace 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 chaque NOMBRE_VIES_INITIALES par 3.

Voici un exemple de fichier .c avant 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 #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 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 #define puis de recompiler.

À 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 bibliothèques 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 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.

Un define pour la taille des tableaux

On utilise souvent les define pour 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, mais TAILLE_MAX n'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éfinissant TAILLE_MAX ainsi, 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 de char prendront tous la nouvelle taille que vous aurez indiquée.

Calculs dans les define

Il est possible de faire quelques petits calculs dans les define.
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) :

#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 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 mot CONSTANTE est 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 #define on pouvait demander au préprocesseur de remplacer un mot par une valeur. Par exemple :

#define NOMBRE 9

… 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… un 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 :

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

Ce qui change ici, ce sont 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 :

#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 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 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 nomme age. Dans tout notre code de macro, age sera 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 » 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 :

#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é #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 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é précédemment :

#define CONSTANTE

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 :

#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 WINDOWS en 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 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 ».

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

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à que ça commence à coincer, supposez que le fichier B.h contienne à son tour un include du fichier A.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 lit A.h et voit qu'il faut inclure B.h ;

  2. il lit B.h pour l'inclure, et là il voit qu'il faut inclure A.h ;

  3. il inclut donc A.h dans B.h, mais dans A.h on lui indique qu'il doit inclure B.h !

  4. rebelote, il va voir B.h et voit à nouveau qu'il faut inclure A.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 autres include, vos prototypes, vos define…) entre le #ifndef et 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 .h est inclus pour la première fois. Le préprocesseur 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 entre à l'intérieur du if.

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'appelle DEF_NOMDUFICHIER par 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 .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.

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 #include insère le contenu d'un autre fichier.

  • L'instruction #define dé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, #elif et #endif.

  • Pour éviter qu'un fichier .h ne 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 .h devront être protégés de cette manière.

Example of certificate of achievement
Example of certificate of achievement