« Introduction au langage C/Le préprocesseur » : différence entre les versions

Une page de Wikiversité, la communauté pédagogique libre.
Contenu supprimé Contenu ajouté
Thierry46 (discussion | contributions)
→‎Symboles prédéfinis : Compléments
Thierry46 (discussion | contributions)
→‎Opérateurs # et ## : Rédaction et exemples
Ligne 93 : Ligne 93 :


===Opérateurs # et ##===
===Opérateurs # et ##===

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 :
<source lang=c>
#define STR(s) #s
/*
Pour imprimer une chaine contenant des " et
des caracteres d'echappement ex : \n
*/
(void)puts(STR((void)puts("\n");));
</source>

Sera expansé par le préprocesseur en :
<source lang=c>
(void)puts("(void)puts(\"\\n\");");
</source>

Cet opérateur # peut aussi servir pour émettre un message plus précis par la fonction <tt>perror</tt>. 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

<source lang=c>
/*
Transformation de la constante numerique __LINE__ en chaine
de caractere par le preprocesseur a l'aide de l'operateur #.
Remarquer que STR(__LINE__) sera expanse en __LINE__ et non
en numero 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__));
</source>

Donnera à l'exécution :
<pre>
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
</pre>

L'opérateur ## permet de concaténer une chaîne de caractère à un paramètre de macro :
<source lang=c>
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);
</source>

Sera expansé par le préprocesseur en :
<source lang=c>
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);
</source>

Donnera à l'exécution :
<pre>
Creation de variable par concatenation de chaine
x1= 1, x2= 2
</pre>

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.


===Remarques qualité===
===Remarques qualité===

Version du 24 mai 2008 à 14:30


Image logo indiquant que la page n’est pas finiUn contributeur vous informe que cette page, ou cette section de page, n’est pas finie.
  • Son état actuel est provisoire et doit être pris avec prudence.
  • Une version améliorée est en préparation et devrait être disponible prochainement.

Pour en suivre l’avancement ou y participer, veuillez consulter la page de discussion.

Début de la boite de navigation du chapitre
Le préprocesseur
Icône de la faculté
Chapitre no {{{numéro}}}
Leçon : 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

Définitions

  • Le préprocesseur : C'est le programme chargé de la précompilation.
  • La précompilation : C'est une phase ou 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

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

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

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

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.

#include <stdio.h>

Autres Fichiers

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.

/* 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é

  • 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 nom 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

Définition

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é

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 :

/*
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

  • __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.

Opérateurs # et ##

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 :

#define STR(s) #s
/*
Pour imprimer une chaine contenant des " et 
des caracteres d'echappement ex : \n
*/
(void)puts(STR((void)puts("\n");));

Sera expansé par le préprocesseur en :

(void)puts("(void)puts(\"\\n\");");

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 caractere par le preprocesseur a l'aide de l'operateur #.
Remarquer que STR(__LINE__) sera expanse en __LINE__ et non
en numero 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.

Remarques qualité

  • 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.

Les macros

Définition

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

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.

Exemples

/*
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é

  • 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;
}
/*
 *  carre.h
 */
extern inline int carre(const int 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

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

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

#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

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

#define Linux

#ifdef Linux
    /* code pour Linux */
#endif

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

#define Linux

#if defined(Linux)
    /* code pour Linux */
#endif

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

#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

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 :

#ifndef NAMEFILE_H
#define NAMEFILE_H

/* Contenu de votre fichier .h */

#endif

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

Inclusions multiples

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

#line

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, selection 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</source> : 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

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.

#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 systeme */
#error "Readdir non implemente sur cette plateforme" 
#endif

#pragma

La directive #pragma, qui n'existe que depuis la norme ANSI, est entièrement dépendante de l'implantation. 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 :

#pragma segment SEGA
fonction1
fonction2
#pragma segment SEGB
fonction3

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

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