Tutoriel sur le preprocesseur C, a quoi sert-il, commet on s'en sert ...
Le Préprocesseur CPar Godrik (godrik-article@mandragor.org) Vous devez vous demander pourquoi j'écris sur le préprocesseur. Tout simplement parceque énormément de programmeur "amateur" le connaissent mal et très peu le connaissent bien.
A quoi ca sert?Le préprocesseur est appelé par les compilateurs C avant la phase de compilation. Il sert à avoir du code plus simple à lire. Par exemple il permet d'inclure un fichier dans un autre.
Il faut quand même noter que l'on peut appeler le préprocesseur C sans compiler (sous linux: gcc -E ou encore cpp); ce qui peut avoir des avantages dans beaucoup de cas.
Voyons maintenant ce qu'il nous permet de faire!
GénéralitéLes lignes sont analysées les unes après les autres. Si une ligne commence par un # alors c'est une directive du préprocesseur. Si une directive fini par un alors le
est "oublié" et la directive se poursuit sur la ligne d'après. La norme du C inclut la norme du préprocesseur[0]
MacrosUne macro est un mot qui est reconnu par le préprocesseur. Elle peut ou non prendre des arguments. Lorsqu'elle est recontré dans une ligne, la macro est developpé.
Par convention, les macros se voient toujours nommées en majuscule.
La directive utilisée pour developé une macro est : #define suivit du nom de la macro (appelé identificateur) puis des arguments et du code en lui même. Ceci donne:
#
//oui, la directive vide existe.
#define TOTO
//par la suite la macro TOTO sera definit
#define TOTO 1
//desormais toute occurence de TOTO sera remplace par 1
//Mais on peut aussi faire
#define MAX(X, Y) (X>Y?X:Y)
//Rq: il ne faut pas d'espace entre MAX et la premiere parenthèse
//desormais un appel a MAX (2, 3) sera remplace par (2>3?2:3)
Commentons ensemble cette dernière ligne... Nous voyons que c'est très puissant, cela nous permet de faire des pseudo-fonctions qui sont souvent utilisées et simplifient l'écriture. Il est à noter qu'il faut faire très attention lorsqu'on les utilise.
Tout d'abord, il faut bien comprendre que le préprocesseur ne fait aucune vérification de type; ainsi un appel à MAX(2, "toto") ne posera aucun problème au préprocesseur. Mais il faut aussi remarquer que chaque paramètre est évalué plusieurs fois. Demonstration:
#define MAX(X, Y) (X>Y?X:Y)
MAX (2, 3) //(2>3?2:3)
MAX(x++, y++) //(x++>y++?x++:y++)
//ceci n'est tres probablement pas l'effet voulu!
//un MAX (10000, "toto") ne donnera pas le meme resultat a chaque execution car,
//le compilateur C utilisera l'adresse de "toto" qui est definit a l'edition de lien
//Considerons une autre macro et voyons ses faiblesses
#define CARRE(x) x*x
Comme vous l'aurez remarqué le préprocesseur n'évalue pas ses arguments; c'est ce qui pose problème avec le x++. Mais ce n'est pas le seul problème, des problèmes liées à la priorité des opérations peuvent se poser. C'est pour cela qu'il faut forcer le préprocesseur à les évaluer indépendament. Voyons :
#define CARRE(x) x*x
CARRE(3+4) //3+4*3+4 => 3+12+4
#define CARRE(x) ((x)*(x))
CARRE(3+4) //((3+4)*(3+4)) => 7*7
Une dernière chose dont je n'ai pas encore parlé est l'appel de fonction dans une macros. En effet, la fonction sera appelée plusieurs fois.
Un oubli... Il est possible d'annuler la reconnaissance d'un identificateur pas le préprocesseur en utilisant la directive #undef suivie de l'identificateur.
Pour conclure sur les macros, il faut être très prudent lorsque l'on écrit des macros. De nombreux développeurs font encore ce genre d'erreur qui leur coûtent un temps précieux. En règle géneral, préferez les appels de fonctions au macro quand la vitesse n'est pas une de vos préoccupations. En C++ l'utilisation de fonction inline cumule les avantages des fonctions et ceux des macros; Comme qui dirait: "les fonctions inline: c'est bon; mangez-en".
Inclusion de fichierLe préprocesseur C nous permet d'inclure un fichier dans le fichier courant. Ceci s'avère très pratique lors de l'écriture de programme de taille conséquente.
Souvent l'édition de lien nous permet de séparer notre programme en plusieurs fichier, mais pour faire des appels d'un fichier à l'autre il faut que les deux objets définissent les mêmes symboles (en C). C'est pourquoi on utilise l'inclusion de fichier contenant les prototypes de nos fonctions dans des fichier d'en-tête.
--fichier main.c--
#include "fnct.h"
int main ()
{
return fnct();
}
--fichier fnct.h--
int fnct ();
--fichier fnct.c--
#include "fnct.h"
int fnct ()
{
return -1;
}
Le code ci-dessus me semble suffisement éloquent pour que je ne le commente pas. J'entends deja la question: "Pourquoi des fois on ecrit #include et d'autre fois #include "fnct.h"? ". La norme du préprocesseur definit les deux et nous dit que la forme <> recherche le fichier dans une suite de répertoire dépendant de l'implémentation; tandis que la forme "" la recherche d'abord dans le fichier source original associé (la norme le dit comme ca, c'est pas moi qui complique.)
Pratiquement parlant, la plupart des compilateurs (en fait j'en ai jamais vu qui ne respectait pas ca) definisent une suite de répertoire où l'on peut trouver les fichier d'en-tête dit "système" où l'on met généralement les fichiers d'en-têtes des bibliothèques installées sur la machine. Tandis que la forme en "" désigne le répertoire où se trouve le fichier source que l'on est en train de compiler. Avec GCC c'est la variable INCLUDE_DIR qui fait foi; on peut toutefois le modifier à la compilation à l'aide de l'option -I.
Compilation conditionelleQuel art que faire de la compilation conditionelle! Elle permet en effet de regler énormement de problème. L'un des plus courant étant la gestion des différents OS supportés par le logiciel; elle permet aussi de gerer différente bibliothèque ou encore sert a simplifier le développement...
La compilation conditionelle se fait a l'aide de la directive #if par rapport à une valeur booléene (C'est fou ca, c'est une mode ?:) ). Tout les operateurs "classiques" du C existent, mais il en existe un propre au préprocesseur qui est defined et qui permet de savoir si un identificateur a été défini plus tôt dans le fichier.
Voyons tout de suite un exemple:
#if 1>0
//ceci est toujours ecrit
#else
//jamais ecrit
#endif
Mais on peut aussi utiliser la compilation conditionelle pour s'assurer de la version d'un logiciel. Par exemple:
#if WINVER >= 5
//ici j'ai le droit au option de windows 2000
#elif WINVER >= 4
//ici j'ai le droit au option de NT 4
#else
//ici a priori just a celle de NT 3.51 et +
#endif
En C, on utilise souvent la compilation conditionelle pour s'assurer de la non multi-inclusion d'un fichier. Comme suis:
--fichier fonction.h--
#ifndef __FONCTION_H__
//#ifndef est un alias de #if !(defined (__FONCTION_H__))
#define __FONCTION_H__
/*En definissant l'identificateur dans le fichier d'en-tête.
On peut savoir si le fichier a deja ete inclu ou non*/
/*toute les lignes du fichier d'en-tête*/
#endif
--fin fonction.h--
Chaine de caractereLe préprocesseur C propose deux opérateurs qui servent à manipuler des chaines de caractères. Le premier est # et est un opérateur de conversion de parametre effectif en chaine de caractere:
#define AFF(x) printf ("%s: %d
", #x, x);
int toto = 2;
AFF(toto);
/*cette ligne est equivalente a
printf ("%s: %d
", "toto", toto);*/
/*faisons remarque que le C concatene automatiquement
les chaines de caractere. On peut donc ecrire*/
#define AFF(x) printf ("%s%d
", #x ": ", x);
/*Ce qui produira exactement le meme resultat au final.
NB: je ne sais pas si c'est la compilateur ou le préprocesseur
qui fait cela. Des informations ?*/
Le dernier opérateur concerne les chaines de caractères est l'opérateur ## qui sert à concatener plusieurs paramètres ensemble.
#define CONCAT(x, y) x##y
printf ("%s", CONCAT (toto, tata));
/*equivalent a printf ("%s", "tototata");*/
AutreLe préprocesseur permet d'utiliser des constantes touours valides à toute les lignes: __LINE__: le numero de la ligne courante; __FILE__: le nom du fichier en cours de compilation (attention, pas les en-têtes); __DATE__ : la date de compilation; __TIME__ l'heure de compilation; et __STDC__ qui vaut un et qui n'est definit que dans les implémenations conformes à la norme C.
Le préprocesseur permet égalment d'utiliser #error qui arrêtera la compilation en affichant le message d'erreur qui suit. #warning quant à lui affiche un message sans stopper la compilation.
On pourra égalment utiliser #line qui permet de changer le numero de la ligne suivante de compilation (ce qui peut être utile dans des applications de debuggage). A la suite du numero de ligne on peut également spécifier un nom de fichier qui se substituera pour la ligne suivante au nom de fichier courant.
On peut configurer le préprocesseur en cours de compilation à l'aide de la directive #pragma. Cette directive est entierement laissé à la discrétion de l'implémentation([1] et [2]).
ConclusionsPour conclure ce tutoriel; je dirais que le préprocesseur est très simple mais très puissant, l'utilisation de ses options peut grandement simplifier le dévelopement au quotidien.
Il ne faut pas avoir peur de l'utiliser mais toujours se rappeler qu'après compilation, il n'y à plus aucune trace des directives et qu'il est parfois préférable d'utiliser des astuces du langages plutot que le préprocesseur.
Référence[0]: "Le langage C, Norme Ansi", "Brian W.Kernighan et Denis M. Ritchie". Plus particulierement 4.11 et A.12