Chaque fichier C que vous avez compilé commençait par une ligne comme #include <stdio.h>, et cette ligne n'est pas du C. C'est une instruction destinée à un programme distinct - le préprocesseur - qui s'exécute d'abord, réécrit le texte de votre source, et remet le résultat au compilateur.
Comprendre cette division en deux étapes explique beaucoup du comportement du C : pourquoi les en-têtes sont collés plutôt qu'importés, pourquoi une mauvaise macro produit une erreur sur une ligne parfaitement correcte, et pourquoi un même fichier source peut compiler vers des programmes différents sur des systèmes différents.
Ce qui se passe réellement avant la compilation
Compiler un fichier .c n'est pas une étape. Grossièrement, c'en est quatre :
- Le prétraitement - obéir à chaque ligne commençant par
#, produisant un grand texte source développé (une unité de traduction). - La compilation - transformer ce texte en assembleur, puis en fichier objet.
- L'assemblage - produire du code machine.
- L'édition de liens - joindre les fichiers objets et les bibliothèques en un exécutable.
Le préprocesseur travaille purement sur du texte. Il ne sait pas ce qu'est une variable, ce qu'est un type, ni si vos accolades s'équilibrent. Il voit des caractères et des jetons, en remplace certains par d'autres, et passe à la suite. Tout ce qui est étrange dans les macros découle de ce seul fait.
Au moment où le compilateur lit ce programme, il n'y a plus aucun GREETING nulle part. Le préprocesseur l'a déjà remplacé par le littéral "Bonjour depuis une macro", exactement comme si vous l'aviez tapé.
La famille des directives
Toute directive du préprocesseur commence par # comme premier caractère non blanc de sa ligne. Il n'y a pas de point-virgule, et la directive se termine en fin de ligne sauf si vous la prolongez avec une barre oblique inverse.
| Directive | Ce qu'elle fait |
|---|---|
#include | Coller le contenu d'un autre fichier |
#define | Définir une macro (une substitution de texte) |
#undef | Supprimer une définition de macro |
#ifdef, #ifndef | Garder le code suivant seulement si une macro est (n'est pas) définie |
#if, #elif, #else, #endif | Garder du code selon une expression constante |
#error | Arrêter la compilation avec un message |
#pragma | Instruction propre au compilateur, comme #pragma once |
#line | Changer le numéro de ligne rapporté (rare) |
Trois d'entre elles ont leur propre page : les macros couvrent #define en profondeur, la compilation conditionnelle couvre la famille #if, et les fichiers d'en-tête couvrent l'usage de #include pour structurer un programme multi-fichiers.
#include : chevrons contre guillemets
#include fait exactement une chose : il remplace sa propre ligne par tout le contenu du fichier nommé. Ce fichier inclus est lui-même prétraité, donc ses propres lignes #include se développent aussi.
#include <stdio.h> /* chercher dans les repertoires d'inclusion systeme */
#include "config.h" /* chercher d'abord dans le repertoire de ce fichier */
La différence est l'ordre de recherche :
<chevrons>regardent dans les répertoires d'inclusion standard du compilateur -/usr/include, les dossiers propres à la chaîne d'outils, plus tout ce que vous ajoutez avec-I. C'est pour les en-têtes de bibliothèque."guillemets"regardent d'abord dans le répertoire contenant le fichier qui inclut, puis se rabattent sur la même liste que les chevrons. C'est pour les en-têtes que vous avez écrits.
Les deux formes fonctionnent pour l'un ou l'autre type d'en-tête sur la plupart des compilateurs, mais la convention porte un sens : les chevrons disent « c'est l'en-tête de quelqu'un d'autre », les guillemets disent « c'est le mien ». Les mélanger, c'est ainsi qu'un projet finit par inclure un en-tête système périmé au lieu du sien.
Parce que l'inclusion est un collage textuel, inclure deux fois le même en-tête le colle deux fois - c'est pourquoi les en-têtes ont besoin de gardes d'inclusion. C'est la première chose que corrige la page fichiers d'en-tête.
#define : de la substitution de texte, rien de plus
#define NOM remplacement dit au préprocesseur : à partir d'ici, partout où le jeton NOM apparaît, mets remplacement à la place.
Remarquez ce qui ne se passe pas ici. MAX_USERS n'a pas de type. Il n'est stocké nulle part. Il ne peut pas être inspecté dans un débogueur. C'est une règle de rechercher-remplacer, et après prétraitement le programme contient littéralement printf("%s autorise %d utilisateurs\n", "Coddy", 100);.
Cela signifie aussi que la substitution est aveugle. Ceci compile, et fait quelque chose de surprenant :
#define SIZE 5 + 1
int arr[SIZE]; /* ok : int arr[5 + 1]; */
int total = SIZE * 2; /* 5 + 1 * 2 == 7, pas 12 */
La solution - des parenthèses autour de tout - est la règle centrale de la page macros, avec les macros qui prennent des arguments.
Un #define sans texte de remplacement définit le nom comme « présent mais vide ». C'est inutile en substitution et essentiel comme drapeau :
#define DEBUG /* defini, se developpe en rien */
Le code peut alors demander si DEBUG existe avec #ifdef.
Voir la sortie du préprocesseur
La meilleure façon de se construire une intuition est de regarder ce que le préprocesseur a réellement produit. gcc -E s'arrête après le prétraitement et affiche le résultat :
gcc -E hello.c
Pour un fichier qui inclut <stdio.h>, cela fait de 700 à 30 000 lignes selon votre système - presque tout étant le contenu de l'en-tête lui-même. Pour ne voir que votre partie, prenez la fin :
gcc -E hello.c | tail -20
Essayez sur un fichier comme celui-ci :
#define SQUARE(x) ((x) * (x))
#define LIMIT 10
int main(void) {
int n = SQUARE(LIMIT);
return n;
}
La fin de la sortie montre :
int main(void) {
int n = ((10) * (10));
return n;
}
Toutes les macros ont disparu ; seul le texte substitué reste. Quand une macro se comporte mal, cette commande vous dit pourquoi en quelques secondes, et cela vaut mieux que deviner. Deux compagnons utiles : gcc -dM -E - < /dev/null liste toutes les macros que votre compilateur prédéfinit, et gcc -E -P fichier.c supprime le bruit des marqueurs de ligne.
Pourquoi les erreurs pointent la mauvaise ligne
Parce que le compilateur voit du texte développé, une faute dans une macro est signalée là où la macro a été utilisée, pas là où elle a été écrite :
#define HALF(x) (x / 2
int main(void) {
int y = HALF(8); /* erreur signalee ici */
return 0;
}
La parenthèse manquante est dans la ligne #define, mais le compilateur se plaint de la ligne contenant HALF(8), souvent avec un message sur un jeton inattendu qui n'a aucun sens dans le contexte. Quand une erreur semble impossible, développez le fichier avec gcc -E et lisez la vraie ligne.
Les compilateurs modernes aident : GCC et clang affichent une note « in expansion of macro » renvoyant à la définition. Compilez avec -Wall -Wextra pour voir réellement ces notes.
Les macros prédéfinies
Le préprocesseur fournit lui-même certaines macros. Elles sont réellement utiles pour le diagnostic :
__FILE__ et __LINE__ se développent en nom de fichier et numéro de ligne courants, c'est ainsi que les macros d'assertion et de journalisation signalent où quelque chose a mal tourné. (__func__ est légèrement différent - c'est un vrai identifiant fourni par le compilateur, pas une macro du préprocesseur, mais il s'utilise de la même façon.)
Les compilateurs prédéfinissent aussi des macros de plateforme comme __linux__, _WIN32 et __APPLE__. Le code qui doit différer par système les teste avec #ifdef, ce qui est le sujet de la compilation conditionnelle.
Ce qu'il faut retenir
Le préprocesseur est petit et bête, et les deux sont voulus. Il vous donne trois pouvoirs - tirer un fichier, substituer du texte, activer ou désactiver du code - et aucune vérification de types pour les accompagner.
Ce compromis explique le conseil standard : privilégiez d'abord les fonctionnalités du langage : utilisez const int ou une enum plutôt que #define pour les constantes quand vous le pouvez, et une vraie fonction plutôt qu'une macro à la fonction. Là où le préprocesseur est réellement le bon outil - en-têtes, interrupteurs de portabilité, configuration à la compilation - il est irremplaçable.
Ensuite, les macros prennent #define au sérieux : arguments, règles de parenthèses, et les pièges qui viennent avec la substitution de texte dans du code que vous n'avez pas écrit.
Questions fréquentes
Qu'est-ce que le préprocesseur en C ?
Une étape de traitement de texte qui s'exécute avant la compilation. Il obéit aux lignes commençant par # - collant le contenu des fichiers d'en-tête à la place de #include, substituant les macros définies avec #define, et supprimant ou gardant du code selon #if/#ifdef. Le compilateur ne voit jamais que le résultat, jamais votre fichier d'origine.
Quelle est la différence entre #include <stdio.h> et #include "monfichier.h" ?
Les chevrons explorent les répertoires d'inclusion système du compilateur, où vivent les en-têtes de la bibliothèque standard. Les guillemets explorent d'abord le répertoire du fichier courant, puis se rabattent sur les chemins système. Utilisez les chevrons pour les en-têtes de bibliothèque et les guillemets pour les en-têtes que vous avez écrits.
Comment voir ce que le préprocesseur a produit ?
Lancez gcc -E fichier.c pour vous arrêter après le prétraitement et afficher la source développée. Sur un fichier incluant <stdio.h>, la sortie fait des milliers de lignes, alors filtrez-la : gcc -E fichier.c | tail -30 montre juste votre propre code avec toutes les macros déjà substituées.
#include est-il une instruction C ?
Non. Les directives ne font pas partie du langage C proprement dit - elles ont leur propre syntaxe par ligne, ne prennent pas de point-virgule, et ont disparu quand le compilateur analyse votre programme. C'est pourquoi une erreur dans une macro apparaît comme une erreur déroutante sur une ligne qui semble correcte.