Tutorial Infini Software
Le pré-processeur C

Accès rapide :
   Macro-génération de code
   Inclusion de fichiers
   Compilation conditionnelle
   Quelques autres instructions

Une particularité propre aux compilateurs C est qu'ils ne travaillent pas directement sur le code source fournit par le programmeur. En fait une phase, spéciale de réécriture du programme précède toute compilation. L'utilitaire se chargeant de cette phase de pré traitement se nomme le préprocesseur.

Comme nous allons le voir, ce préprocesseur permet, entre autre, de définir des macros, de réaliser l'inclusion de fichiers (afin de réaliser une approche, peut être un peu archaïque, de la modularité) ou encore des compilations de code C conditionnelles. Nous allons donc étudier un à un chacun de ces points et quelques autres encore.

Macro-génération de code

Donc le langage C possède un moyen de réaliser de la macro-génération de code. Il est clair que cette possibilité est un atout indéniable pour ce langage. Cependant, en C, le moyen d'y parvenir peut troubler certains puristes de l'Informatique. En effet, Cette macro-génération ne peut s'effectuer que durant la phase du préprocesseur. Il apparaît donc que ce mécanisme ne fait pas partie intégrante du langage, comme cela est le cas, par exemple, en Scheme.

L'instruction #define

L'instruction du préprocesseur (et donc pas réellement C) qui permet cette possibilité se nomme #define (attention en C il y a une différence entre minuscules et majuscules). On remarquera que cette instruction commence par un dièse, comme toutes les autres instructions du préprocesseur.

Cette instruction (ou cette directive, ce terme étant aussi employé) permet de définir soit une macro constante, soit une macro paramétrée. L'exemple qui suit vous expliquera mieux les choses. Sachez juste que l'instruction doit être suivie du nom de la macro (avec éventuellement des paramètres) puis de sa valeur de remplacement, les trois parties étant juste séparées par des espaces.

Définition Exemple d'utilisation
	
	#define False 0
	#define True 1
	#define max(a,b) (a>=b?a:b)
	#define min(a,b) (a<=b?a:b)
	if (var==False) return max(var,autreVar)
	else return min(var,encoreAutreVar);
sera remplacé par
 
	if (var==0) return (var>=autreVar?var:autreVar)
	else return (var<=encoreautrevar?var:encoreautrevar);

Il ne faut pas perdre de l'esprit le fait que le programme n'est pas compilé durant le traitement du préprocesseur. Si des erreurs existent, elles ne seront pas déterminées dès lors. Il s'agit juste de réécrire le programme selon des règles que vous précisez.

Encore quelques petites remarques. Premièrement, la définition de macros est récursive max(min(a,b),c) produira ((a<=b?a:b)>=c?(a<=b?a:b):c) et non pas (min(a,b)>=c?min(a,b):c). Ensuite, la norme ANSI dit que s'il y a redéfinition de la macro, alors il y a remplacement de la définition. Certains préprocesseurs signalent tout de même la redéfinition par un petit message afin qu'il n'y ai pas eut malveillance de votre part. Certains autres préprocesseurs admettent même une pile de définition, permettant ainsi de restituer les anciennes définitions lors de l'utilisation de l'instruction que nous allons étudier maintenant (mais l'existence d'une telle pile de définitions ne fait pas partie de la norme ANSI).

L'instruction #undef

Cette instruction permet d'annuler une définition de macro. Il suffit juste de spécifier le nom de la macro à invalider et ce directement après l'instruction.

Avant le préprocesseur Après le préprocesseur
	
	#define a 10
	a + b;
	#undef a
	a + c;
		         
	10 + b;
	a + c;

Les macros prédéfinies

Il existe un certain nombre de macros qui sont prédéfinies et qui vous permettent de connaître un certain nombre de choses. Ces macros ne peuvent en aucun cas être supprimées ou altérées. Elles sont donc immuables. Le tableau suivant vous donne le nom de ces macros ainsi que leur développement.

Nom des macros Développement de la macro associée
__LINE__

Se développe en une valeur numérique associée au numéro de la ligne courante dans le code du programme.

__FILE__

Cette macro sera remplacée par le nom du fichier en cours de traitement.

__DATE__

Celle-ci se transformera en une chaîne de caractères contenant la date de traitement du fichier sous un format "Mmm jj aaaa".

__TIME__

De même, se transformera en un chaîne représentant l'heure de traitement du fichier sous le format "hh:mm:ss".

__STDC__

Cette macro n'est censée être définie uniquement que dans les implémentations respectant la norme ANSI. Si c'est le cas, elle doit de plus valoir 1.

__STDC_VERSION__

Cette macro existe depuis C99. Elle est donc non définie en C ANSI (C89). En C99 elle renverra la valeur 199901L

Faites attention : devant et derrière chaque nom se trouvent
deux caractères _ (blancs soulignés et pas des tirés -).

Inclusion de fichiers

Nous avons donc déjà vu que le préprocesseur pouvait nous permettre de définir des macros. Mais ce n'est pas tout. Une autre grande utilisation de cet utilitaire, en C, permet d'inclure des fichiers dans d'autres. Nous verrons dans le chapitre suivant que ce mécanisme permet de faire usage de la modularité. Pour l'heure, attachons nous à comprendre comment ce mécanisme fonctionne.

L'instruction #include

Donc, l'instruction du préprocesseur permettant d'inclure un fichier dans celui en cours de traitement se nomme #include. Cette instruction prend en unique paramètre le nom du fichier à include. Cette inclusion est ponctuelle : la totalité du fichier à inclure se retrouve donc entre la ligne précédente l'instruction d'inclusion et celle que la suit. Le compilateur, lui, n'aura plus qu'à traiter un gros fichier résultant de la fusion des deux fichiers initiaux. Vous n'avez pas de souci à vous faire. Si une erreur est détectée durant la phase de compilation, le compilateur sera capable de retrouver le numéro de la ligne dans le fichier initial (celui écrit par le programmeur). Voici un petit exemple d'utilisation.

 
	#include "monfichier.h";
	#include <stdxxx.h>

Ne vous bloquez pas sur la signification de l'extension h du fichier. Nous y reviendrons dans le chapitre suivant. Par contre une remarque peut être faîte dès à présent. Il vous est possible d'entourer le nom du fichier par les signes < et >, plutôt que des guillemets. La signification change alors. Dans ce cas, le fichier est cherché uniquement dans les répertoires contenants les fichiers standards fournis avec le compilateur (notez au passage que le préprocesseur y est fournit aussi avec). Dans le cas de l'exemple le fichier est cherché d'abord dans le répertoire courant dans lequel vous vous trouvez, et si le fichier n'existe pas, alors le préprocesseur ira voir du coté des répertoires standards. Avec le temps, ce choix deviendra un réflexe.

Compilation conditionnelle

Une autre grosse utilisation du préprocesseur permet de faire ce que l'on appelle de la compilation conditionnelle. C'est à dire que selon qu'une condition soit remplie ou non, le fichier à compiler sera différent. Pour réaliser de telles compilations, le préprocesseur fournie un certain nombre d'instructions. Nous allons donc regarder comment ces instructions fonctionnent.

Les instructions #if #ifdef #ifndef et #endif

Afin d'introduire une portion de code dans laquelle les données peuvent varier selon un critère particulier, il vous faut utiliser une des trois instructions commençant par #if (#if, #ifdef, #ifndef). Il vous faut aussi marquer la fin de cette zone en utilisant l'instruction #endif. Dès lors la section de code intermédiaire sera laissée telle quelle uniquement si la condition est remplie, sinon la section sera supprimée dans le fichier résultant.

La question qui vient naturellement à l'esprit est de savoir quelle instruction choisir pour introduire le bloc conditionnel. En fait, ce n'est pas très compliqué (comme le reste d'ailleurs). L'instruction #if demande en paramètre une expression constante. Notez que l'opérateur defined identifier retourne la constante 1 si la variable est définie en temps que macro, 0 dans tout autre cas. Pour les deux autres instructions, elles peuvent se définir à partir de la première. Le tableau suivant vous donne les correspondances.

if defined identifier ifdef identifier
if ! defined identifier ifndef identifier
Correspondances entre les directives #if, #ifdef et #ifndef

Notez que dans l'exemple précédent, le ! est un opérateur du préprocesseur qui renvoie 1 si son opérande et 0 et 1 dans tous les autres cas. L'exemple suivant présente une utilisation des instructions dont nous venons de parler.

Avant le préprocesseur Après le préprocesseur
 
	printf( "Ligne 1\n" );
	#ifdef Toto
	printf( "Ligne 2\n" );
	#endif
	printf( "Ligne 3\n" );
 
	printf( "Ligne 1\n" );
	printf( "Ligne 3\n" );

Les instructions #else et #elif

Deux instructions supplémentaires viennent s'ajouter aux instructions de compilation conditionnelle. Il s'agit de #else et de #elif. Dans les deux cas, l'utilité est de pouvoir spécifier du code quand le test n'est pas validé. La différence, entre les deux instructions, tient dans le fait que #elif permet de spécifier un nouveau test qui, s'il est vérifié, sélectionnera une nouvelle section de code. L'exemple suivant sera plus parlant.

Avant le préprocesseur Après le préprocesseur
 
	#define tata
	printf( "Ligne 1\n" );
	#ifdef Toto
	printf( "Ligne 2\n" );
	#elif defined tata
	printf( "Ligne 3\n" );
	#endif
	printf( "Ligne 4\n" );
 
	printf( "Ligne 1\n" );
	printf( "Ligne 3\n" );
	printf( "Ligne 4\n" );

Il peut y avoir autant de #elif que vous le souhaitez, le #else ne pouvant apparaître qu'une seule fois. Par contre, seule une condition vérifiée sera acceptée, même si par la suite d'autres conditions pourraient l'être.

Quelques autres instructions

Il existe encore quelques autres instructions du préprocesseur, mais elles sont beaucoup moins utiles (du moins pour ce que l'on va faire dans la suite de ce cours). Dans quelques cas bien précis, elles peuvent être fort utiles. Il est donc bien pour vous que vous sachiez qu'elles existent. Nous allons donc brièvement les présenter.

L'instruction #line

Cette directive permet de berner le compilateur en fixant le numéro de la ligne courante à une valeur fictive. Cette instruction peut aussi fixer le nom du fichier compilé. A priori, vous n'aurez jamais à utiliser cette instruction, sauf si, par exemple, vous deviez écrire un préprocesseur C.

 
	#line number
	#line number "file_name"

L'instruction #error

Cette instruction fait écrire au préprocesseur un message d'erreur. Ce message est en fait la suite de mots que vous placez en paramètre de l'instruction.

 
	#error mots ...

L'instruction #pragma

Cette dernière directive, sert à faire lancer des actions au préprocesseur. Si l'action n'est pas reconnue, elle est ignorée.

Conclusion

Pour conclure ce chapitre, il est important de signaler que l'utilisation du préprocesseur est vitale en C. Cependant, si lors de votre première lecture l'intérêt de ce préprocesseur ne vous semble pas exister ou si une instruction ne vous semble pas claire, ce n'est pas bien grave. Le but de ce chapitre était de vous signaler que cela existe afin que dès les premières utilisations de celui-ci vous ne vous demandiez pas d'ou cela sort. Dans bien d'autres cours ou ouvrages sur C l'apprentissage de cet utilitaire passe en dernier. Est-ce mieux ou non, je n'en sais trop rien, mais je pense qu'il vaut mieux, dès le départ, situer les choses afin de ne pas trop s'y perdre.


Besoin d'une formation : vous recherchez un centre de formation en informatique pour suivre un stage sur le langage C ANSI ? Si tel est le cas, consultez notre plan de cours pour la formation suivante : Le langage C ANSI

ATTENTION : Les tutoriaux Infini Software vous sont fournis dans le but de vous aider à acquérir les compétences nécessaires à l'utilisation des langages ou des technologies considérés. Infini Software ne pourra nullement être tenu responsable de l'utilisation des informations présentes dans ces tutoriaux. De plus, si vous remarquez des erreurs ou des oublis dans ce document, n'hésitez surtout pas à nous le signaler en activant ce lien.

Dominique LIARD - © 2007..2012 SARL Infini Software - Tous droits réservés
Les autres marques et les noms de produits cités dans ces documents sont la propriété de leurs éditeurs respectifs.