Menu

Compilation conditionnelle en C : #ifdef, #ifndef, #if et -D

Compilez du code différent selon la version avec #ifdef, #ifndef, #if, #elif et #else - interrupteurs de débogage, branches par plateforme, définition de macros en ligne de commande avec -D, et usage de #if 0 pour désactiver du code.

Cette page contient des éditeurs exécutables - modifiez, exécutez et voyez la sortie instantanément.

La compilation conditionnelle permet à un seul fichier source de devenir plusieurs programmes différents. Le préprocesseur décide, avant que le compilateur ne tourne, quels blocs de texte survivent - donc le code dans une branche qui échoue n'est pas simplement sauté à l'exécution, il est supprimé. Il n'est jamais analysé, ne contribue donc en rien à l'exécutable, et peut même contenir des constructions que le compilateur courant rejetterait.

C'est là la différence avec un if ordinaire. Les deux choisissent entre des chemins ; seul l'un des deux choisit avant que le programme n'existe.

#ifdef et #ifndef

La question la plus simple est de savoir si une macro est définie :

Supprimez la ligne #define DEBUG et chaque affichage de débogage disparaît de la construction - pas désactivé, disparu. Lancez-le une fois tel quel, puis retirez cette ligne et relancez.

#ifndef est la négation : garder le bloc seulement si la macro n'est pas définie. Son usage le plus courant est la garde d'inclusion vue dans fichiers d'en-tête, et c'est aussi ainsi qu'on fournit une valeur par défaut qu'un appelant peut remplacer :

Chaque #ifdef et #ifndef doit être fermé par #endif. Annoter la fermeture dans les longs fichiers - #endif /* DEBUG */ - fait gagner un temps réel plus tard.

#if, #elif, #else

#if prend une expression entière constante et garde le bloc quand elle est non nulle. Cela rend possibles les comparaisons de version et de niveau :

Changez LOG_LEVEL en 0 ou 3 et relancez pour voir le programme lui-même changer de forme.

L'expression est évaluée par le préprocesseur, ce qui signifie qu'elle ne peut utiliser que des constantes entières, des opérateurs arithmétiques et de comparaison, et des macros qui se développent en cela. Elle ne voit pas sizeof, les valeurs d'enum, les variables, ni rien qui demande le compilateur. Une macro non définie dans un #if s'évalue à 0 plutôt que d'être une erreur, ce qui est pratique et parfois surprenant :

#if FEATURE_X        /* FEATURE_X n'est definie nulle part -> 0 -> bloc supprime */

defined(NOM) transforme la question de #ifdef en quelque chose d'utilisable dans un #if, ce qui permet de combiner les tests :

#if defined(DEBUG) && !defined(NDEBUG)
    /* build de debug, et les assertions sont actives */
#endif

#if defined(LINUX) || defined(BSD)
    /* l'un ou l'autre systeme de type unix */
#endif

#if defined(X) et #ifdef X signifient la même chose ; prenez le premier quand vous avez besoin de &&, || ou !.

Définir des macros en ligne de commande

La vraie puissance de ces interrupteurs, c'est que le source n'a pas besoin de changer du tout. gcc -D définit une macro pour toute la compilation :

gcc -DDEBUG program.c -o program        # DEBUG defini a 1
gcc -DLOG_LEVEL=3 program.c -o program  # valeur precise
gcc -DDEBUG -DBUFFER_SIZE=512 a.c b.c -o app

-DNOM seul équivaut à #define NOM 1. -U NOM en annule une, ce qui compte quand un en-tête définit quelque chose que vous voulez désactiver.

L'arrangement habituel est donc : le source contient des valeurs par défaut gardées par #ifndef, et la commande de construction sélectionne une configuration.

/* config.h */
#ifndef LOG_LEVEL
#define LOG_LEVEL 1        /* discret par defaut */
#endif

#ifndef MAX_CONNECTIONS
#define MAX_CONNECTIONS 64
#endif
# build de developpement
gcc -DDEBUG -DLOG_LEVEL=3 -Wall -Wextra -g src/*.c -o app-dev

# build de production
gcc -DNDEBUG -O2 src/*.c -o app

NDEBUG est normalisé : le définir désactive chaque assert() du programme, car <assert.h> est lui-même écrit en compilation conditionnelle. C'est le motif en miniature - un en-tête qui se compile en choses différentes selon ce que le build a défini.

Interrupteurs de plateforme

Les compilateurs prédéfinissent des macros identifiant le système cible, de sorte qu'un seul fichier source peut appeler la bonne API sur chacun :

#if defined(_WIN32)
    #include <windows.h>
    #define CLEAR_SCREEN "cls"
#elif defined(__APPLE__)
    #include <unistd.h>
    #define CLEAR_SCREEN "clear"
#elif defined(__linux__)
    #include <unistd.h>
    #define CLEAR_SCREEN "clear"
#else
    #error "Plateforme non prise en charge"
#endif

Les plus courantes : _WIN32 (définie sur Windows 32 et 64 bits), _WIN64, __linux__, __APPLE__, __unix__, __ANDROID__. L'identité du compilateur a son propre jeu - __GNUC__, __clang__, _MSC_VER - et l'architecture aussi - __x86_64__, __aarch64__.

Une version exécutable qui rapporte ce sur quoi elle a été construite :

#error mérite d'être connu pour lui-même : il arrête la compilation avec votre message. Terminer une chaîne de plateformes par #else / #error "Plateforme non prise en charge" transforme un build silencieusement faux en échec clair en haut du journal de construction.

Pour lister toutes les macros que votre compilateur prédéfinit :

gcc -dM -E - < /dev/null

Cette sortie est la réponse faisant autorité à « que puis-je tester sur cette machine ».

#if 0 pour désactiver du code

Commenter un bloc avec /* ... */ échoue dès que le bloc contient lui-même un commentaire, car les commentaires C ne s'imbriquent pas - le premier */ intérieur termine le commentaire extérieur et tout ce qui suit devient du code égaré. #if 0 n'a pas ce problème :

#if 0
    /* Toute cette zone est supprimee, commentaires compris. */
    legacy_init();
    int n = old_calculation(42);  /* meme ce commentaire ne pose pas de probleme */
    report(n);
#endif

Basculez en #if 1 pour ramener le code. Les éditeurs continuent de le colorer comme du C, et vous pouvez imbriquer un #if 0 dans un autre #if.

C'est un outil de débogage, pas un système de rangement. Le code posé dans un #if 0 n'est jamais compilé, il pourrit donc en silence - il ne construira plus le jour où quelqu'un rebasculera l'interrupteur. Utilisez-le pendant que vous dichotomisez un problème, puis supprimez le bloc et laissez le gestionnaire de versions s'en souvenir.

Là où la compilation conditionnelle tourne mal

Trois modes de défaillance expliquent l'essentiel de la douleur.

Le code jamais compilé n'est jamais vérifié. Une faute de frappe dans une branche #ifdef inactive est invisible jusqu'à ce que quelqu'un construise cette configuration - peut-être des mois plus tard, peut-être en CI sur une plateforme que vous n'avez pas. Si un projet a des branches de plateforme intéressantes, construisez-les toutes régulièrement.

Entrelacer #ifdef avec le flux de contrôle devient illisible très vite. Ce genre de chose est difficile à suivre :

if (ready) {
#ifdef FAST_PATH
    fast_send(buf);
} else {
#endif
    slow_send(buf);
}

Des accolades ouvertes dans une branche et fermées dans une autre sont légales et affreuses. Préférez des conditionnelles qui enveloppent des fonctions entières, et choisissez entre elles :

#ifdef FAST_PATH
static void send_data(const char *buf) { fast_send(buf); }
#else
static void send_data(const char *buf) { slow_send(buf); }
#endif

Un if à l'exécution est souvent meilleur. Si les deux branches compileraient partout, un if (debug_enabled) ordinaire garde les deux chemins vérifiés au niveau des types, testables et commutables sans recompiler. Réservez le préprocesseur à ce que lui seul peut faire : du code qui réellement ne peut pas compiler sur l'autre plateforme.

Pour le motif qui combine tout cela - un en-tête de valeurs par défaut, un -D par build et des branches de plateforme entourées de gardes d'inclusion - voir fichiers d'en-tête ; pour #define lui-même, les macros.

Questions fréquentes

Qu'est-ce que la compilation conditionnelle en C ?

L'usage de directives du préprocesseur pour garder ou jeter des blocs de code source avant que le compilateur ne tourne. Le code dans un #ifdef qui échoue est entièrement supprimé du texte - il n'est jamais analysé, jamais compilé et n'apparaît jamais dans l'exécutable. C'est ainsi qu'un seul fichier source prend en charge plusieurs plateformes ou types de build.

Quelle est la différence entre #ifdef et #if ?

#ifdef NOM demande seulement si la macro existe, quelle que soit sa valeur. #if expression évalue une expression entière constante, et peut donc comparer des valeurs : #if VERSION >= 3. Utilisez #ifdef pour des drapeaux simplement présents ou absents, #if quand la valeur compte. #if defined(NOM) combine les deux et s'assemble avec && et ||.

Comment définir une macro en ligne de commande avec gcc ?

Utilisez -D : gcc -DDEBUG program.c définit DEBUG comme si le fichier commençait par #define DEBUG 1, et -DMAX=50 lui donne une valeur précise. C'est ainsi que les systèmes de construction activent des fonctionnalités sans toucher au source, et -U NOM en annule une.

Pourquoi utiliser #if 0 plutôt que de commenter le code ?

Parce que les commentaires /* ... */ ne s'imbriquent pas - le premier */ à l'intérieur de la zone termine le commentaire trop tôt et le reste devient du code cassé. #if 0 ... #endif supprime n'importe quelle quantité de code contenant n'importe quel nombre de commentaires, garde la coloration syntaxique vivante, et se rebascule trivialement en #if 1.

Coddy programming languages illustration

Apprendre à coder avec Coddy

COMMENCER