Retrouvez cet article dans : Linux Magazine 105
1. Une défaillance peu connue du compilateur
Grâce au macro processeur du langage C, il est possible de définir et d’utiliser des constantes de la manière suivante :1 #include <stdio.h>
2
3 #define CONSTANTE_1 (0x3d+20)
4 #define CONSTANTE_2 (0x3e+20)
5 #define CONSTANTE_3 (0x3f+20)
6
7 int main(int ac, char *av[])
8 {
9 printf(“%u\n”, CONSTANTE_1);
10 printf(“%u\n”, CONSTANTE_2);
11 printf(“%u\n”, CONSTANTE_3);
12 }
Si l’on compile ce programme, on obtient une erreur inattendue :
> gcc main.c main.c:10:18: erreur: suffixe " +20 " invalide pour une constante entièreLe compilateur détecte une erreur à la ligne 10, colonne 18, c’est-à-dire au niveau de la macro
#define CONSTANTE_2 (0x3e + 20)Ainsi, la compilation et l’exécution peuvent se réaliser :
> gcc main.c > ./a.out 81 82 83Il est intéressant de noter que ce problème existe dans les sources du noyau Linux en version PowerPC. Il suffit de consulter le fichier "
#define SIU_INT_PC1 ((uint)0x3e+CPM_IRQ_OFFSET)Il est donc conseillé de systématiquement mettre une espace de part et d’autre des opérateurs " + " et " – " quand on définit une macro avec le chiffre hexadécimal " e ".
2. Terminaison de macro
Il n’est pas recommandé de terminer une macro par un " ; " comme dans l’exemple suivant :1En effet, cela mène à une erreur de compilation :#define ADD(a, b) a + b;2 3 int main(int ac, char *av[]) 4 { 5 int r = ac; 6 7 if (r) 8 r = ADD(4, 5); 9 else 10 r = ADD(5, 6); 11 12 return 0; 13 }
> gcc main.c main.c: In function "main”: main.c:9: erreur:La ligne 9 contient deux instructions du fait de l’ajout du " ; " derrière l’appel àexpected expression before "else"
> gcc -E main.c
[...]
if (r)
r = 4 + 5;;
else
r = 5 + 6;;
L’instruction 3. Regrouper les instructions
Quand une macro contient plusieurs instructions comme :#define ADD(a, b, c) \
printf("%d + %d ", a, b); \
c = a + b; \
printf(“= %d\n”, c);
On peut se retrouver confronté à l’erreur de compilation évoquée au § 2 si elle est utilisée dans une instruction #include <stdio.h>
#define ADD(a, b, c) \
printf("%d + %d ", a, b); \
c = a + b; \
printf("= %d\n", c);
int main(int ac, char *av[])
{
int i = 0;
int r = 0;
while (av[i++])
ADD(r, 1, r);
return 0;
}
On s’attend à ce que toutes les instructions regroupées dans #define ADD(a, b, c)Le compilateur ne génèrera pas de boucle vu que la condition d’itération est toujours fausse :do {\ printf(“%d + %d “, a, b); \ c = a + b; \ printf(“= %d\n”, c); \} while(0)
4. Parenthéser les paramètres
Considérons le petit programme suivant qui définit et utilise une macro effectuant une division entière :#include <stdio.h>À l’exécution, on s’attend à l’affichage du résultat 2 pour la première division (12 / 6) et du résultat 3 pour la deuxième division ((12 + 6) / 6). Mais, on obtient le résultat erroné suivant :#define DIV(a, b, c) do { \ printf(“%d / %d “, a, b); \ c = a / b; \ printf(“= %d\n”, c); \ } while(0)int main(int ac, char *av[]) { int r = 5; int v1 = 12; int v2 = 6; DIV(v1, v2, r); DIV(v1 + 6, v2, r); return 0; }
> ./a.out 12 / 6 = 2Comme d’habitude, lorsque l’on a un doute avec les macros, il faut visualiser la sortie du préprocesseur :18 / 6 = 13
int main(int ac, char *av[])
{
int r = 5;
int v1 = 12;
int v2 = 6;
do { printf(“%d / %d “, v1, v2);
r = v1 / v2;
printf(“= %d\n”, r);
} while(0);
do { printf(“%d / %d “, v1 + 6, v2);
r = v1 + 6 / v2;
printf(“= %d\n”, r);
} while(0);
return 0;
}
On constate que l’on se retrouve confronté à un problème de priorité des opérateurs au niveau du deuxième appel à #define DIV(a, b, c) do { \
printf("%d / %d ", (a), (b)); \
(c) = (a) / (b); \
printf(“= %d\n”, (c)); \
} while(0)
Le deuxième appel à do { printf("%d / %d ", (v1 + 6), (v2));
(r) = (v1 + 6) / (v2);
printf(“= %d\n”, (r));
} while(0);
5. Parenthéser les expressions
Considérons la constante définie sous la forme de la macro CONSTANTE et utilisée comme suit :#include <stdio.h>L’affichage espéré est le résultat de l’opération " 2002 / 2 ", soit 1001. Et pourtant, on obtient 2001. La sortie du préprocesseur montre que le programme génère un problème de priorité des opérateurs comme on l’a vu au § 4 :#define BASE 2000 #define CONSTANTE BASE + 2int main(int ac, char *av[]) { printf("%d\n", CONSTANTE / 2); return 0; }
printf("%d\n", 2000 + 2 / 2);
D’une manière générale, si une macro consiste en une expression (constante, test, opérateur ternaire...), il est prudent de la parenthéser. D’où la correction de l’exemple :
#define BASE(2000)#define CONSTANTE(BASE + 2)
6. Éviter les expressions en paramètres
On a souvent tendance à utiliser une macro comme si c’était une fonction ou alors des évolutions de code peuvent mener au fait qu’une fonction devienne une macro pour des raisons de lisibilité ou d’optimisation. Voici un exemple de programme qui définit et utilise la macro#include <stdio.h>Ce programme est censé afficher les indices des caractères blancs dans son nom. Pourtant, l’indice affiché est erroné ou certains blancs ne sont pas détectés :#define IS_BLANK(c) ((c) == ‘\t’ || (c) == ‘ ‘)int main(int ac, char *av[]) { char *p = av[0]; while(*p) { if (IS_BLANK(*(p++))) { printf(“Blanc à l’indice %d\n", (p - 1) - av[0]); } } return 0; }
> gcc main.c -o "nom avec espaces" > ./nom\ avec\ espaces Blanc à l’indice 5Le seul blanc détecté est indiqué à l’indice 5, alors que dans la chaîne de caractères " ./nom avec espaces ", il y a des blancs aux indices 5 et 10. Ceci est dû au fait que le paramètre
if (((*(p++)) == ‘\t’ || (*(p++)) == ‘ ‘))En d’autres termes, la première comparaison se fait avec le caractère pointé par
#include <stdio.h>
#define IS_BLANK(c) ((c) == ‘\t’ || (c) == ‘ ‘)
int main(int ac, char *av[])
{
char *p = av[0];
while(*p)
{
if (IS_BLANK(*p))
{
printf(“Blanc à l’indice %d\n", p - av[0]);
}
p ++;
}
return 0;
}
Il n’est pas toujours évident de respecter cette règle surtout si #include <stdio.h>La variable locale#define IS_BLANK(c) ({char _c = c;((_c) == ‘\t’ || (_c) == ‘ ‘);})int main(int ac, char *av[]) { char *p = av[0]; while(*p) { if (IS_BLANK(*(p++))) { printf(“Blanc à l’indice %d\n",(p - 1)- av[0]); } } return 0; }
7. Nombre variable d’arguments
La norme ISO C99 permet de définir des macros avec un nombre variable d’arguments. Il existe deux notations :1 #include <stdio.h> 2 3 #define DEBUG(fmt, ...) \ 4 fprintf(stderr, fmt „\n“, __LINE__,__VA_ARGS__) 5 6 #define DEBUG2(fmt,args...) \ 7 fprintf(stderr, fmt „\n“, __LINE__,args) 8 9 int main(int ac, char *av[]) 10 { 11 DEBUG("Nom du programme = %s", av[0]); 12 DEBUG("Message sans arguments"); 13 14 DEBUG2("Nom du programme = %s", av[0]); 15 DEBUG2("Message sans arguments"); 16 17 return 0; 18 }
> gcc main.c main.c: In function "main”: main.c:Les erreurs de compilation viennent de la virgule qui précède la liste vide des arguments variables au deuxième et quatrième appel à12: erreur: expected expression before ")” tokenmain.c:15: erreur: expected expression before ")” token> gcc -E main.c [...] int main(int ac, char *av[]) { fprintf(stderr, „Nom du programme = %s“ „\n“, 11, av[0]);fprintf(stderr, "Message sans arguments" "\n", 12, );fprintf(stderr, "Nom du programme = %s" "\n", 14, av[0]);fprintf(stderr, "Message sans arguments" "\n", 15, );return 0; }
#define DEBUG(fmt, ...) \
fprintf(stderr, fmt "\n", __LINE__, ## __VA_ARGS__)
#define DEBUG2(fmt, args...) \
fprintf(stderr, fmt „\n“, __LINE__, ## args)
8. Les macros spéciales
Il existe de nombreuses macros et notations ayant un rôle particulier pour le préprocesseur. Dans ce paragraphe, seules sont cités les plus utiles ou tout au moins les plus usitées.8.1. __GNUC__
La macro8.2. __LINE__, __FILE__ et __FUNCTION__
Les macros#include <stdio.h>
#define DEBUG(fmt, args...) \
fprintf(stderr, „%s(%s)#%d : „ fmt , \
__FILE__, __FUNCTION__, __LINE__, ## args)
int main(int ac, char *av[])
{
int i;
for (i = 0; i < ac; i ++)
{
DEBUG("Param %d is : %s\n", i, av[i]);
}
return 0;
}
[...]
> gcc debug.c
> ./a.out param1 param2
debug.c(main)#14 : Param 0 is : ./a.out
debug.c(main)#14 : Param 1 is : param1
debug.c(main)#14 : Param 2 is : param2
On notera que 8.3. L’opérateur " # "
La notation " # " permet de convertir un paramètre de macro en chaîne de caractères. Par exemple, voici une fonction qui affiche un numéro de signal Linux grâce à la macro#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#define CASE_SIG(s) case (s) : printf("%s\n", "SIGNAL_" #s); break
void signum(int sig)
{
switch(sig)
{
CASE_SIG(SIGINT);
CASE_SIG(SIGTERM);
CASE_SIG(SIGKILL);
CASE_SIG(SIGTRAP);
CASE_SIG(SIGSEGV);
CASE_SIG(SIGCHLD);
[...]
default : printf(„???\n“);
}
}
int main(int ac, char *av[])
{
if (ac > 1)
{
signum(atoi(av[1]));
}
}
[...]
> gcc signal.c
> ./a.out 5
SIGNAL_SIGTRAP
8.4. L’opérateur " ## "
On a déjà eu une forme d’utilisation de " ## " dans le § 7. Mais, il existe une autre forme d’utilisation où cette directive concatène les éléments lexicaux qui l’entourent pour former une nouvelle unité lexicale. Dans l’exemple suivant, les mots#include <stdio.h>
#define CONSTANTE 233
#define CONCAT(a, b) a##b
int main(int ac, char *av[])
{
printf("%d\n", CONCAT(CONSTA, NTE));
}
[...]
> gcc concat.c
> ./a.out
233
9. Comment éliminer les attributs de GCC
Les attributs sont des extensions spécifiques à GCC pour augmenter les contrôles et contribuer à l’optimisation du code généré. Considérons l’exemple suivant où est décrit l’attribut " format " (pour de plus amples informations sur les attributs de GCC, cf. [4]).1 extern void my_printf1 (const char *fmt, ...); 2 3 extern void my_printf2 (const char *fmt, ...) 4La compilation de ce programme avec l’option__attribute__ ((format (printf, 1, 2))); 5 6 7 void funct(void) 8 { 9 my_printf1("Affichage de %s suivi de %d\n", 46); 10 11 my_printf2("Affichage de %s suivi de %d\n", 46); 12 }
> gcc -c -Wall attribute.c attribute.c: In function "funct”: attribute.c:Si la notion d’attribut est pratique et puissante, elle peut poser problème lorsque le compilateur utilisé n’est pas GCC. Comme la directive11: attention : format "%s” expects type "char *”, but argument 2 has type "int”attribute.c:11: attention : trop peu d’arguments dans le format
#ifndef __GNUC__ #define __attribute__(p) // Rien #endif // __GNUC__Dans cet exemple, la condition de compilation utilise le drapeau
10. Inclusions conditionnelles
Un fichier d’en-tête est inclus dans un fichier source à l’aide de la directive " #include ". Ces fichiers contiennent la plupart du temps des déclarations externes de variables ou fonctions, des définitions de types et de macros. Un fichier d’en-tête peut lui-même inclure d’autres fichiers d’en-tête, car une règle de base en programmation C est de rendre un fichier d’en-tête indépendant. En d’autres termes, si un fichier d’en-tête utilise un type, une macro, une fonction ou une variable, il est recommandé que soit inclus, dans ce fichier, le fichier d’en-tête où se trouve la définition correspondante. Dans l’exemple de la figure 1, le fichier
La compilation de main.c donne les erreurs suivantes :
> gcc -c main.c
In file included from fct.h:1,
from main.c:2:
entier.h:1: erreur: redefinition of typedef "ENTIER”
entier.h:1: erreur: previous declaration of "ENTIER” was here
Le compilateur remonte le fait que le type ENTIER est défini deux fois. La première définition provient du fichier str.h et la seconde du fichier fct.h qui incluent tous deux le fichier entier.h. Cela donne comme résultat que le fichier main.c inclut le fichier entier.h deux fois. Pour résoudre ce problème, on peut utiliser la compilation conditionnelle de sorte à inclure un fichier d’en-tête seulement si une définition qui lui est propre n’est pas déjà définie. Cette définition est généralement faite à partir du nom du fichier. Afin d’illustrer le propos, voici comment le fichier entier.h est modifié pour n’être inclus qu’à la condition que ENTIER_H ne soit pas déjà défini :
#ifndef ENTIER_H #define ENTIER_H typedef int ENTIER; #endif // ENTIER_HCela permet de compiler

Conclusion
Cet article a présenté de nombreuses règles et astuces pour tirer parti au mieux des facilités du préprocesseur C de sorte à contribuer à la robustesse, la portabilité et la facilité de mise au point des programmes. Ce ne sont là qu’un sous-ensemble des possibilités offertes. Le lecteur pourra consulter les liens et références de cet article pour aller plus loin.
Liens
- [1] Extensions de GCC : http://sunsite.ualberta.ca/Documentation/Gnu/gcc-2.95.2/html_chapter/gcc_4.html
- [2] Le préprocesseur C : http://www.game-lab.com/index.php?section=tutorials§ion_id=1&p=tutorial&action=showtut&id=221
- [3] Le préprocesseur C : http://www.redhat.com/docs/manuals/enterprise/RHEL-4-Manual/cpp/index.html
- [4] Attributs de GCC : http://www.unixwiz.net/techtips/gnu-c-attributes.html
- [5] Manuels de GCC : http://gcc.gnu.org/onlinedocs/
- [6] BOULAY (Nicolas), " Le C n’est pas portable ", GLMF 102, février 2008.
- [7] BOULAY (Nicolas), " Le processus de compilation C ", GLMF 103, mars 2008.
- [8] KERNIGHAN (Brian. W.) & RITCHIE (Dennis. M.), " Le langage C ", 2ème édition, Masson, 1990.
Retrouvez cet article dans : Linux Magazine 105





Le paragraphe sur les macros à nombre d’arguments variables n’est pas très clair, il n’existe en effet qu’une seule notation en C99, à savoir __VA_ARGS__, l’autre n’est qu’une extension GCC, à ne plus utiliser, donc.
Cordialement.