16 Préprocesseur¶
Fig. 16.1 Illustration du mécanisme de pré-processing avant la compilation¶
Comme nous l'avons vu en introduction (c.f. Section 1.6), le langage C est basé sur une double grammaire, c'est-à-dire qu'avant la compilation du code, un autre processus est appelé visant à préparer le code source avant la compilation.
Le coeur de cette opération est appelé préprocesseur. Les instructions du préprocesseur C sont faciles à reconnaître, car elles débutent toutes par le croisillon (#
), hash en anglais et utilisé récemment comme hashtag sur les réseaux sociaux. Notons au passage que ce caractère était historiquement utilisé par les Anglais sous le dénominatif pound (livre). Lorsqu'il est apparu en Europe, il a été confondu avec le caractère dièse (♯
) présent sur les pavés numériques de téléphone.
Le vocabulaire du préprocesseur est le suivant :
#include
Inclus un fichier dans le fichier courant
#define
Crée une définition (Macro)
#undef
Détruis une définition existante
#if defined
Teste si une définition existe
#if
..#endif
Test conditionnel (similaire à l'instruction
if
du langage C)#
Opérateur de conversion en chaîne de caractères
##
Opérateur de concaténation de chaînes
#error "error message"
Génère une erreur
#pragma
Directive spécifique au compilateur.
Le préprocesseur C est indépendant du langage C, c'est-à-dire qu'il peut être exécuté sur n'importe quel type de fichier. Prenons l'exemple d'une lettre générique d'un cabinet dentaire :
#ifdef FEMALE
# define NAME Madame
#else
# define NAME Monsieur
#endif
Bonjour NAME,
Veuillez noter votre prochain rendez-vous le DATE, à HOUR heure.
Veuillez agréer, NAME, nos meilleures salutations,
#ifdef IS_BOSS
Le directeur
#elif defined IS_ASSISTANT
La secrétaire du directeur
#elif defined OWNER_NAME
OWNER_NAME
#else
# error "Lettre sans signature"
#endif
Il est possible d'appeler le préprocesseur directement avec l'option -E
. Des directives define
peuvent être renseignées depuis la ligne de commande :
$ gcc -xc -E test.txt \
-DDATE=22 -DHOUR=9:00 \
-DFEMALE \
-DOWNER_NAME="Adam" -DPOSITION=employee
# 1 "test.txt"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 1 "<command-line>" 2
# 1 "test.txt"
Bonjour Madame,
Veuillez noter votre prochain rendez-vous le 22, à 9:00 heure.
Veuillez agréer, Madame, nos meilleures salutations,
Adam
Notez que les instructions du préprocesseur (à l'exception des opérateurs de concaténation de conversion en chaîne de caractère) sont des instructions de ligne (line-wise), et doivent se terminer par un caractère de fin de ligne.
16.1 Phases de traduction¶
Le standard décrit 4 phases de pré-processing :
Remplacement des caractères spéciaux, décodage des trigraphes, traitement des fin de lignes.
Fusionne les lignes utilisant un retour virtuel
\
.Supprime les commentaires, décompose les symboles du préprocesseur
Exécute les directives du préprocesseur (
#define
et#include
)
16.2 Extensions des fichiers¶
Par convention, et selon le standard GNU, les extensions suivantes sont en vigueur :
.h
Fichier d'en-tête ne comportant que des définitions préprocesseur, des déclarations (structures, unions ...) et des prototypes de fonction, mais aucun code exécutable. Ce fichier sera soumis au préprocesseur.
.c
Fichier source C comportant les implémentations de fonction et les variables globales. Ce fichier sera soumis au préprocesseur.
.i
Fichier source C qui ne sera pas soumis au préprocesseur:
gcc -E -o foo.i foo.c
.s
Fichier assembleur non soumis au préprocesseur.
.S
Fichier assembleur soumis au préprocesseur. Notons toutefois que cette convention n'est pas applicable sous Windows, car le système de fichier n'est pas sensible à la casse.
16.3 Inclusion de fichiers¶
16.3.1 #include¶
La directive include peut prendre deux formes, l'inclusion locale et l'inclusion globale. Il s'agit d'ailleurs de l'une des questions les plus posées (c.f. cette question.).
#include <filename>
Le préprocesseur va chercher le chemin du fichier à inclure dans les chemins de l'implémentation.
#include "filename"
Le préprocesseur cherche le chemin du fichier à partir du chemin courant et les chemins donnés par les des directives
-I
.
L'inclusion de fichier est simplement du remplacement de chaînes :
$ echo "Ce début de phrase est ici" > head.h
$ echo ", mais cette fin est là." > tail.h
$ echo -e '#include "head.h"\n#include "tail.h"\n' > main.c
$ gcc -E main.c -o-
# 1 "main.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 31 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 32 "<command-line>" 2
# 1 "main.c"
# 1 "head.h" 1
Ce début de phrase est ici
# 2 "main.c" 2
# 1 "tail.h" 1
, mais cette fin est là.
# 3 "main.c" 2
La directive #include
est principalement utilisée pour inclure des fichiers d'en-tête (header), mais rarement (jamais), des fichiers C.
16.4 Définitions¶
16.4.1 #define¶
Les définitions sont des symboles généralement écrits en majuscule et qui sont remplacés par le préprocesseur. Ces définitions peuvent être utiles pour définir des constantes globales qui sont définies à la compilation :
#ifndef WINDOW_SIZE
# define WINDOW_SIZE 10
#endif
int tab[WINDOW_SIZE];
void init(void) {
for(size_t i = 0; i < WINDOW_SIZE; i++)
tab[i] = i;
}
Il est ainsi possible de définir la taille du tableau à la compilation avec :
$ gcc main.c -DWINDOW_SIZE=42
Notons qu'au pré-processing, toute occurrence d'un symbole défini est remplacée par le contenu de sa définition. C'est un remplacement de chaîne bête, idiot et naïf. Il est par conséquent possible d'écrire :
#define MAIN int main(
#define BEGIN ) {
#define END return 0; }
#define EOF "\n"
MAIN
BEGIN
printf("Hello" EOF);
END
On relèvera qu'il est aussi possible de commettre certaines erreurs :
#define ADD a + b
int a = 12;
int b = 23;
int c = ADD * ADD
Après pré-processing on aura un comportement non désiré, car la multiplication est plus prioritaire que l'addition.
#define ADD a + b
int a = 12;
int b = 23;
int c = a + b * a + b
Pour se prémunir contre ces éventuelles coquilles, on protègera toujours les définitions avec des parenthèses #define ADD (a + b)
.
16.4.2 #undef¶
Un symbole défini soit par la ligne de commande -DFOO=1
, soit par la directive #define FOO 1
ne peut pas être redéfini. C'est pourquoi il est possible d'utiliser #undef
pour supprimer une directive préprocesseur :
#ifdef FOO
# undef FOO
#endif
#define FOO 1
Généralement on évitera de faire appel à #undef
car le bon programmeur aura forcé la définition d'une directive en amont pour contraindre le développement en aval.
16.5 Débogage¶
16.5.1 #error¶
Cette directive génère une erreur avec le texte qui suit la directive :
#if !(KERNEL_SIZE % 2)
# error Le noyau du filtre est pair
#endif
16.5.2 Directives spéciales¶
Le standard définit certains symboles utiles pour le débogage :
__LINE__
Est remplacé par le numéro de la ligne sur laquelle est placé ce symbole
__FILE__
Est remplacé par le nom du fichier sur lequel est placé ce symbole
__func__
Est remplacé par le nom de la fonction du bloc dans lequel la directive se trouve
__STDC__
Est remplacé par 1 pour indiquer que l'implémentation est compatible avec C90
__DATE__
Est remplacé par la date sous la forme
"Mmm dd yyyy"
__TIME__
Est remplacé par l'heure au moment du pre-processing
"hh:mm:ss"
16.6 Caractère d'échappement¶
L'anti-slash (backslash
) est interprété par le préprocesseur comme un saut de ligne virtuel. Il permet par exemple de casser les longues lignes :
#define TRACE printf("Le programme est passé " \
" dans le fichier %s" \
" ligne %d\n", \
__FILE__, __LINE__);
16.7 Macros¶
Une macro est une définition qui prend des arguments en paramètre :
#define MIN(x, y) ((x) < (y) ? (x) : (y))
De la même manière que pour les définissions simple, il s'agit d'un remplacement de chaîne :
$ cat test.c
#define MIN(x, y) ((x) < (y) ? (x) : (y))
int main(void) {
return MIN(23, 12);
}
$ gcc -E test.c -o-
int main(void) {
return ((23) < (12) ? (23) : (12));
}
Notez que l'absence d'espace entre le nom de la macro et la parenthèse est importante. L'exemple suivant le démontre :
$ cat test.c
#define ADD (x, y) ((x) + (y))
int main(void) {
return ADD(23, 12);
}
$ gcc -E test.c -o-
int main(void) {
return (x, y) ((x) + (y))(23, 12);
}
16.8 Concaténation¶
Parfois il est utile de vouloir concaténer deux symboles comme si ce n'était qu'un seul. Attention il est nécessaire de passer par une macro pour que cela fonctionne :
int foobar = 42;
#define CONCAT(a, b) a ## b
printf("%d", CONCAT(foo, bar));
En appelant seulement le préprocesseur on constate ce résultat :
$ gcc -E ww.c
int foobar = 42;
printf("%d", foobar);
16.9 Directives conditionnelles¶
Les directives #if
, #else
, #elif
et #endif
sont utiles pour rendre conditionnelle une section de code. Cela peut être utilisé pour définir une structure selon le boutisme de l'architecture cible :
#ifdef BIG_ENDIAN
typedef struct {
int header;
int body;
int tail;
} Dataframe;
#else
typedef struct {
int tail;
int body;
int header;
} Dataframe;
#endif
16.9.1 Désactivation de code¶
On voit souvent des développeurs commenter des sections de code pour le débogage. Cette pratique n'est pas recommandée, car les outils de refactoring (réusinage de code), ne parviendront pas à interpréter le code en commentaire jugeant qu'il ne s'agit pas de code, mais de texte insignifiant. Une méthode plus robuste et plus sure consiste à utiliser une directive conditionnelle :
#if 0 // TODO: Check if this code is still required.
if (x < 0) {
x = 0;
}
#endif
16.9.2 Include guard¶
La protection des fichiers d'en-tête permet d'éviter d'inclure un fichier s'il a déjà été inclus.
Imaginons que la constante M_PI
soit définie dans le header <math.h>
:
#define M_PI 3.14159265358979323846
Si ce fichier d'en-tête est inclus à nouveau, le préprocesseur générera une erreur, car le symbole est déjà défini. Pour éviter ce genre d'erreur, les fichiers d'en-tête sont protégés par un garde :
#ifndef MATH_H
#define MATH_H
...
#endif
Si le fichier a déjà été inclus, la définition MATH_H
sera déjà déclarée et le fichier d'en-tête ne sera pas ré-inclus.
On préfèrera utiliser la directive #pragma once qui est plus simple à l'usage et évite une collision de nom. Néanmoins et bien que cette directive ne soit pas standardisée par l'ISO, elle est compatible avec la très grande majorité des compilateurs C.
#pragma once
...