The Preprocessor
Textual substitution that runs before the compiler sees your code — powerful, and the source of a specific family of bugs.
The preprocessor performs text substitution before compilation. It knows nothing about C's syntax or types — it moves characters around.
gcc -E main.c | less # see exactly what the compiler receivesThat command is the debugging tool for anything macro-related, and it is worth reaching for early.
#include#
#include <stdio.h> /* system headers — searched in the system paths */
#include "myheader.h" /* your headers — searched relative to the file first */#include literally pastes the file's contents in place. Which is why every header needs a guard against being pasted twice:
#ifndef MYHEADER_H
#define MYHEADER_H
typedef struct { int x, y; } point_t;
void draw(const point_t *p);
#endif /* MYHEADER_H */Without the guard, including it from two places gives you a duplicate definition error. #pragma once does the same in one line and is supported by every compiler you are likely to use, though the #ifndef form remains the strictly portable one.
Object-like macros#
#define MAX_USERS 100
#define PI 3.14159265358979
#define APP_NAME "myapp"For constants, prefer the language's own facilities:
enum { MAX_USERS = 100 }; /* has a type, visible to the debugger */
static const double PI = 3.14159; /* has a type, respects scope */A #define has no type and no scope — it applies from that line to the end of the translation unit, including inside other headers. enum constants and const values are visible to your debugger and obey normal scoping rules.
Function-like macros, and their traps#
#define SQUARE(x) x * x
int a = SQUARE(3); /* 9 — fine */
int b = SQUARE(1 + 2); /* 1 + 2 * 1 + 2 = 5, NOT 9 */
int c = 10 / SQUARE(2); /* 10 / 2 * 2 = 10, NOT 2.5 */Pure text substitution, so precedence breaks. Parenthesise everything:
#define SQUARE(x) ((x) * (x))That fixes precedence and leaves a second problem — double evaluation:
int i = 5;
int d = SQUARE(i++); /* ((i++) * (i++)) — undefined behaviour */Any argument with a side effect is evaluated twice. The same applies to the classic MAX:
#define MAX(a, b) ((a) > (b) ? (a) : (b))
MAX(next_value(), other()); /* calls one of them twice */The fix is usually to not write a macro. A static inline function has types, evaluates each argument once, and is just as fast:
static inline int max_int(int a, int b) { return a > b ? a : b; }Reserve function-like macros for the things a function genuinely cannot do — capturing __FILE__ and __LINE__, or generating code.
Multi-statement macros#
#define SWAP(a, b) do { int _t = (a); (a) = (b); (b) = _t; } while (0)The do { } while (0) wrapper is the idiom that makes a multi-statement macro behave like a single statement, so it works correctly after an if with no braces. Without it:
#define BAD_SWAP(a,b) { int t=(a); (a)=(b); (b)=t; }
if (x) BAD_SWAP(p, q); else other(); /* syntax error — the ; ends the if */Where macros genuinely earn their place#
#define LOG(fmt, ...) \
fprintf(stderr, "[%s:%d] " fmt "\n", __FILE__, __LINE__, __VA_ARGS__)
LOG("user %s not found", id);
/* [main.c:42] user abc not found */A function cannot capture the caller's file and line. That is the canonical legitimate use.
#define ARRAY_LEN(a) (sizeof (a) / sizeof (a)[0])
int nums[10];
for (size_t i = 0; i < ARRAY_LEN(nums); i++) { }Also legitimate — and note it only works on a real array. Pass a pointer and you get garbage, which is failure mode 4. Recent compilers warn via -Wsizeof-pointer-div.
Conditional compilation#
#ifdef DEBUG
fprintf(stderr, "state: %d\n", state);
#endif
#if defined(__linux__)
/* Linux */
#elif defined(__APPLE__)
/* macOS */
#else
# error "unsupported platform"
#endif#ifdef NDEBUG /* defined in release builds; disables assert() */#error is underused: it turns an unsupported configuration into a clear compile-time message rather than a mysterious failure later.
Keep conditional compilation shallow. Deeply nested #ifdef blocks produce code where no single reader knows which lines are live, and where a change compiles on your machine and breaks someone else's.
Useful predefined macros#
__FILE__ /* current file name */
__LINE__ /* current line number */
__func__ /* enclosing function name (C99; not a macro, but used the same way) */
__DATE__ __TIME__
__STDC_VERSION__ /* 201710L for C17 */Exercise#
#include <stdio.h>
/* 1. Write a header guard for a header declaring a `vec2_t` struct.
2. Write a CHECK(cond, msg) macro that, when cond is false, prints
file and line plus msg to stderr and returns -1 from the caller.
Use do/while(0) and parenthesise properly.
3. Replace this broken macro with a static inline function:
#define CUBE(x) x*x*x
4. Run `gcc -E` on your file and read what the compiler actually sees. */
int main(void) { printf("start\n"); return 0; }Common questions#
#define or const for a constant?#
enum for integer constants and static const for others. Both have types, obey scope and appear in the debugger; a #define has none of those properties and leaks into every file that includes the header.
When is a function-like macro the right tool?#
When a function genuinely cannot do the job: capturing __FILE__ and __LINE__, generating declarations, or working across types before you have generics. Everything else should be a static inline function, which is as fast and does not double-evaluate its arguments.
Why does my macro give the wrong answer inside an expression?#
Almost certainly missing parentheses. The preprocessor substitutes text, so #define SQUARE(x) x * x expands SQUARE(1+2) to 1 + 2 * 1 + 2. Wrap both the whole body and every parameter: ((x) * (x)).
Get the C agent pack
A battle-tested AGENTS.md, the review checklist, and the failure-mode cheat sheet for C. One email, then occasional updates when the tooling shifts. No course pitch.
AGENTS.md now — no email needed.