Par commodité le langage C exploite un préprocesseur. Un préprocesseur de texte est un logiciel qui parcourt un fichier source pour le transformer en un autre source, avant de donner ce nouveau source au véritable compilateur. A priori, le préprocesseur utilisé par le langage C est destiné au sources écrites en C, mais aussi en C++ et en Objective C. Cependant il peut être utilisé sur d’autres langages pourvu que ces derniers utilisent des conventions de commentaires et de chaînes de caractères équivalentes à celles du langage C. Il pourrait même être utilisé sur des sources Java, même si ce n’est pas l’usage.
Le préprocesseur permet donc de remplacer des symboles d’un fichier source par «autre chose», c’est-à-dire par une chaîne de caractères quelconque que le compilateur traitera en lieu et place du symbole.
Considérez le code source suivant disponible dans le fichier macro1.c (on travaille pour cetet section sur le processeur dans le sous-répertoire prepro/ du dépôt Git) :
extern int putchar(int c); int main() { int u=68; putchar(u); /* premier char */ putchar(T); /* deuxième char */ }
Il est possible d’indiquer au compilateur que le symbole T doit être transformé en autre chose.
Ces symboles qui sont simplement, syntaxiquement, remplacés par autre chose sont appelés des macros.
Il existe deux manières de définir une macro.
Sur cet exemple, on indique au préprocesseur du compilateur C que les symboles T doivent être remplacés par des 65.bash$ gcc macro1.c -D T=65@
Expliquez ce qu’affiche le programme ainsi compilé ?
La seconde solution consiste à utiliser la directive #define. dans le fichier source. À l’instar de la solution précédente, cette directive permet d’indiquer au préprocesseur qu’il doit remplacer un symbole par autre chose. Ajoutez la ligne suivante en tête du fichier source :
#define T (65+1)
Notez qu’il ne s’agit pas d’une instruction du langage C. Elle ne se termine pas par un ;. Elle ne correspond pas à une affectation, il n’y a pas de =... Ici on dit simplement au préprocesseur du compilateur, qu’à partir de maintenant, à chaque fois qu’il rencontre T, il doit le remplacer par (65+1).
Il est possible de demander un compilateur de n’exécuter que le traitement préprocesseur et de produire le fichier résultant de ce traitement sur la sortie standard (ou dans un fichier). Il s’agit de l’option -E.
qui ne retiendra que les lignes qui ne contiennent pas un #. Comment le préprocesseur a modifié le source ?... | grep -v "#"
Dans les exemples précédents la substitution opère sur ce qui semble être une simple variable. Il n’en est rien. Il s’agit en fait d’une substitution syntaxique, qui n’a aucune valeur sémantique : t n’est pas une variable, elle n’a d’ailleyrs pas étét déclarée, mais un symbole qui a été remplacé par (65+1).
Il est donc possible de remplacer n’importe quel symbole par autre chose, pour vu que ce soit par quelque chose que le compilateur C définisse, comme une fonction, une instruction élémentaire, ou une séquence d’instructions.
Considérez le programme elif.c suivant :
extern int putchar(int c); extern int getchar(void); #define DIGIT 'D' #define LOWER 'L' #define UPPER 'U' #define OTHER 'O' int main() { int c; for (;;) { c = getchar(); if ('0'<= c && c<='9') putchar(DIGIT); else if ('a'<=c && c<='z') putchar(LOWER); else if ('A'<=c && c<='Z') putchar(UPPER); else putchar(OTHER); } return 0; }
Il existe un certain nombre de macros qui sont prédéfinies. elles qui permettent d’instrumenter le code source. Les plus utiles sont les macros implicites suivantes :
et affichez la valeur de ln dans la fonction main() avant, et après le putdec(). Qu’observez vous ?int ln=__LINE__;
Le système de substitution de symbole permet de définir des paramètres de substitution. On parle alors de macros paramétrées. L’exemple suivant illustre cette possibilité (fichier macrop.c) :
extern int putchar(int c); #define bit(i,j) (i>>j)&1 int main() { int i=16; int i0=bit(i,0); int i4=bit(i,4); putchar('0'+i0); putchar('0'+i4); putchar('\n'); putchar('0'+bit(i,0)); putchar('0'+bit(i,4)); putchar('\n'); }
Dans ce source macrop.c, la macro bit est paramétrée par deux informations, i et j. Dans une lecture maladroite, on pourrait percevoir cette macro comme une fonction qui prend 2 paramètres (i et j) et qui renvoie la valeur du j-ième bit de i (0 ou 1).
Cependant ce n’est pas le sens de cette ligne de C. Cette ligne doit être comprise comme une indication au préprocesseur pour qu’il remplace, partout dans le code source, les occurrences de bit(x,y) par des (x>>y)&1.
Pour vous aider à mieux comprendre ce qui ce passe exactement, vous pouvez consulter le code produit par le préprocesseur, option -E du compilateur.
Le préprocesseur permet aussi de supprimer du fichier source des lignes de codes, selon des conditions qui seront appréciées au moment de la compilation. Il est par exemple possible que le même fichier source soit compiler avec, ou sans, certaines fonctions, selon que des macros soient définies ou pas dans la ligne de commande lors de la compilation.
Le préprocesseur permet de conditionner le fait que certaines portions de codes seront (ou pas) données au compilateur. Le développeur peut par exemple conditionner la compilation d’une portion du code source, en fonction de la valeur d’une macro particulière.
Considérez le programme suivant
extern int putchar(int c); #if NO_LOG==1 int logchar(int c) { return 0; } #else int logchar(int c) { return putchar(c); } #endif int main() { int i=1; i=3*i; logchar('0'+i); return i; }
disponible dans le fichier compcond.c (compilation conditionnelle).
Notez que l’expression évaluée par le préprocesseur ne peut porter que sur des constantes et sur des macros. Le préprocesseur ne peut pas apprécier la valeur d’une variable C par exemple.
Simplement le préprocesseur ne «comprends rien» au langage C.
En général nous n’avons pas besoin de tester une valeur particulière mais simple de tester un «oui ou non». Pour cela l’usage consiste à tester l’existence ou non d’une macro et non sa valeur. Les primitives #ifdef MACRO sont alors utilisées.
#ifdef MACRO est validée par le préprocesseur si la macro MACRO est définie, quelque soit sa valeur (et en particulier, même si elle n’a aucune valeur, ce que l’on obtiendrait avec un #define MACRO sans rien de plus...).
Notez qu’il n’est plus utile de donner une valeur à la macro NO_LOG puisque seule son existance est testée.bash$ gcc compcond.c -D NO_LOG
Comparez le résultat avec celui de la compilation obtenue par :
bash$ gcc compcond.c
Considérez maintenant le programme suivant (compchk.c:
#ifndef SIZE #error "définissez SIZE avec l'option -D SIZE=n" #define SIZE 0 #endif #if SIZE & (SIZE-1) #warning "SIZE devrait être une puissance de 2." #endif int main(void) { return SIZE; }
Choisissez une valeur qui n’est pas une puissance de 2. Que ce passe-t-il ? Pourquoi ?
Avant de pourusivre avec l’étude des fonctionnalités du préprocesseur, assurez-vous de bien comprendre les quatre premières recommandations de l’encart Bon usage du préprocesseur.
Bon usage du préprocesseur ...indispensable...
Le préprocesseur permet d’écrire de transformer le code source, et, dans une certaine mesure, de faire évoluer la syntaxe du langage C, de façon assez radicale. Si cet usage a connu son heure de gloire dans les années 1990, il est aujourd’hui largement déprécié. En effet, l’usage intensif du préprocesseur, pour créer de nouveaux «pseudo mots clefs», pose deux problèmes principaux :
- le code source ainsi traité devient difficile à lire par un tier qui ne connaît pas les macros mises en place ;
- le code source devient difficile à débugger. Considérez
#define MAX 3;
int i;
if (i < MAX) {
...Pour mettre en évidence les segments de codes qui seront transformés par le préprocesseur, dans un source C, un certain nombre d’usages se sont progressivement imposés :
- les symboles du préprocesseur sont écrits en majuscule : #define MA_MACRO 17
- les expressions évaluées sont sur-parenthèsées : #define MA_MACRO (3+i*2) plutôt que 3+i*2...
- les macros paramétrées sont dépréciées au profit de fonctions inline du type inline int max(int i,int j) { return (i<j)?i:j; }
- les notices précisent toujours les fonctions susceptibles d’être implémentées avec des macros : voir par exemple $ man putc
- les #include portent toujours sur des fichiers de prototypes .h et pas sur des fichiers .c;
- les fichiers de prototypes ne devraient jamais contenir d’implémentation mais seulement des déclarations (à l’exception des fonctions inline) ;
- il est conseillé d’utiliser des include guard : consulter
https://fr.wikipedia.org/wiki/Include_guard.4.3 Inclure des fichiers sources, les uns dans les autres
Le préprocesseur du langage C permet d’inclure un fichier dans un autre. C’est le sens de la directive #include "un_fichier".
Lorsque le préprocesseur rencontre cette commande il effectue l’équivalent d’un «copié-collé» de un_fichier en lieu est place du #include "un_fichier". Il est ainsi possible d’inclure un autre fichier source dans un fichier source C.
Exercice 42 (L’inclusion de fichiers) Éditez un fichier include_file.c comme suit :int f(int x) { return 2*x; }Puis un fichier main_file.c4
#include "include_file.c" int main() { return f(3); }
Exercice 43 (Un premier #include) Observez le résultat du traitement du fichier main_file.c par le préprocesseur en utilisant les mêmes options que précédemment. Qu’a fait le préprocesseur ? À quoi correspondent les lignes qui commencent par un # ?En utilisant les #include il devient possible de mettre des fonctions dans les fichiers que l’on réutilise d’un programme à l’autre. (Cependant le langage C nous invite à ne pas faire usage du préprocesseur en ce sens, mais à plutôt privilégier la création de fichiers objets que nous expérimenterons dans un exercice ultérieur.)
L’inclusion de fichiers est trés utilisées pour éviter d’avoir à redéclarer les fonctions externes que nous avons l’intention d’utiliser.
Précédemment, nous avons de nombreuses fois déclaré la fonction int putchar(int c); qui est implémentée dans une librairie de fonctions standard du langage C. Cette fonction, comme toutes celles de la librarie standard sont très fréquemment utilisées. Aussi il peut être fastidieux, de déclarer chacune d’elles au début de chaque nouveau programme. C’est pourquoi un fichier de déclaration de ces fonctions a été standardisé. Notez bien que ce fichier ne contient pas l’implémentation mais seulement la déclaration des fonctions (c’est-à-dire la ligne e.g. int putchar(c) ; sans le corps de la fonction).
Exercice 44 (Les fichiers .h) Trouver le nom du fichier dans lequel la fonction putchar() est déclarée en interrogeant le manuel.Notez que dans le manuel le fichier a pour extension .h et pas .c. Un fichier .h est un source en langage C au même titre qu’un fichier .c. Cependant l’extension .h a pour vocation d’indiquer que le fichier contient des déclarations, mais pas d’implementation. On parle de fichiers de prototypes. Ils sont donc destinés à être inclus dans d’autres fichiers C mais pas à être compilés pour eux-mêmes.
Vous pouvez observer que votre répertoire courant (celui de vos fichiers sources) ne contient pas le fichier .h mentionné par le manuel. Vous pouvez aussi remarquer que dans le manuel le nom du fichier à inclure n’est pas donné entre guillemets " ... ", mais pas entre chevrons < ... >.
Lorsqu’un fichier est donné entre guillemets, c’est que le préprocesseur doit le trouver dans le repertoire courant (ou qu’il doit éventuellement être trouvé dans des repertoires annexes définis explicitement au compilateur par l’option -I dir).
Lorsqu’un fichier à inclure est donné entre chevrons, c’est qu’il doit être trouvé dans des répertoires annexes définis implicitement par le compilateur. Pour connaitre la liste de ces répertoires définit par gcc vous pouvez faire :
bash$ echo | gcc -E -Wp,-v -
Exercice 45 (Les #include par défaut) Modifier le programme précédent pour qu’il affiche le résultat de f(3) sur l’écran avant de sortir. Pour cela utilisez la fonction putchar(), mais au lieu de la déclarer en début de fichier, demandez au préprocesseur d’inclure le fichier indiqué par le manuel.Vérifiez que votre nouveau programme fonctionne.
Observez le fichier source produit par le préprocesseur. Combien de ligne contient le fichier produit par le préprocesseur ? Comment est déclarée la fonction putchar() ?
Lorsque les programmes deviennent plus complexes, il peut être tentant de mettre des directives #include dans des fichiers .h. Dans ce cas, par défaut, le préprocesseur va inclure, récursivement les fichiers référencés. La norme du langage C précise qu’un compilateur doit pouvoir gérer jusqu’à 16 niveaux d’inclusion (d’un fichier, dans un autre, dans un autre...). gcc en gère jusqu’à 200.
Fonctions inline ...en savoir plus...
Les fonctions ...
Considérez le petit exemple suivant composé de trois fichiers :
Un fichier abs.h :
inline int abs(int x) { return (x<0)?-x:x; }Un fichier minmax.h :
#include "abs.h" inline int min(int x,int y) { return (x+y-abs(x-y))/2; } inline int max(int x,int y) { return (x+y+abs(x-y))/2; }Et enfin un dernier fichier guard.c :
#include "minmax.h" #include "abs.h" int putchar(int c); int main() { putchar('0' + min(3, 4)); putchar('0' + max(3, 4)); putchar('0' + abs(-2)); return 0; }Dans cet exemple, on imagine que les deux fichiers .h ont été conçus par Alice. Bob désire utiliser les fonctions proposées par Alice a écrit le fichier .c indépendamment.
La compilation de guard.c échoue, le compilateur rapporte que la fonction abs() est définie plusieurs fois.
Exercice 46 (Règle de la simple définition) Observez le résultat du traitement du préprocesseur sur le fichier guard.c.Pourquoi Bob transgresse la règle de simple définition ?
Bob pourrait corriger son fichier guard.c. Cependant sa première version est raisonnable et ne devrait pas poser de problème de compilation.
C’est à Alice de fournir, en tant que le développeuse expérimentée, de livrer des fichiers sources qui puissent toujours être utilisés.
Exercice 47 (#include guard) Proposez une modifcation des fichiers d’Alice pour que Bob ne rencontre plus de problème de compilation, sans toucher à son programme.Notez aussi que Alice ne souhaite pas fusionner les deux fichiers .h et qu’elle souhaite bien inclure abs.h dans minmax.h (pour que min() et max() profitent des prochaines évolutions de abs()).
![]()
![]()
![]()