🎩 The Preprocessor
Conditional compilation: code that decides to exist
▶ Open the interactive lesson — free, no signupOne game's source code can build on both Windows and Linux, even though each system needs code the other can't even compile — and the chatty "debug mode" messages developers rely on vanish completely from the version players download, at zero cost in speed. Both feats are the same trick: code that gets erased before the compiler ever looks at it. Soon you'll be flipping whole features on and off with a single compiler flag.
An if statement chooses at runtime. The preprocessor's #if chooses at build time — while your program is being compiled — and the losing side isn't skipped, it's deleted before the compiler ever sees it. That deleted code can call functions that only exist on another operating system, sit half-written, or make no sense at all: deleted text can't cause errors.
The directive family
| directive | meaning |
|---|---|
#if expr | keep block if the constant expression is non-zero |
#ifdef NAME | shorthand for #if defined(NAME) |
#ifndef NAME | shorthand for #if !defined(NAME) (hello, include guards) |
#elif expr | else-if chain |
#else / #endif | fallback / mandatory closer |
#elifdef / #elifndef | C23 shorthands for #elif defined / #elif !defined |
The expression after #if is an integer constant expression evaluated by the preprocessor: arithmetic, comparisons, &&/||, and the special operator defined(NAME), which is 1 if the macro exists (regardless of its value). No sizeof, no casts, no floats, no enum constants — the preprocessor knows only macros and integers.
Sneaky rule: in a #if expression, any identifier that is not a defined macro silently becomes 0. So #if VERSOIN >= 2 (typo!) is always false — no error, no warning by default. GCC's -Wundef catches this; turn it on.
🧠 Checkpoint: In #if MY_FLAG == 1, what happens if MY_FLAG was never defined?
- Preprocessor error: unknown identifier
- The identifier is treated as 0, so the block is skipped
- The block is kept as a safe default
- The compiler asks the linker
Show answer
The identifier is treated as 0, so the block is skipped — Undefined identifiers in #if expressions quietly evaluate to 0 — a rich source of typo bugs. Compile with -Wundef to get warned.
Platform detection
Compilers predefine macros that identify the target OS — the standard way to write portable code with unportable pieces:
#include <stdio.h>
#if defined(_WIN32)
#define PLATFORM "Windows"
#elif defined(__APPLE__)
#define PLATFORM "macOS"
#elif defined(__linux__)
#define PLATFORM "Linux"
#else
#define PLATFORM "something exotic"
#endif
int main(void) {
printf("compiled for: %s\n", PLATFORM);
return 0;
}▶ This spot has an interactive flow widget — open the interactive lesson to play with it.
Only ONE of those branches survives preprocessing — on Linux, the compiler literally never sees the string "Windows". That's why the Windows branch could call <windows.h> functions that don't exist on Linux, and still build fine there.
🧠 Checkpoint: On Linux, what does the compiler (not the preprocessor) see of the _WIN32 branch?
- It sees it but skips code generation
- It sees it as a comment
- Nothing — the text was deleted before parsing
- It compiles it into a disabled section
Show answer
Nothing — the text was deleted before parsing — That is the superpower of conditional compilation: the dead branch can reference Windows-only headers and functions, because on Linux that text simply no longer exists after preprocessing.
Debug builds: -D defines macros from the command line
The most-used pattern in all of C: log verbosely in development, compile the logging away entirely in release. The switch is gcc -DDEBUG, which acts exactly like a #define DEBUG 1 at the top of every file:
#include <stdio.h>
#ifdef DEBUG
#define DBG(...) fprintf(stderr, "[debug] " __VA_ARGS__)
#else
#define DBG(...) ((void)0) /* expands to nothing useful */
#endif
int main(void) {
int items = 3;
DBG("starting up, items=%d\n", items);
printf("processed %d items\n", items);
DBG("done\n");
return 0;
}$ gcc app.c -o app && ./app processed 3 items # release build: DBG lines cost literally zero instructions $ gcc -DDEBUG app.c -o app && ./app [debug] starting up, items=3 processed 3 items [debug] done
In the release build the DBG calls expand to ((void)0) — a statement that does nothing and costs nothing. Zero runtime overhead, not even a branch. You can also pass values (-DLEVEL=3) and un-define with -U.
🧠 Checkpoint: What does the -DDEBUG compiler flag do?
- Enables the debugger
- Acts like
#define DEBUG 1before the first line of each file - Disables optimizations
- Defines DEBUG only inside main()
Show answer
Acts like #define DEBUG 1 before the first line of each file — -DNAME defines NAME as 1 (or -DNAME=value for a specific value) for the whole translation unit — the command-line twin of #define. -UNAME un-defines. The debugger flag is -g; optimizations are -O.
#if 0: the nuclear comment
#if 0
/* old algorithm — kept for reference */
total = slow_sum(data, n); /* O(n^2), ouch */
#endif
total = fast_sum(data, n);
/* trying the same with a comment would die here ^ at
the FIRST */ ... because block comments do not nest */C's /* */ comments don't nest — commenting out code that contains comments breaks at the first */. #if 0 ... #endif blocks nest with other conditionals and swallow (almost) anything, making them the standard way to disable a chunk of code temporarily. Just don't ship code full of them.
Feature-test macros: the reverse direction
Conditionals also flow the other way: you define macros to ask system headers for more. POSIX functions like getline or clock_gettime are hidden behind guards inside glibc's headers; defining _POSIX_C_SOURCE 200809L (or _GNU_SOURCE for everything) before any #include unlocks them. If a man page mentions a feature-test macro requirement, this is what it means.
Try it
▶ This spot has an interactive editor widget — open the interactive lesson to play with it.
Conditionals decide what compiles — the next lesson covers the directives that talk back: errors, warnings, and pragmas.
▶ Practice this lesson interactively (with live gcc)