Menu

Bedingte Übersetzung in C: #ifdef, #ifndef, #if und -D

Mit #ifdef, #ifndef, #if, #elif und #else unterschiedlichen Code für unterschiedliche Builds übersetzen - Debug-Schalter, Plattformzweige, Makros per -D auf der Kommandozeile definieren und Code mit #if 0 abschalten.

Diese Seite enthält ausführbare Editoren - bearbeiten, ausführen und Ausgabe sofort sehen.

Bedingte Übersetzung lässt eine Quelldatei zu mehreren verschiedenen Programmen werden. Der Präprozessor entscheidet, bevor der Compiler läuft, welche Textblöcke überleben - Code in einem nicht erfüllten Zweig wird also nicht bloß zur Laufzeit übersprungen, er wird gelöscht. Er wird nie geparst, trägt also nichts zur ausführbaren Datei bei und darf sogar Konstrukte enthalten, die der aktuelle Compiler ablehnen würde.

Das ist der Unterschied zu einem gewöhnlichen if. Beide wählen zwischen Pfaden; nur eines davon wählt, bevor das Programm existiert.

#ifdef und #ifndef

Die einfachste Frage ist, ob ein Makro überhaupt definiert ist:

Lösche die Zeile #define DEBUG, und jede Debug-Ausgabe verschwindet aus dem Build - nicht abgeschaltet, weg. Führe es einmal so aus, entferne dann diese Zeile und führe es erneut aus.

#ifndef ist die Verneinung: Behalte den Block nur, wenn das Makro nicht definiert ist. Seine häufigste Verwendung ist der Include-Schutz, den Header-Dateien behandeln, und es ist auch die Art, einen Standardwert bereitzustellen, den ein Aufrufer überschreiben kann:

Jedes #ifdef und #ifndef muss mit #endif geschlossen werden. Den Abschluss in langen Dateien zu beschriften - #endif /* DEBUG */ - spart später echte Zeit.

#if, #elif, #else

#if nimmt einen konstanten Ganzzahlausdruck entgegen und behält den Block, wenn er ungleich null ist. Das macht Versions- und Stufenvergleiche möglich:

Ändere LOG_LEVEL auf 0 oder 3 und führe es erneut aus, um zu sehen, wie das Programm selbst seine Gestalt ändert.

Der Ausdruck wird vom Präprozessor ausgewertet, er darf also nur Ganzzahlkonstanten, Rechen- und Vergleichsoperatoren sowie Makros enthalten, die sich dazu expandieren. Er kann sizeof, enum-Werte, Variablen oder irgendetwas, das den Compiler braucht, nicht sehen. Ein undefiniertes Makro in einem #if wertet zu 0 aus, statt ein Fehler zu sein, was praktisch und gelegentlich überraschend ist:

#if FEATURE_X        /* FEATURE_X ist nirgends definiert -> 0 -> Block verworfen */

defined(NAME) verwandelt die #ifdef-Frage in etwas, das in einem #if benutzbar ist, sodass du Prüfungen kombinieren kannst:

#if defined(DEBUG) && !defined(NDEBUG)
    /* Debug-Build, und Zusicherungen sind an */
#endif

#if defined(LINUX) || defined(BSD)
    /* eines der unix-artigen Systeme */
#endif

#if defined(X) und #ifdef X bedeuten dasselbe; greife zum Ersten, wenn du &&, || oder ! brauchst.

Makros von der Kommandozeile definieren

Die eigentliche Stärke dieser Schalter ist, dass sich der Quelltext gar nicht ändern muss. gcc -D definiert ein Makro für die gesamte Übersetzung:

gcc -DDEBUG program.c -o program        # DEBUG als 1 definiert
gcc -DLOG_LEVEL=3 program.c -o program  # bestimmter Wert
gcc -DDEBUG -DBUFFER_SIZE=512 a.c b.c -o app

-DNAME allein entspricht #define NAME 1. -U NAME hebt eine Definition auf, was zählt, wenn ein Header etwas definiert, das du aus haben willst.

Die übliche Anordnung ist also: Der Quelltext enthält per #ifndef geschützte Standardwerte, und der Build-Befehl wählt eine Konfiguration.

/* config.h */
#ifndef LOG_LEVEL
#define LOG_LEVEL 1        /* standardmäßig leise */
#endif

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

# Release-Build
gcc -DNDEBUG -O2 src/*.c -o app

NDEBUG ist standardisiert: Es zu definieren schaltet jedes assert() im Programm ab, weil <assert.h> selbst mit bedingter Übersetzung geschrieben ist. Das ist das Muster im Kleinen - ein Header, der sich je nach dem, was der Build definiert hat, zu etwas anderem übersetzt.

Plattformschalter

Compiler definieren Makros vor, die das Zielsystem kennzeichnen, sodass eine Quelldatei auf jedem die richtige API aufrufen kann:

#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 "Nicht unterstützte Plattform"
#endif

Die geläufigen: _WIN32 (auf 32- und 64-Bit-Windows definiert), _WIN64, __linux__, __APPLE__, __unix__, __ANDROID__. Die Compiler-Identität hat ihren eigenen Satz - __GNUC__, __clang__, _MSC_VER - und die Architektur ebenfalls - __x86_64__, __aarch64__.

Eine lauffähige Version, die berichtet, worauf sie gebaut wurde:

#error ist für sich wissenswert: Es stoppt die Übersetzung mit deiner Meldung. Eine Plattformkette mit #else / #error "Nicht unterstützte Plattform" zu beenden macht aus einem stillen Fehlbau einen klaren Fehlschlag am Anfang des Build-Protokolls.

Um jedes Makro aufzulisten, das dein Compiler vordefiniert:

gcc -dM -E - < /dev/null

Diese Ausgabe ist die maßgebliche Antwort auf „worauf kann ich auf dieser Maschine prüfen".

#if 0 zum Abschalten von Code

Einen Block mit /* ... */ auszukommentieren scheitert in dem Moment, in dem der Block selbst einen Kommentar enthält, denn C-Kommentare lassen sich nicht verschachteln - das erste */ darin beendet den äußeren Kommentar, und alles danach wird zu verirrtem Code. #if 0 hat dieses Problem nicht:

#if 0
    /* Dieser ganze Bereich wird entfernt, Kommentare inklusive. */
    legacy_init();
    int n = old_calculation(42);  /* sogar dieser Kommentar ist in Ordnung */
    report(n);
#endif

Dreh es auf #if 1, um den Code zurückzuholen. Editoren heben ihn weiterhin als C hervor, und du kannst ein #if 0 in ein anderes #if verschachteln.

Es ist ein Debugging-Werkzeug, kein Ablagesystem. Code in einem #if 0 wird nie übersetzt und verrottet daher still - er baut nicht mehr, wenn jemand den Schalter umlegt. Nutze es, während du ein Problem eingrenzt, und lösche den Block dann und überlass ihn der Versionsverwaltung.

Wo bedingte Übersetzung schiefgeht

Drei Fehlerarten machen den größten Teil des Schmerzes aus.

Code, der nie übersetzt wird, wird nie geprüft. Ein Tippfehler in einem inaktiven #ifdef-Zweig ist unsichtbar, bis jemand diese Konfiguration baut - womöglich Monate später, womöglich in der CI auf einer Plattform, die du nicht hast. Wenn ein Projekt interessante Plattformzweige hat, baue sie alle regelmäßig.

#ifdef mit Kontrollfluss zu verschränken wird schnell unlesbar. So etwas ist schwer zu durchdenken:

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

Klammern, die in einem Zweig geöffnet und in einem anderen geschlossen werden, sind erlaubt und schrecklich. Bevorzuge Bedingungen, die ganze Funktionen umschließen, und wähle zwischen ihnen:

#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

Ein if zur Laufzeit ist oft besser. Wenn beide Zweige überall übersetzen würden, hält ein gewöhnliches if (debug_enabled) beide Pfade typgeprüft, testbar und ohne Neubau umschaltbar. Behalte den Präprozessor für das, was nur er kann: Code, der auf der anderen Plattform wirklich nicht übersetzen kann.

Für das Muster, das all das verbindet - ein Header mit Standardwerten, ein -D pro Build und Plattformzweige in Include-Schutz gewickelt - siehe Header-Dateien; zu #define selbst siehe Makros.

Häufig gestellte Fragen

Was ist bedingte Übersetzung in C?

Präprozessor-Direktiven zu nutzen, um Blöcke des Quelltextes zu behalten oder zu verwerfen, bevor der Compiler läuft. Code in einem nicht erfüllten #ifdef wird vollständig aus dem Text gelöscht - er wird nie geparst, nie übersetzt und taucht nie in der ausführbaren Datei auf. So unterstützt eine Quelldatei mehrere Plattformen oder Build-Varianten.

Was ist der Unterschied zwischen #ifdef und #if?

#ifdef NAME fragt nur, ob das Makro existiert, unabhängig von seinem Wert. #if ausdruck wertet einen konstanten Ganzzahlausdruck aus und kann daher Werte vergleichen: #if VERSION >= 3. Nimm #ifdef für Flags, die bloß vorhanden oder abwesend sind, und #if, wenn der Wert zählt. #if defined(NAME) verbindet beides und lässt sich mit && und || kombinieren.

Wie definiere ich mit gcc ein Makro auf der Kommandozeile?

Mit -D: gcc -DDEBUG program.c definiert DEBUG, als stünde am Anfang der Datei #define DEBUG 1, und -DMAX=50 gibt ihm einen bestimmten Wert. So schalten Build-Systeme Funktionen ein, ohne den Quelltext zu ändern, und -U NAME hebt eine Definition auf.

Warum #if 0 statt Code auszukommentieren?

Weil /* ... */-Kommentare sich nicht verschachteln lassen - das erste */ im Bereich beendet den Kommentar zu früh und der Rest wird zu kaputtem Code. #if 0 ... #endif entfernt beliebig viel Code mit beliebig vielen Kommentaren darin, erhält die Syntaxhervorhebung und lässt sich trivial zu #if 1 zurückdrehen.

Coddy programming languages illustration

Lerne mit Coddy zu programmieren

LOS GEHT'S