Preprocessor
The preprocessor is a textual substitution layer that runs before the C compiler proper. The standard specifies it as the first four of C’s eight translation phases: physical-source-character mapping, line splicing, tokenisation and comment removal, and directive execution. The output of the preprocessor is a translation unit — a sequence of tokens — that the compiler then parses. Because the preprocessor operates on tokens before semantic analysis, its facilities are fundamentally textual: macros are substitutions, conditional compilation produces or hides text, and the resulting code may be syntactically or semantically distinct from what appears in the source.
The preprocessor is, effectively, a sub-language with its own grammar and its own pitfalls. Most modern C uses the preprocessor sparingly; the language has acquired better mechanisms for what the preprocessor was historically used for — enum for symbolic constants, static const for typed constants, static inline functions for short routines. The genuinely irreplaceable uses are header inclusion, conditional compilation, and a small number of macros that take advantage of textual substitution in ways no language-level mechanism can.
The preprocessor’s role in translation
Translation phase 4, in the standard’s enumeration, processes preprocessing directives and expands macros. Each line beginning with # (after optional whitespace) is a directive. The complete list of directives is:
| Directive | Purpose |
|---|---|
#include | Replace this line with the contents of the named file. |
#define | Define a macro. |
#undef | Remove a macro definition. |
#if, #elif, #else, #endif | Conditional compilation. |
#ifdef, #ifndef | Conditional compilation based on macro definedness. |
#error | Cause translation to fail with a diagnostic. |
#warning | (C23, common extension before) Produce a diagnostic. |
#pragma | Implementation-specific directive. |
#line | Override the line-number tracking. |
The output of the preprocessor — typically inspectable via cc -E file.c — is the translation unit that the compiler proper sees. For non-trivial macro investigations, examining -E output is often the quickest way to understand what the code actually means.
File inclusion
#include directs the preprocessor to replace the line with the content of the named file. Two forms are conventional:
#include <stdio.h> /* angle-bracket: search standard system paths */
#include "config.h" /* quote: search the current directory first, then system */
The angle-bracket form looks up the file in implementation-defined paths (the system include directories: /usr/include, /usr/local/include, and so on). The quote form searches a more permissive list, conventionally starting with the directory of the including source file. The two forms are equivalent if both fail to find the file in the angle-bracket paths and both find it in the same place.
The standard imposes no limit on the depth of inclusion — files may include files that include files. The conventional discipline limits this to keep build dependencies tractable; some projects forbid one header from including another, requiring the source files to include the dependencies they need directly.
Object-like macros
#define name body creates a macro: after the directive, every subsequent occurrence of name as a token is replaced by body:
#define MAX_USERS 1024
#define PI 3.14159265358979323846
#define EOL "\r\n"
The replacement is textual: the body is inserted literally into the token stream. Macros do not respect scope: once defined, a macro is in effect until #undef or the end of the translation unit.
The convention to write macro names in upper case is well-established and useful: it signals at the call site that the identifier is not subject to the usual rules of evaluation.
Function-like macros
A macro may take parameters:
#define SQUARE(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int s = SQUARE(7); /* expands to ((7) * (7)) */
int m = MAX(a + b, c); /* expands to ((a + b) > (c) ? (a + b) : (c)) */
Function-like macros are powerful but treacherous. Two pitfalls dominate.
Multi-evaluation
The macro substitutes its arguments textually, with no notion of single evaluation. An argument with a side effect is evaluated as many times as it appears in the body:
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int x = MAX(read_int(), 0); /* read_int() called twice if it returns >= 0 */
The defence is to evaluate arguments into temporaries at the call site, or to use a function. C99’s static inline functions give the same code as a macro at the call site without the substitution semantics:
static inline int max_int(int a, int b) { return a > b ? a : b; }
Operator-precedence surprises
The substitution is purely token-based, so without parentheses the surrounding context can bind an argument’s tokens differently than intended:
#define DOUBLE(x) x + x
int y = DOUBLE(3) * 2; /* expands to 3 + 3 * 2 == 9, not 12 */
The correct form parenthesises both each argument’s expansion and the entire body:
#define DOUBLE(x) ((x) + (x))
The combination of the two pitfalls produces a discipline: parenthesise everything, never use side-effecting expressions as arguments, and prefer static inline functions when the macro is acting as a function.
Stringification
The # operator, applied to a macro parameter in the replacement list, produces a string literal whose content is the spelling of the argument:
#define PRINT_VAR(x) printf(#x " = %d\n", x)
PRINT_VAR(counter);
/* expands to: printf("counter" " = %d\n", counter); */
/* concatenated: printf("counter = %d\n", counter); */
Adjacent string literals are concatenated by the preprocessor in phase 6, which is what permits this construction to produce a single literal at the call site.
The principal use is logging and assertion macros that emit the textual form of an expression alongside its value.
Token pasting
The ## operator concatenates two tokens into one:
#define MAKE_GETTER(field) int get_ ## field(struct s *s) { return s->field; }
MAKE_GETTER(name)
MAKE_GETTER(age)
/* expands to:
int get_name(struct s *s) { return s->name; }
int get_age(struct s *s) { return s->age; }
*/
The result of ## must be a valid single token; pasting 1 and 2 yields the integer constant 12, but pasting 1 and + yields a diagnostic. Token pasting is the principal mechanism for code generation in C; the X-macro pattern (described below) makes heavy use of it.
Variadic macros
Macros may take a variable number of arguments, indicated by ...:
#define LOG(fmt, ...) fprintf(stderr, fmt "\n", __VA_ARGS__)
LOG("user %s logged in at %s", user, timestamp);
The extra arguments are accessible as the identifier __VA_ARGS__. C23 added __VA_OPT__(content), which expands to content only if the variable arguments are non-empty; this resolves the long-standing inconvenience of trailing commas when no extra arguments are given:
#define LOG(fmt, ...) fprintf(stderr, fmt "\n" __VA_OPT__(,) __VA_ARGS__)
LOG("started"); /* well-formed since C23 */
LOG("user %s logged in", user); /* unchanged */
Before C23, GCC’s , ## __VA_ARGS__ extension served the same purpose; it is widely supported but non-standard.
Conditional compilation
Five directives — #if, #elif, #else, #endif, plus #ifdef/#ifndef shortcuts — produce or hide regions of source text based on compile-time conditions:
#if __STDC_VERSION__ >= 201112L
#include <stdatomic.h>
#endif
#ifdef DEBUG
fprintf(stderr, "in function %s\n", __func__);
#endif
#if defined(_WIN32)
/* Windows-specific code */
#elif defined(__APPLE__) || defined(__linux__)
/* Unix-specific code */
#else
#error "unsupported platform"
#endif
The condition in #if is an integer constant expression; identifiers that are not macros are taken to be 0. The defined(name) operator evaluates to 1 if name is currently defined as a macro and 0 otherwise; it is the same construct as #ifdef but composable with && and ||.
Conditional compilation is the conventional mechanism for portability. It is also the conventional mechanism for opt-in optional features and for build-configuration plumbing. Both uses tend to accumulate; managing the proliferation of #if blocks is one of the practical challenges of maintaining a long-lived C codebase.
#undef
#undef name removes a macro definition. After #undef, the identifier is undefined; a subsequent #ifdef reports it as such, and a subsequent #define produces a fresh definition.
#define X 1
/* X is 1 */
#undef X
/* X is undefined */
#define X 2
/* X is 2 */
Use is rare in production code but conventional in test code that needs to rebuild macros under different conditions.
Predefined macros
The standard requires several macros to be predefined. The most useful are:
| Macro | Value |
|---|---|
__FILE__ | The current source filename, as a string literal. |
__LINE__ | The current line number in the current source file, as an integer constant. |
__DATE__ | The translation date, in the form "Mmm dd yyyy". |
__TIME__ | The translation time, in the form "hh:mm:ss". |
__func__ | The name of the enclosing function, as a static const char[] (since C99; not strictly a preprocessor macro but conventionally listed alongside). |
__STDC__ | 1 if the implementation conforms to the C standard. |
__STDC_VERSION__ | The version of the standard implemented (199901L, 201112L, 201710L, 202311L). |
__STDC_HOSTED__ | 1 for hosted implementations, 0 for freestanding. |
Many implementations define additional macros: GCC and Clang define __GNUC__ and __clang__, MSVC defines _MSC_VER. The conventional defence against compiler-specific macros is to avoid them where possible and to centralise the cases where they are needed.
#pragma and _Pragma
#pragma is the standard’s mechanism for implementation-specific directives. The set of recognised pragmas is implementation-defined; common ones include:
#pragma once /* widely supported, non-standard: include-once */
#pragma pack(push, 1) /* MSVC, GCC: alter structure packing */
#pragma GCC diagnostic ignored "-Wunused-parameter"
C99 introduced _Pragma("…") as a function-like alternative whose argument is a string literal, useful for embedding pragmas in macro expansions.
#error and #warning
#error causes translation to fail with the supplied diagnostic:
#if !defined(__STDC_VERSION__) || __STDC_VERSION__ < 199901L
#error "this code requires C99 or later"
#endif
C23 standardised #warning, long supported as an extension, with the same syntax but only producing a warning.
#line
#line N overrides the line counter. #line N "file" overrides both the line counter and the filename. The directive is principally useful for code generators that produce C source from another input format: the #line directives let the generated file’s diagnostics point back to the original source.
#line 1 "input.y"
/* compiler diagnostics about the next code refer to input.y line 1 */
Header guards
Because #include is textual substitution, including the same header twice yields two copies of the contents. For headers that declare types or extern objects, the second copy produces redefinition diagnostics. The conventional defence is the header guard: a pattern that ensures the body of a header is processed at most once per translation unit.
#ifndef MYHEADER_H
#define MYHEADER_H
/* … declarations … */
#endif /* MYHEADER_H */
The first include defines the guard macro and processes the body; subsequent includes find the guard already defined and skip to the matching #endif. The convention is that the guard macro should be unique within the project, conventionally derived from the path or filename in upper case with non-identifier characters replaced by underscores.
#pragma once is a non-standard alternative supported by every contemporary compiler; it has the same effect with one fewer line of boilerplate. The standard form remains the safer choice in long-lived code that may need to compile under unusual implementations.
When not to use the preprocessor
Several modern alternatives have largely replaced traditional preprocessor uses:
| Old | Replacement |
|---|---|
#define MAX_USERS 1024 | enum { MAX_USERS = 1024 }; (typed, debuggable) |
#define PI 3.14159 | static const double pi = 3.14159; (typed, addressable) |
#define SQUARE(x) ((x)*(x)) | static inline double square(double x) { return x * x; } |
#if SOMETHING for run-time configuration | a static variable read at startup |
#define TYPED_LIST(T) … for generics | _Generic (C11), or hand-specialisation |
The preprocessor remains essential for header inclusion, conditional compilation across platforms, header guards, and a small number of constructs (X-macros for parallel arrays, assertion macros that capture textual expressions) for which no language-level alternative exists.
The X-macro pattern, briefly: define a list as a macro that takes another macro as an argument, then instantiate it multiple ways:
#define COLOURS \
X(RED, "red") \
X(GREEN, "green") \
X(BLUE, "blue")
#define X(name, str) name,
enum colour { COLOURS };
#undef X
#define X(name, str) [name] = str,
static const char *colour_names[] = { COLOURS };
#undef X
The pattern keeps parallel arrays — enumerator and name table — synchronised by construction. It is one of the few preprocessor uses that has no clean language-level replacement.