Année universitaire 2002-2003 |
Licence d'informatique
|
Le "préprocesseur" est un programme qui effectue un pré-traitement, lors de la compilation d'un programme C, en supprimant dans un premier temps tous les commentaires, puis en traitant les "directives de compilation". Une fois cette étape préalable franchie, il envoie le programme C modifié au compilateur. Les directives de compilation, dans un programme C, commencent toutes par un caractère #. Elles ne se trouvent pas forcément en début de fichier. Les trois principaux types de directives sont :
Certaines de ces directives ont déjà été utilisées lors des séances de travaux dirigés ou de travaux pratiques.
#include <nom_fichier>
#include <fichier.h>
Cette directive réalise l'inclusion du fichier fichier.h contenu dans un répertoire spécial (dans le cas des travaux pratiques, ce répertoire est /usr/include).
#include "nom_fichier"
#include "fichier.h"
Cette directive réalise l'inclusion du fichier fichier.h contenu dans le répertoire courant ou, à défaut, dans /usr/include. Il est également possible d'indiquer un chemin précis pour la recherche d'un fichier, soit par exemple ici : #include "/usr/include/fichier.h"
L'extension .h d'un fichier est l'abréviation de "header" (qui signifie "en-tête"). Il n'est pas obligatoire qu'un fichier possède cette extension pour pouvoir être inclus dans un programme C.
#if comparaison1
...
#elif comparaison2
...
#else
...
#endif
... #define TEST 20 ... #if TEST > 5 printf("OK\n"); #elif TEST <= 0 scanf("%d",&a); #else scanf("%d",&b); #endif ... |
Dans cet exemple, en fonction de la valeur de la constante TEST,
ce sera soit l'instruction printf("OK\n"); soit
l'instruction scanf("%d",&a); soit l'instruction
scanf("%d",&b); qui sera compilée.
Dans le cas présent, le fragment de programme envoyé au compilateur
par le préprocesseur sera le suivant :
... printf("OK\n"); ... |
Les comparaisons effectuées par le préprocesseur ne peuvent porter que sur des constantes entières (et pas sur des variables du programme, dont l'évaluation n'est possible qu'à l'exécution...).
#ifdef symbole
...
#endif
... #define PARTIE_1 ... #ifdef PARTIE_1 printf("OK\n"); #endif #ifdef PARTIE_2 scanf("%d",&a); #endif ... |
Certains tronçons de programmes peuvent n'être compilés qu'en fonction de l'existence de la définition de certains symboles (ou "constantes symboliques"). Dans cet exemple, le fragment de programme envoyé au compilateur par le préprocesseur sera le suivant :
... printf("OK\n"); ... |
... #define DEBOGAGE ... #ifdef DEBOGAGE fprintf(stderr,"Ligne 1234 : a==%d - b==%d - c==%d\n",a,b,c); #endif ... #ifdef DEBOGAGE fprintf(stderr,"Ligne 2345 : a==%d - b==%d - c==%d\n",a,b,c); #endif ... |
Les directives de compilation conditionnelle sont particulièrement utiles pour le "débogage" d'un programme C. Dans l'exemple 3, la "trace" (on appelle ainsi l'affichage de la valeur des variables dans les principales étapes d'un programme) n'est effective que si le symbole DEBOGAGE est défini. La suppression de la définition de ce symbole dans le programme produira la disparition de ces affichages après la prochaine compilation.
#ifndef symbole
...
#endif
La directive #ifndef a le rôle inverse de #ifdef
Le lecteur pourra revenir à la correction de l'exercice 3 de la deuxième séance de travaux dirigés, contenue dans le fichier td02ex03.c. Il y trouvera une directive de compilation conditionnelle qui permet de compiler, au choix, l'une ou l'autre des deux solutions proposées.
#define symbole équivalent
...
#undef symbole
Le préprocesseur remplace dans un programme C toutes les occurrences du symbole par son équivalent (éventuellement vide, comme cela a été vu dans le paragraphe précédent), excepté :
Le préprocesseur tient à jour une "table des symboles", et réalise les substitutions sur le texte, de manière séquentielle, à l'aide de cette table. À chaque fois qu'il rencontre une directive de substitution symbolique (c'est-à-dire une ligne commençant par #define ou #undef), il met à jour la table des symboles.
#define PI 3.14159
#define FAUX 0
#define VRAI 1/* Ligne a. */
...
#undef PI
#define FAUX 1
#define VRAI 0/* Ligne b. */
Symbole |
Équivalent |
Symbole |
Équivalent |
Le symbole doit être composé seulement de lettres, de chiffres et du caractère _ et ne peut pas commencer par un chiffre (on préfère souvent n'utiliser comme lettres que des majuscules, bien que ceci n'ait aucun caractère obligatoire). L'équivalent doit être tapé sur une seule ligne. Si l'écriture de l'équivalent nécessite plusieurs lignes, il faut faire précéder la frappe de chaque caractère retour-chariot d'un caractère \
Comment faire pour ne pas modifier la séquence suivante, et la rendre néanmoins acceptable par un compilateur C (cette séquence n'est ni de l'Ada, ni du Pascal, ni du C) :
... if (i>0) then begin a=0; b=1 end ... |
Il existe une forme paramétrée pour la substitution symbolique.
#define symbole(paramètre1,paramètre2,...) équivalent
Les paramètres situés entre parenthèses qui suivent une occurrence de symbole dans le programme, sont identifiés par le préprocesseur à paramètre1, paramètre2, etc... L'équivalent est envoyé au compilateur par le préprocesseur, avec la même substitution des paramètres. On appelle cela une "macro-instruction" (ou "macro").
Soit la macro-instruction suivante :
#define ABS(n) ((n>0)?n:-n) |
La séquence :
int main(void) { int a,b=-8; a=ABS(b); /* Ligne 4. */ printf("%d\n",a); exit(0); } |
sera transformée par le préprocesseur en :
int main(void) { int a,b=-8; a=((b>0)?b:-b); printf("%d\n",a); exit(0); } |
À l'exécution, la valeur 8 s'affichera à l'écran.
Donner la valeur affichée à l'écran quand on remplace, dans l'exemple précédent, la Ligne 4 du programme principal par :
Donner la cause de l'erreur (ou des erreurs) lors de la compilation des deux programmes suivants :
|
#define N 4 void config_init(int mat[],int N) { } int main(void) { } |
|
#include <stdlib.h> #define MIN(a,b) (((a)>(b))?(b):(a)) #define PLUS(a,b) a+b #define MOINS(a,b) a-b #define INCR (a) a++ int main(void) { } |
... #define MESSAGE(n,ch) {int i;\ for (i=0;i<n;i++) printf("%s\n",ch);\ } ... |
L'emploi des macro-instructions doit faire l'objet d'une attention particulière.
Pour éviter de nombreux problèmes (dus aux priorités
des opérateurs), il est conseillé de parenthéser les
paramètres de la macro-instruction. Il faut, de plus, éviter
de rendre les programmes incompréhensibles par l'abus de directives
#define
Une macro-instruction permet d'optimiser le code compilé,
en limitant le nombre d'appels à une fonction dans le programme
exécutable. De plus, elle permet d'effectuer des actions sur des
variables dont le type n'est pas connu a priori (dans l'exemple du paragraphe
6, la macro-instruction ABS peut calculer la valeur absolue
d'un entier ou d'un flottant).
1) Écrire les macro-instructions suivantes, qui sont effectivement définies, sous d'autres noms, dans le fichier /usr/include/ctype.h :
a) CHIFFRE(c) : teste si une variable c de type char contient un chiffre.
b) MINUSCULE(c) : teste si une variable c de type char contient une minuscule.
c) MAJUSCULE(c) : teste si une variable c de type char contient une majuscule.
d) MIN_MAJ(c) : convertit une variable c de type char en majuscule, si c'est une minuscule.
e) MAJ_MIN(c) : convertit une variable c de type char en minuscule, si c'est une majuscule.
f) PERMUTE(a,b) : permute les valeurs de deux variables entières a et b.
g) COPIE(s,t) : recopie une chaîne de caractères s dans une autre chaîne de caractères t (on suppose que les espaces mémoire où sont logées la chaîne et sa copie sont correctement réservés).
2) Donner la ligne produite par le préprocesseur, en remplacement de la ligne suivante :
a=MIN_MAJ(a);
Qu'affichent les trois programmes suivants :
|
#include <stdio.h> #include <stdlib.h> #define F(a,b) (a)+(b) #define C1 x #define C2 y int main(void) { } |
|
#include <stdio.h> #include <stdlib.h> #define bA a #define AB b #define aB a int main(void) { int a=1; int b=2; int bbB=3; int ABABB=4; printf("%d\n",ABABB); exit(0); } |
|
#include <stdio.h> #include <stdlib.h> #define C1 0 #define C2 C1 #define C1 1 int main(void) { } |