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.