next up previous contents index
Next: 10 Programmation avancée Up: Le langage C Previous: 8 Entrées-sorties

Subsections


9 Préprocesseur

    Comme nous l'avons déjà dit, la transformation d'un texte source en un programme exécutable se fait en plusieurs étapes (voir figure 9.1).

La phase de transformation d'un fichier source en fichier objet se décompose en deux phases (figure 9.2):

Les directives   du préprocesseur ne font par partie du langage C à proprement parlé. Celles-ci se distinguent des instructions du langage C par le fait qu'elles commencent toujours par le caractère #.

9.1 La directive include

    On a déjà rencontré une première directive dans la section 1.2: il s'agit de la directive #inlude. La directive #include <stdio.h> demande au préprocesseur d'inclure le fichier stdio.h dans le fichier source avant de le compiler. Ce fichier stdio.h contient toutes les déclarations nécessaires pour l'utilisation des fonctions entrée-sorties.

Comme vous l'avez sûrement déjà noté, les fichiers include ont traditionellement l'extension .h[*]: ce sont les fichiers d'interface ou header files. Le fichier stdio.h et bien d'autres encore sont livrés avec le compilateur C. Vous trouverez généralement tous ces fichiers dans le répertoire /usr/include. Ces fichiers sont éditables et il est très vivement déconseillé de les modifier.

En dehors des fichiers fournis avec le compilateur, un programmeur peut créer ses propres fichiers d'interface pour les programmes qu'il réalise. Supposons que l'on veuille réaliser un programme qui utilise des piles de caractères. On décompose ce programme en deux modules (ou plusieurs), l'un qui implante les piles de caractères et l'autre qui les uitlise. Le programmeur qui réalise le module d'implantation des piles founit au(x) programmeur(s) qui réalisent le reste du programme deux fichiers: le premier contenant l'implantation effective des piles et la deuxième une interface pour ce module.

 
/* Interface du module PILE de caracteres (pile.h) */

typedef char * pile; pile creer_pile(int); void empiler(char, pile); char depiler(pile); void detruire_pile(pile); /* Fin de l'interface */

Les utilisateurs de ce module ignorent parfaitement les détails d'implantation de cette pile. En particulier, s'agit-il d'un tableau ou d'une liste chainée ? Seul l'implanteur de ce module le sait. L'utilisation de ce module par d'autres se fait alors de la manière suivante:
 
/* Module XXX utilisant une pile de caractères */

#include "pile.h"

void une_fonction(void) { short c1, c2; pile P; P = creer_pile(100); empiler('a', P); empiler('b', P); ... c1 = depiler(P); if (c1 == EOF) erreur("la pile est vide"); c2 = depiler(P); if (c2 == EOF) erreur("la pile est vide"); ... } /* Fin du module XXX */

Le préprocesseur, selon que la directive est de la forme #include <fichier> ou de la forme #include "fichier" ira chercher ce fichier dans le répertoire /usr/include ou dans le répertoire spécifié par l'utilisateur.

9.2 Les macros et constantes symboliques

        En dehors des variables et fonctions qui possèdent un nom symbolique, il est possible d'associer des noms symboliques à des constantes et des portions de code C. Ces noms symboliques n'ont d'intérêt que pour le programmeur: ils sont censés rendre le programme plus lisible et plus facile à maintenir.

9.2.1 Les constantes symboliques

Les constantes symboliques se définissent à l'aide de la directive

#define Nom constante

Le Nom devient alors un nom symbolique qui peut être utilisé partout où l'on veut faire figurer la constante. Le préprocesseur se contente de substituer syntaxiquement le nom symbolique par la constante avant de compiler.

Par exemple, lorsqu'on définit un tableau de 20 éléments par exemple, on pourrait avoir le code suivant:

 
int tab[20];
main() {
    ...
    for (i=0; i<20; i++)
        printf("tab[%i]=%d, tab[i]);
    ...
}
On notera que la valeur 20 se trouve éparpillé dans tout le code du programme. Un manière élégante de regrouper cette constante en un seul endroit consiste à définir une constante symbolique MAX_TAB et utiliser celle-ci dans tout le programme.
 
#define MAX_TAB 20
int tab[MAX_TAB];

main() { ... for (i=0; i < MAX_TAB; i++) printf("tab[%i]=%d, tab[i]); ... }

Ainsi, si l'on veut modifier la taille du tableau, on se contentera de modifier la seule définition de la constante symbolique MAX_TAB et le reste programme reste inchangé.

9.2.2 Les constantes symboliques sans valeur

On peut également définir des constantes symboliques sans valeur comme :

 
#define UNIX 
Le texte à remplacer étant vide, le préprocesseur se contentera de remplacer toutes les occurences de UNIX par la chaîne vide. Quoiqu'étant remplacé par ``rien'', cette constante sybolique est considéré définie par le préprocesseur. Ce type de constantes sont utilisées avec les directives conditionnelles, comme nous le verrons plus loin.

9.2.3 Supprimer la définition d'une constante symbolique

Il est également possible de supprimer la définition d'un nom symbolique par la directive

 
#undef UNIX

9.2.4 Les macros sans paramètres

Comme les constantes symboliques, les macros se définissent à l'aide de la directive

#define Nom <texte à remplacer>

Le Nom devient alors un nom symbolique qui peut être utilisé partout où l'on veut faire figurer la texte à remplacer. Le préprocesseur se contente de substituer syntaxiquement le nom symbolique par le texte à remplacer avant de compiler.
 
#define BONJOUR printf("Bonjour, tout le monde !!!");

9.2.5 Les macros avec plusieurs instructions

Puisque la directive define ne sert qu'à substituer un nom par le texte défini, rien ne s'oppose à donner un nom symbolique à une suite d'instructions ou tout partie de programme valide.

 
#define ERREUR_MALLOC  { printf("La fonction malloc a produit une code d'erreur"); exit(0); }
Comme vous l'avez remarqué, tout le texte à remplacer se trouve sur une même ligne. Et c'est là une restriction (désagréable, j'en conviens) de la directive define. Cette restriction étant inappliquable, on contourne celle-ci avec les fameux caractère \ comme nous l'avions fait pour les chaînes de caractères (voir 1.5.4).
#define ERREUR_MALLOC  {                                      \
    printf("La fonction malloc a produit une code d'erreur"); \
    exit(0);                                                  \
}

9.2.6 Les macros avec paramètres

Comme les fonction, les macros peuvent avoir des paramètres.

#define Nom(arg1, ..., argn) texte_à_remplacer

Attention, il ne s'agit pas de fonction, ce ne sont que des macros.

#define ERREUR(x)  {                                     \
    printf("La fonction x a produit une code d'erreur"); \
    exit(0);                                             \
}

9.2.7 Concaténation de mots (token)

  Il existe un opérateur qui permet de concaténer des mots: il s'agit de ##.
 
#define coller(x, y)  x##y

main() { int coller(toto, 5); équivalent à la déclaration int toto5; }

9.3 Compilation conditionnelle

          Il existe des directives permettant, selon un critère que l'on définira, de compiler de manière conditionnelle un programme. Supposons que l'on veuille réaliser un programme qui doit fonctionner sur un PC, une station SUN ou une machine ALPHE de DIGITAL. Il est vraisemblable que des parties du programme devront être réécrite suivant l'architecture de la machine choisie. Il est tout aussi vraisemblable, que des grands bouts de ce programme (écrit en C) seront communes aux divers machines.

La compilation conditionelle va permettre de réaliser un seul code contenant l'ensemble des codes nécessaire à toutes les machines et en spécifiant au compilateur, selon la machine choisie, quelle partie du code il faudra utiliser.

Pour ce faire, on dispose des directives

 
#if
#elif
#else
#endif
#ifdef
#indef
Voici des bouts de programmes trouvés dans le programme xcoral:
 
...
#ifdef SYSV
#include 
extern void bzero();
#else
#include 
#endif

... #ifdef USE_NANOSLEEP struct timespec rqtp; ... #elif USE_POLL struct pollfd unused; ... #else /* default to select */ struct timeval delay; ... #endif /* USE_NANOSLEEP */

Contrairement à la directive #ifdef qui est toujours associée à une seule constante symbolique, la directive #if peut être combinée avec une expression à condition qu'elle soit constante entière.

Voici quelques exemples trouvés dans les sources de Linux.

 
#if EVERY_ACCESS || ERRORS_ONLY || DEBUG_ABORT
   ...
#endif

#if ((~0UL) == 0xffffffff) ... #else ... #endif

#if (((DEBUG & PHASE_ETC) == PHASE_ETC) || (DEBUG & PRINT_COMMAND) ) ... #enif

9.3.1 L'opérateur defined

 

La directive #if peut être combinée avec l'opérateur defined. Le code

 
#if defined(CONSTANTE)
    ...
#endif
est équivalent au code
 
#ifdef (CONSTANTE)
    ...
#endif
L'intérêt de cet opérateur que l'utilisation de la directive #if (au lieu de #ifdef) permet d'utiliser des expressions complexes.
 
#if defined(MODULE) && !defined(GFP_DMA)
    ...
#endif

9.3.2 Numérotation des lignes

  La directive #line initialise des lignes du fichier source. C'est ainsi que le compilateur peut afficher les numéros de ligne d'une erreur; n'oublions pas que le fichier passé au compilateur est différent du fichier source de l'utilisateur à cause de la phase du préprocesseur. C'est d'ailleurs ce préprocesseur qui ajoute automatiquement les directives #line pour que le compilateur puisse afficher correctement les messages d'erreurs.
 
#line n "fichier"


next up previous contents index
Next: 10 Programmation avancée Up: Le langage C Previous: 8 Entrées-sorties

Touraivane
9/21/1998