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.
#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.
#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 — correctConditional 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.
#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