Introduction au langage C/Le préprocesseur

Début de la boite de navigation du chapitre
Le préprocesseur
Icône de la faculté
Chapitre no 25
Leçon : Introduction au langage C
Chap. préc. :Fonctions de base
Chap. suiv. :Sources
fin de la boite de navigation du chapitre
En raison de limitations techniques, la typographie souhaitable du titre, « Introduction au langage C : Le préprocesseur
Introduction au langage C/Le préprocesseur
 », n'a pu être restituée correctement ci-dessus.

Généralités

modifier

Définitions

modifier
  • Le préprocesseur : C’est le programme chargé de la précompilation.
  • La précompilation : C’est une phase pendant laquelle le préprocesseur va ajouter, supprimer, remplacer certaines chaînes de texte dans le fichier source selon des directives à interpréter. La précompilation est la première étape de la chaîne de compilation. Il en sort un fichier texte modifié.

Interprétation par ligne

modifier

Une directive destinée au préprocesseur se déclare par un "#" dans le texte du fichier source et se termine au moment du passage à la ligne : c'est-à-dire qu'une directive prend au minimum une ligne et peut se poursuivre sur plusieurs lignes si elle contient le caractère "\" en fin de ligne.

Afficher les résultats du préprocesseur

modifier

Habituellement, lors d'une compilation, le préprocesseur et ses résultats sont masqués. Dans certains cas, vous pouvez soupçonner que des directives et des actions du préprocesseur sont en cause, par exemple :

  • macro-instructions et autres directives du préprocesseur modifiées récemment et posant des problèmes;
  • inclusion de fichiers suspects ou fortement imbriqués;
  • expansion de macros compliquées;
  • portage sur un système non prévu par les développeur du programme original.

Il vous faudra alors contrôler le texte produit par le préprocesseur. Pour obtenir ce texte intermédiaire, vous fournirez une options au compilateur, par exemple -E pour gcc. Cette option demande l'arrêt de la compilation après l'action du préprocesseur. Les sorties du préprocesseur sont envoyées sur la sortie standard.

gcc -E age.c > age.i : demande de préprocesser le source age.c et dirige la sortie standard dans le fichier age.i. Vous pourrez éditer ce fichier et le contrôler. Dedans ce .i, des directives du genre # 132 "/usr/include/stdio.h" 3 4 permettent de se situer dans les fichiers d'origine. Les fichiers .i peuvent être utilisés dans des phases de compilations ultérieures. Cette extension .i est réservée aux fichiers sources en langage C qui ne doivent pas être préprocessés.

Inclusion de fichier

modifier

Il est souvent nécessaire de répéter les mêmes directives destinées au préprocesseur ou de partager les prototypes de fonctions dans plusieurs fichiers sources. Ces instructions sont rassemblées dans des fichier texte d'extension .h (fichiers d'entête ou include). Des directives #include permettent de demander au préprocesseur de recopier automatiquement le contenu de ces includes dans les fichiers sources.

Bibliothèques

modifier

La libC propose nativement un panel de bibliothèques servant à écrire à l'écran ou à manipuler des chaînes de caractères par exemple. Pour utiliser ces bibliothèques, il faut inclure les fichiers d'entête systèmes appropriés. On utilise dans l'instruction #include des chevrons "<" et ">". Le programmeur n'est donc pas obligé de spécifier le chemin complet pour accéder à ces fichiers d'entête, c’est le préprocesseur qui se charge de leur recherche dans son arborescence

Un exemple avec stdio.h, qui permet de gérer les entrées/sorties.

Début d’un principe
Fin du principe


Autres Fichiers

modifier

Quand un projet prend de l'ampleur, le programmeur a besoin de regrouper dans ses fichiers d'entête des définitions à partager par plusieurs sources. Pour les inclure ensuite, il suffit d’utiliser des "guillemets" dans l'instruction #include.

Attention : à la différence des bibliothèques fournies par la libC, il faut spécifier le chemin d'accès au fichier d'entête en relatif par rapport au fichier à compiler ou par rapport à des répertoires passés au compilateur par l'option -I.

Début d’un principe
Fin du principe


Exemple d’utilisation de l'option -I du compilateur pour fournir un nom de répertoire utilisé pour la recherche de fichier du système X11 sur un Mac sous MacOS X.4: gcc -c -I/Developer/SDKs/MacOSX10.4u.sdk/usr/X11R6/include source.c

Remarques qualité

modifier
  • Il est déconseillé d'inclure des fichiers non système avec des chevrons <> car certains environnements de développement (Windows/MinGW/gcc) ne recherchent alors ces fichiers seulement dans l'arborescence du compilateur et non dans les répertoires spécifiés avec l'option -I.
  • Un chemin absolu entre "guillements" est à éviter car il rend impossible le déplacement des fichiers sources sans modification. Ce mécanisme peut être utilisé pour piéger les développeurs par un système de licence et d'installation (genre side by side versioning à la Microsoft). Cette état d'esprit est très éloigné de l'esprit des logiciels libres.
  • La pratique consistant à inclure des instructions dans un fichier d'entête doit être proscrite, il faut définir une fonction ou un groupe de fonction dans un fichier source séparé. Seuls les prototypes de fonctions seront placées dans un fichier d'entête.

Les Symboles

modifier

Définition

modifier

La définition de symbole sert à substituer un objet à un autre. Pour être considéré comme symbole, il faut que la directive respecte une certaine syntaxe. Il faut qu’il y ait deux tokens entre le #define et l'identificateur puis entre l'identificateur et l’objet de substitution.

Utilité

modifier

L'utilité de cette fonctionnalité réside dans le fait que le source possèdent des valeurs récurrentes liées à un objet. Par conséquent, il peut arriver que le programmeur ait à changer cette valeur pour diverses raisons. Dans ce cas précis il suffit de changer une seule et unique fois la valeur associée à cet objet grâce à ce mécanisme.
Voici un exemple qui vous paraîtra peut être plus explicite :

Début de l'exemple
Fin de l'exemple


Si NBLIVRE vaut 11 au lieu de 10, il me suffira de changer la valeur de la constante symbolique NBLIVRE et de recompiler. Sans cette directive, il aurait fallu parcourir le source et faire 3 modifications. Cette directive m'a permis de faire une seule modification au lieu de trois. Cela peut vous paraître dérisoire mais sur un plus grand nombre, ça l'est beaucoup moins.

Symboles prédéfinis

modifier
  • __DATE__ : chaîne de caractère représentant la date de compilation.
  • __TIME__ : chaîne de caractère représentant l’heure de compilation.
  • __FILE__ : Nom du fichier source d'origine.
  • __LINE__ : Ligne courante.
  • __STDC_VERSION__ : permet de connaître la version du compilateur utilisé : entier long qui vaut 199901L si le compilateur respecte la norme C99.

Remarques qualité

modifier
  • Les constantes littérale (10, "config.txt") doivent être évitée au sein du code source.
  • Les améliorations possibles :
    1. Utiliser des définitions de constantes symboliques en tête du fichier source.
    2. Regrouper ces constantes symboliques dans un fichier d'entête inclus.
    3. Lire tous ces paramètres dans un fichier de configuration. Cette méthode permet de ne pas avoir à recompiler le code en cas de modification de paramètres et donc d’être modifiée par un non programmeur.
    4. Mettre des parenthèses autour des termes définis, sans quoi certaines opérations pourraient les traiter indépendamment :
Début de l'exemple
Fin de l'exemple


Les macros

modifier

Définition

modifier

Une macro possède les mêmes caractéristiques qu'un symbole à la différence qu’il y a plus qu'un simple remplacement de texte, c’est une substitution avec prise en compte de paramètres.

En général, les macros servent à définir une substitution par une ou plusieurs opérations traitables par la suite par le compilateur. Les macros se définissent par la directive "#define", comme un symbole.

Début de l'exemple
Fin de l'exemple


Le préprocesseur va traiter le source carre.c : le résultat dans carre.i obtenu par gcc -E carre.c > carre.i :

Début d’un principe
Fin du principe


On remarque que (j+1)*(j+1) donne (2+1)*(2+1) = 3*3 = 9, mais j+1*j+1 donne 2+ (1*2) + 1 = 5. Il faut veiller à bien mettre des parenthèses autour des termes d'une macro.

À l'écran, vous verrez donc s'afficher :

Le carre de 2 est 4
Le carre de 3 est 9
Le carre (faux) de 3 est 5

Remarques Qualité

modifier
  • Les macros complexes peuvent provoquer des erreurs difficiles à détecter : le source avant expansion des macros peut sembler correct.
  • Le contrôle des types des paramètres des macros est inexistant.
  • La norme C99 permet de s'affranchir de ces problèmes. Elle offre le mot-clé inline qui suggère au compilateur de recopier le code d'une fonction à l'endroit où il est appelé pour améliorer les performances.

Implémentation de la macro carre en utilisant une fonction inline en C99 :

Début d’un principe
Fin du principe


Début d’un principe
Fin du principe


Début de l'exemple
Fin de l'exemple


Compilation conditionnelle

modifier

Les instructions de compilation conditionnelle indiquent au préprocesseur de prendre en compte ou d'ignorer un ensemble de ligne de code du fichier source selon une condition. La principale utilité de ce mécanisme permet d’avoir un seul code source que l’on peut compiler pour différentes architectures.

Options de la ligne de commande

modifier

Certaines options de la ligne de commande permettent de définir ou non des constantes symbolique utilisées par le préprocesseur et ainsi d'orienter ses traitements.

  • -D nom permet de définir la constante symbolique nom.
  • -D nom=definition permet en plus, lui donner la valeur definition.
  • -U nom permet d'annuler la définition de la constante symbolique nom.

#if, #elif, #else et #endif

modifier
Début d’un principe
Fin du principe


condition est une opération booléenne mettant en jeu des tests sur des constantes symboliques. Les opérateurs booléens utilisables ont la même syntaxe que pour ceux du C : &&, ||, !, de même pour les opérateurs de comparaison : ==, <, <=, >, >=. Un opérateur defined(SYMBOLE) qui est VRAI si SYMBOLE est défini.

Début d’un principe
Fin du principe


Dans l'extrait de code ci-dessus, si la constante symbolique est définie et contient la valeur 2 et la constante symbolique ESSAI non définie, alors la ligne (void)puts("DEBUG defini a 2 et ESSAI non defini"); sera conservée par le préprocesseur.

La ligne de compilation suivante remplit ces conditions : gcc -c -D DEBUG=2 define.c, mais pas celle ci : gcc -c -D DEBUG=2 -D ESSAI define.c.

#ifdef marche un peu comme un #if à la seul différence qu’il vérifie seulement si une constante a été définie.

Début d’un principe
Fin du principe


On peut tout aussi bien faire ceci avec un #if de cette façon :

Début d’un principe
Fin du principe


Attention : l'opérateur defined ne peux être utilisé que dans le contexte d'une commande #if et #elif.

L'intérêt de la deuxième méthode est que l’on peut faire des #elif alors qu'avec la première on est obligé de faire un nouveau #ifdef. L'opérateur defined permet de construire des conditions logiques plus complexes.

#ifndef

modifier

#ifndef SYMBOLE permet de tester si une constante symbolique ici SYMBOLE n’est pas définie. C’est un équivalent de #if !defined(SYMBOLE)

#ifndef est très utilisé dans les fichiers d'entête .h pour éviter les inclusions infinies ou multiples.

Inclusions infinies

modifier

Soit un fichier A.h et un fichier B.h. Le fichier A.h contient #include"B.h". Le fichier B.h est donc inclus dans le fichier A. Mais, si B.h contient à son tour #include"A.h". Le premier fichier a besoin du second pour fonctionner, et le second a besoin du premier.

Ce qui va se passer :

  1. L'ordinateur lit A.h et voit qu’il faut inclure B.h
  2. Il lit B.h pour l'inclure, et là il voit qu’il faut inclure A.h
  3. Donc il inclut A.h dans B.h, mais dans A.h on lui indique qu’il doit inclure B.h !
  4. Ce scénario va boucler à l'infini.

Pour éviter cela :

Début d’un principe
Fin du principe


NAMEFILE_H représente le nom du fichier .h en majuscule. Ce mécanisme va éviter les inclusion en boucle.

Inclusions multiples

modifier

Si plusieurs fichiers d'entête inclus demandent tous les deux d'inclure le même troisième. Toutes les définitions, prototypes... contenues dans ce dernier fichier vont être répétés dans le résultat produit par le préprocesseur.

Autres directives

modifier

L'instruction #line permet de changer le numéro de ligne et le nom du fichier courant. Malgré le passage du préprocesseur (inclusion de fichiers, expansion de macro, sélection et inclusion de lignes), les résultats de cette directive permettent au compilateur de relier ses messages d'erreur à une ligne et au nom d'un fichier source.

La directive existe sous trois formes :

  • #line numéro : positionne le compteur de lignes à numéro.
  • #line numéro "fichier" : positionne le compteur de lignes à numéro et spécifie que le nom du fichier courant est fichier.
  • #line macros</syntaxhighlight> : le préprocesseur procède d’abord à l'expansion des macros. L'instruction qui en résulte doit alors être de l'une des deux formes précédentes.

Deux constantes symboliques vous permettent de récupérer ces informations :

  • __FILE__ : pour le nom du fichier source
  • __LINE__ : pour le numéro de ligne dans laquelle __LINE__ apparait.

La directive #error vous sert à arrêter la compilation lorsque vous jugez que votre programme ne pourra pas fonctionner. Les raisons sont par exemple :

  • Une plate-forme non supportée;
  • Une ressource non trouvée par un outil comme configure;
  • Une constante symbolique dont la valeur est incorrecte.

Elle est souvent placée dans la partie #else d'une instruction de compilation conditionnelle #if ... #else ... #endif.

Sa syntaxe est la suivante : #error "message d'erreur". Lorsque le compilateur arrive à cette ligne, il arrête la compilation et affiche message d'erreur.

Début d’un principe
Fin du principe


Opérateurs # et ##

modifier

L'opérateur # précédant un nom de constante symbolique peut servir à préparer l'impression d'expressions contenant des délimiteurs de chaîne " ou des caractères d'échappement comme \n.

L'extrait de code source :

Début d’un principe
Fin du principe


Sera expansé par le préprocesseur en :

Début d’un principe
Fin du principe


Cet opérateur # peut aussi servir pour émettre un message plus précis par la fonction perror. Le message d'erreur peut alors contenir le nom du fichier source dans lequel s'est produit l'erreur ainsi que le numéro de la ligne concernée

Début de l'exemple
Fin de l'exemple


Donnera à l'exécution :

Transformation de constante numerique en chaine
__LINE__ = 42
STR(__LINE__) = __LINE__
XSTR(__LINE__) = 44

Application a perror
macro.c: ligne 49: Numerical argument out of domain

L'opérateur ## permet de concaténer une chaîne de caractère à un paramètre de macro :

Début d’un principe
Fin du principe


Sera expansé par le préprocesseur en :

Début d’un principe
Fin du principe


Donnera à l'exécution :

Creation de variable par concatenation de chaine
x1= 1, x2= 2

L'utilisation de cet opérateur ## peut cependant rendre le code obscur. L'utilisation d'une fonction peut être plus lisible par un programmeur habitué à d'autres langage que le C.

#pragma

modifier

La directive #pragma, qui n'existe que depuis la norme ANSI, est entièrement dépendante de l'implémentation. Les concepteurs d'environnement ont une liberté complète pour décider de son utilisation. Le préprocesseur de l'environnement de développement MPW sur Macintosh, par exemple, utilise cette directive pour permettre au programmeur de décomposer son code en segments pour optimiser l'occupation de la mémoire centrale. Ainsi le code suivant :

Début d’un principe
Fin du principe


aura pour effet que le code exécutable de fonction1 et fonction2 sera placé dans un segment de nom SEGA tandis que celui de fonction3 sera placé dans un segment de nom SEGB.

  Faîtes les exercices du WikiLivre Exercices en langage C sur le préprocesseur.