Introduction au langage C/Le préprocesseur
Généralités
modifierDé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
modifierUne 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
modifierHabituellement, 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
modifierIl 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
modifierLa 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.
Autres Fichiers
modifierQuand 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.
/* En relatif par rapport au répertoire du source courant */
#include "../libs/maBiblio.h"
/* En relatif par rapport au répertoire du source courant ou d'un repertoire passé par -I */
#include "X11/X.h"
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
modifierDéfinition
modifierLa 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é
modifierL'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 :
/*
Compilation : gcc -Wall -pedantic -o livre.exe livre.c
Vérification resultats du preprocesseur :
gcc -E livre.c > livre.i
*/
#include <stdio.h>
#include <stdlib.h>
#define NBLIVRE 10
int main(void)
{
int i=0;
int maBibliotheque[NBLIVRE];
for (i=0; i<NBLIVRE; i++)
{
/* traitement fictif */
maBibliotheque[i]=1;
}
(void)printf("J’ai %d livres.\n", NBLIVRE);
return EXIT_SUCCESS;
}
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 :
- Utiliser des définitions de constantes symboliques en tête du fichier source.
- Regrouper ces constantes symboliques dans un fichier d'entête inclus.
- 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.
- Mettre des parenthèses autour des termes définis, sans quoi certaines opérations pourraient les traiter indépendamment :
#define x 1 + 2
int ProduitFaux()
{
return (x * 2);
}
#define y (1 + 2)
int ProduitJuste()
{
return (y * 2);
}
main()
{
printf("Le double de 1 + 2 est %d\n", ProduitFaux());
printf("Le double de 1 + 2 est %d\n", ProduitJuste());
scanf("fin");
}
Ici, ProduitFaux renvoie 5 (1 + 2*2), et ProduitJuste 6.
Les macros
modifierDéfinition
modifierUne 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.
/*
Compilation : gcc -Wall -pedantic -o carre.exe carre.c
Vérification des resultats du preprocesseur :
gcc -E carre.c > carre.i
*/
#include <stdio.h>
#include <stdlib.h>
#define carre(x) (x)*(x)
#define carre_faux(x) x*x
int main(void)
{
int j = 2;
(void)printf("Le carre de %i est %i\n", j, carre(j));
(void)printf("Le carre de %i est %i\n", j+1, carre(j+1));
(void)printf("Le carre (faux) de %i est %i\n", j+1, carre_faux(j+1));
return EXIT_SUCCESS;
}
Le préprocesseur va traiter le source carre.c : le résultat dans carre.i obtenu par gcc -E carre.c > carre.i
:
int main(void)
{
int j = 2;
(void)printf("Le carre de %i est %i\n", j, (j)*(j));
(void)printf("Le carre de %i est %i\n", j+1, (j+1)*(j+1));
(void)printf("Le carre (faux) de %i est %i\n", j+1, j+1*j+1);
return 0;
}
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 :
/*
* carre.c
*/
#include "carre.h"
inline int carre(const int valeur)
{
return valeur * valeur;
}
/*
Nom : carre_main.c
Auteur : Thierry46
Role : Utilisation de fonction inline C99.
Paramètres : non pris en compte.
Pour produire un exécutable avec le compilateur libre GCC :
gcc -Wall -pedantic -std=c99 -o carre_main.exe carre_main.c carre.c
Pour exécuter, tapez : ./carre_main.exe
Version : 1.0 du 18/5/2008
Licence : GNU GPL
*/
#include <stdio.h>
#include <stdlib.h>
#include "carre.h"
int main(void)
{
int j = 2;
(void)printf("Le carre de %i est %i\n", j, carre(j));
return EXIT_SUCCESS;
}
Compilation conditionnelle
modifierLes 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
modifierCertaines 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#if condition
/* Code à compiler si la condition est vraie */
#elif condition2
/* Sinon si la condition 2 est vraie compiler ce bout de code */
#else
/* Sinon on compile ce bout de code */
#endif
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.
#if (DEBUG==2) && !defined(ESSAI)
(void)puts("DEBUG defini a 2 et ESSAI non defini");
#endif
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
modifier#ifdef marche un peu comme un #if à la seul différence qu’il vérifie seulement si une constante a été définie.
On peut tout aussi bien faire ceci avec un #if de cette façon :
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
modifierSoit 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 :
- L'ordinateur lit A.h et voit qu’il faut inclure B.h
- Il lit B.h pour l'inclure, et là il voit qu’il faut inclure A.h
- Donc il inclut A.h dans B.h, mais dans A.h on lui indique qu’il doit inclure B.h !
- Ce scénario va boucler à l'infini.
Pour éviter cela :
NAMEFILE_H représente le nom du fichier .h en majuscule. Ce mécanisme va éviter les inclusion en boucle.
Inclusions multiples
modifierSi 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#line
modifierL'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.
#error
modifierLa 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.
#if defined(HAVE_DIRENT_H) && defined(HAVE_SYS_TYPES_H)
#include <dirent.h>
#include <sys/types.h>
#else
/* Arret : dirent.h et sys/types.h non trouves sur ce système */
#error "Readdir non implemente sur cette plateforme"
#endif
Opérateurs # et ##
modifierL'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 :
#define STR(s) #s
/*
Pour imprimer une chaine contenant des " et
des caractères d'echappement ex : \n
*/
(void)puts(STR((void)puts("\n");));
Sera expansé par le préprocesseur en :
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
/*
Transformation de la constante numerique __LINE__ en chaine
de caractère par le preprocesseur a l'aide de l'operateur #.
Remarquer que STR(__LINE__) sera expanse en __LINE__ et non
en numéro de ligne.
*/
(void)puts("Transformation de constante numerique en chaine");
#define STR(s) #s
#define XSTR(s) STR(s)
(void)printf("__LINE__ = %d\n", __LINE__);
(void)printf("STR(__LINE__) = %s\n", STR(__LINE__));
(void)printf("XSTR(__LINE__) = %s\n", XSTR(__LINE__));
/* Utilisation avec perror. Evite un sprintf dans buffer */
(void)puts("\nApplication a perror");
errno = EDOM; /* Simulation erreur sur domaine de fonction */
perror(__FILE__": ligne " XSTR(__LINE__));
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 :
int x1 = 1;
int x2 = 2;
/* Exemple de macro utilisant la concatenation de variables */
#define debug(s, t) \
(void)printf("x" # s "= %d, x" # t "= %d\n", \
x ## s, x ## t)
(void)puts("\nCreation de variable par concatenation de chaine");
debug(1, 2);
Sera expansé par le préprocesseur en :
int x1 = 1;
int x2 = 2;
(void)puts("\nCreation de variable par concatenation de chaine");
(void)printf("x" "1" "= %d, x" "2" "= %d\n", x1, x2);
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
modifierLa 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 :
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.
TP
modifier