thecodex.expert · The Codex Family of Knowledge
C

The Preprocessor

A macro without enough parentheses is a genuinely common, real bug — the preprocessor doesn’t know about operator precedence, it just substitutes text exactly as written.

C17 / C23 Pure text substitution Last verified:
Canonical Definition

Before a compiler ever parses C syntax, the preprocessor runs a separate, earlier pass over the source text, handling every line starting with #. #include pastes in another file's contents; #define creates a text-substitution macro (either a simple constant or a function-like macro with parameters); #ifdef/#ifndef/#endif conditionally include or exclude blocks of code entirely. Critically, the preprocessor operates on raw text with no understanding of C's grammar, types, or operator precedence — it substitutes exactly what was written, which is the root cause of several classic macro bugs.

Object-like and function-like macros

A function-like macro looks like a function call but is actually pure text substitution — no type checking, no real function call overhead, and none of the safety a real function provides.

Cbasic_macros.c
#define MAX_SIZE 100           // object-like macro: a simple text substitution
#define SQUARE(x) ((x) * (x))   // function-like macro: parameters, still just text substitution

int arr[MAX_SIZE];               // becomes: int arr[100];
int y = SQUARE(5);                // becomes: int y = ((5) * (5));

The classic missing-parentheses bug

Without the extra parentheses, SQUARE(1+2) expands to 1+2 * 1+2, which by normal operator precedence evaluates to 5, not 9 — the preprocessor has no concept of "the whole expression passed as x," it substitutes the literal text 1+2 everywhere x appeared.

Cmacro_bug.c
#define BROKEN_SQUARE(x) x * x   // missing parentheses — a real, classic bug

int result = BROKEN_SQUARE(1 + 2);
// expands to: 1 + 2 * 1 + 2
// = 1 + 2 + 2 = 5, NOT 9 — because + has lower precedence than *

#define SAFE_SQUARE(x) ((x) * (x))   // correctly parenthesized
int correct = SAFE_SQUARE(1 + 2);
// expands to: ((1 + 2) * (1 + 2)) = 9 — correct

Conditional compilation: #ifdef / #ifndef

This is how the same codebase can compile differently depending on the target platform or build configuration — code inside an inactive branch isn't just skipped at runtime, it's never even seen by the actual compiler.

Cconditional_compile.c
#ifdef DEBUG
    printf("Debug: value is %d\n", x);   // only compiled in if DEBUG is defined
#endif

#ifdef _WIN32
    // Windows-specific code
#elif defined(__linux__)
    // Linux-specific code
#else
    // fallback for everything else
#endif

Sources

1
ISO/IEC 9899 (C Standard), "Preprocessing directives," cppreference.com/w/c/preprocessor.