The Preprocessor
# are directives. This textual phase is C's answer to modules and metaprogramming — and its most abused corner. Understand its model (pure text substitution, zero types, zero scopes) and you will write macros that help instead of hurt.
The Compilation Pipeline
Compilation is four stages. The preprocessor runs first, on raw text; only its output is C. That is why a macro can never check types and a #define can be passed arguments — and why misuse is so easy to trigger.
Figure 1 — four stages; the preprocessor is pure text transformation, before the compiler interprets anything.
#include — Reusing Text
#include pastes another file into yours. Angle brackets search the standard include paths (<stdio.h>), quotes search the current directory first ("myheader.h"). Every header should be wrapped in an include guard so pasting it twice — which happens easily through nested includes — costs nothing.
#ifndef POINT_H // if the guard macro is not defined yet...
#define POINT_H // ...define it now (so later includes skip the body)
struct Point { int x, y; };
#endif // end of the guarded region
Since C11 there is also #pragma once, a simpler non-standard-but-universally-supported guard. Pick one style and use it everywhere.
#define — Object & Function Macros
#define NAME value creates an object-like macro: every NAME is replaced by its value. #define SQUARE(x) ((x)*(x)) is function-like: the arguments are substituted textually. The parentheses above are not decoration — without them SQUARE(a+b) computes a+b*a+b, and without the outer pair 1/SQUARE(2) computes 1/2*2. Every macro parameter must be wrapped in parentheses.
#include <stdio.h>
#define ARRAY_LEN(a) (sizeof(a) / sizeof((a)[0])) // safe: parenthesized
#define SQUARE(x) ((x) * (x)) // safe: parenthesized
int main(void) {
int arr[] = {1, 2, 3, 4, 5};
printf("len = %zu\n", ARRAY_LEN(arr)); // 5 — parentheses preserved it
printf("sq = %d\n", SQUARE(2 + 3)); // 25, not 11
return 0;
}
Function-like macros still have sharp edges: arguments are evaluated every time they appear, so side effects like SQUARE(i++) are undefined behavior; and macros have no type checking. Modern C prefers static inline functions (typed, safe, still zero overhead) for logic, and macros only for genuine text needs — like the compile-time ARRAY_LEN trick.
Conditional Compilation
#if, #ifdef, #ifndef, and #else/#elif decide at compile time which chunks of text survive. Typical uses: debug builds, platform-specific code, and feature detection. The preprocessor keeps no runtime cost — the loser branches simply vanish.
#include <stdio.h>
// one source, many targets: pick the branch from the -D flag
#ifdef _WIN32
#define CLEAR "cls"
#else
#define CLEAR "clear"
#endif
#if defined(NDEBUG) // release build? strip the trace
#define TRACE(msg) ((void)0) // compiles to nothing
#else
#define TRACE(msg) fprintf(stderr, "%s:%d %s\n", __FILE__, __LINE__, msg)
#endif
int main(void) {
TRACE("starting"); // in debug: prints file and line
printf("clear command: %s\n", CLEAR);
return 0;
}
Compile with -DNDEBUG to activate the release branch, -D_DEBUG or nothing for the debug one. Multi-line macros end each line with a backslash — a trailing space after \ is an infamous silent bug.
Predefined Macros
The compiler defines useful facts about itself and the source. You already met __FILE__ and __LINE__; the full family includes __DATE__, __TIME__, __func__ (the current function's name), and feature macros such as __STDC_VERSION__ that report which C standard is active.
#include <stdio.h>
int main(void) {
printf("file: %s\n", __FILE__); // the .c file name
printf("line: %d\n", __LINE__); // this very line
printf("std: %ld\n", __STDC_VERSION__); // 201112L = C11, 201710L = C17
return 0;
}
Preprocessor Discipline
Half the C bugs in the wild are preprocessor accidents. Follow these rules and you will be on the safe side of them:
- Prefer
enumandconstfor constants — they have types and show up in the debugger. - Prefer
static inlinefunctions over function-like macros — typed, checked, one evaluation. - Reserve macros for: include guards, conditional compilation, and compile-time tricks like
offsetof/ARRAY_LEN. - Always parenthesize every macro parameter and the whole body.
- Name macros in UPPER_SNAKE_CASE so they stand out from functions.
Next: dynamic memory — the heap, and the discipline that keeps it honest.