Why this matters
Every C file you have written starts with #include, and real C code is full of #define: constants, small helper macros, debug logging, platform switches. The preprocessor is powerful precisely because it is dumb: it rewrites text before the compiler sees it, with no knowledge of types or scope. Used carefully it removes repetition. Used carelessly it produces bugs that are invisible in the source, because the code the compiler actually compiles is not the code you wrote. This lesson shows exactly what the preprocessor does so you can always see through it.
A text processor in front of the compiler
You met the pipeline in the first module: preprocess, compile, assemble, link. The preprocessor (stage 1) handles directives, lines whose first non-blank character is #. It works on tokens (identifiers, numbers, string literals, punctuation), not on C semantics. It does not know what a type or a function is.
You can always see its output with gcc -E. Adding -P drops the line-marker noise:
$ gcc -E -P main.c | tail -20
When a macro confuses you, look at the expansion. That habit resolves most macro mysteries in seconds.
#include: pasting files
#include replaces the directive with the entire contents of the named file, recursively. There are two spellings:
#include <stdio.h>searches only the system include directories.#include "util.h"first searches the directory of the file containing the directive, then the system directories.
So use <> for the standard library and system headers, "" for your own project's headers. You can add directories to the search with -I, as in gcc -Iinclude main.c. To see the search list your compiler uses:
$ echo | gcc -xc -E -v - 2>&1 | sed -n '/search starts/,/End of/p'
#include "..." search starts here:
#include <...> search starts here:
/usr/lib/gcc/x86_64-linux-gnu/14/include
/usr/local/include
/usr/include/x86_64-linux-gnu
/usr/include
End of search list.
#include is not an import in the Python sense. It is textual pasting, which is why the same header pasted twice can cause "redefinition" errors (the fix, include guards, is in the next two lessons) and why a header full of code makes every file that includes it bigger.
Object-like macros: named constants
#define BUFFER_SIZE 4096
#define PROGRAM_NAME "wc-lite"
#define PI 3.14159265358979
char buf[BUFFER_SIZE];
After #define NAME replacement, every later occurrence of the token NAME is replaced by the replacement tokens. Not inside string literals ("BUFFER_SIZE" stays as it is), and not parts of other identifiers (BUFFER_SIZE_MAX is a different token). By convention macro names are in UPPER_CASE, so readers know text substitution is going on. #undef NAME removes a definition.
Defining the same name twice with different text gives a warning:
redef.c:2:9: warning: "X" redefined
2 | #define X 2
| ^
redef.c:1:9: note: this is the location of the previous definition
Alternatives for constants are const variables and enum constants, which have types and obey scope. Macros are still universal for array sizes in older code and for values that must also work in #if (next lesson).
Function-like macros
If the macro name is followed immediately by (, with no space, it takes parameters:
#define SQUARE(x) ((x) * (x))
SQUARE(n + 1) is replaced by ((n + 1) * (n + 1)). The argument's tokens are substituted for each parameter, and that is all. There is no evaluation, no type check, no function call. With a space before the parenthesis, #define SQUARE (x) ((x)*(x)) defines an object-like macro whose text begins with (x), and you get confusing errors such as 'x' undeclared.
Why all the parentheses
The parentheses look excessive until you see what happens without them:
#include <stdio.h>
#define BAD_SQUARE(x) x * x
#define SQUARE(x) ((x) * (x))
#define BAD_MAX(a, b) a > b ? a : b
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int main(void)
{
printf("BAD_SQUARE(1 + 2) = %d\n", BAD_SQUARE(1 + 2));
printf("SQUARE(1 + 2) = %d\n", SQUARE(1 + 2));
printf("100 / BAD_SQUARE(5) = %d\n", 100 / BAD_SQUARE(5));
printf("100 / SQUARE(5) = %d\n", 100 / SQUARE(5));
printf("2 * BAD_MAX(3, 4) = %d\n", 2 * BAD_MAX(3, 4));
printf("2 * MAX(3, 4) = %d\n", 2 * MAX(3, 4));
int i = 5, j = 3;
int m = MAX(i++, j);
printf("m = %d, i = %d\n", m, i);
return 0;
}
BAD_SQUARE(1 + 2) = 5
SQUARE(1 + 2) = 9
100 / BAD_SQUARE(5) = 100
100 / SQUARE(5) = 4
2 * BAD_MAX(3, 4) = 3
2 * MAX(3, 4) = 8
m = 6, i = 7
gcc -E -P shows why:
printf("BAD_SQUARE(1 + 2) = %d\n", 1 + 2 * 1 + 2);
printf("100 / BAD_SQUARE(5) = %d\n", 100 / 5 * 5);
printf("2 * BAD_MAX(3, 4) = %d\n", 2 * 3 > 4 ? 3 : 4);
int m = ((i++) > (j) ? (i++) : (j));
Two rules follow:
- Parenthesize every parameter in the body, so an argument like
1 + 2stays one unit. - Parenthesize the whole body, so the surrounding expression (
100 / ...,2 * ...) cannot reach inside.
Double evaluation
The last line shows a problem parentheses cannot fix: MAX mentions a twice, so i++ runs twice. Any argument with a side effect (i++, a function call that prints or allocates, getchar()) is dangerous in a macro that uses its parameter more than once. A real function evaluates each argument exactly once. Modern C code therefore prefers static inline functions for anything non-trivial:
static inline int max_int(int a, int b) { return a > b ? a : b; }
The compiler inlines it just as efficiently, and you get type checking and normal evaluation.
Macros that are statements: do { } while (0)
A macro that expands to several statements breaks under an if without braces:
#define LOG_BAD(msg) printf("log: "); printf("%s\n", msg)
if (!quiet)
LOG_BAD("starting"); /* only the first printf is inside the if! */
With quiet set to 1, this prints starting: the second printf is not guarded. gcc notices:
dw.c:3:22: warning: macro expands to multiple statements [-Wmultistatement-macros]
dw.c:9:5: note: some parts of macro expansion are not guarded by this 'if' clause
Wrapping the body in braces is not enough, because the caller's semicolon after } ends the if, and a following else becomes an error: 'else' without a previous 'if'. The standard idiom is a loop that runs exactly once:
#define LOG(msg) do { printf("log: "); printf("%s\n", msg); } while (0)
do { ... } while (0) is a single statement that requires a semicolon after it, so LOG(x); behaves exactly like a function call everywhere.
A multi-line macro continues with a backslash as the very last character on each line:
#define LOG(msg) \
do { \
printf("log: "); \
printf("%s\n", msg); \
} while (0)
The # and ## operators
Inside a function-like macro, two operators work on tokens:
#paramturns the argument into a string literal (stringizing).a ## bglues two tokens into one (token pasting).
#include <stdio.h>
#define SHOW_INT(expr) printf("%s = %d\n", #expr, (expr))
#define WHERE() printf("%s:%d in %s\n", __FILE__, __LINE__, __func__)
#define MAKE_GETTER(field) int get_##field(void) { return field; }
int width = 640, height = 480;
MAKE_GETTER(width)
MAKE_GETTER(height)
int main(void)
{
SHOW_INT(2 + 3 * 4);
SHOW_INT(get_width() * get_height());
WHERE();
return 0;
}
2 + 3 * 4 = 14
get_width() * get_height() = 307200
strs.c:15 in main
MAKE_GETTER(width) generated a whole function named get_width. __FILE__ and __LINE__ are predefined macros that expand to the current file name and line number, which is how assert reports where it failed. (__func__ looks similar but is not a macro: it is a variable the compiler provides inside every function.) Other predefined macros include __DATE__, __TIME__ and __STDC_VERSION__.
Common mistakes
- Missing parentheses in the body or around parameters, as shown. Always write
((x) * (x)). - A semicolon at the end of a
#define:#define SIZE 100;turnsint a[SIZE];intoint a[100;];and the error message points at your declaration, not the macro:expected ']' before ';' token. - A space between the macro name and
(in a function-like macro definition. - Side effects in arguments to macros that use a parameter twice.
- Multi-statement macros without
do { } while (0). - Thinking macros respect scope. A
#defineinside a function is active until the end of the file (or#undef), not the end of the function.
Recap
- The preprocessor rewrites tokens before compilation;
gcc -E -Pshows the result. #include <...>searches system directories;#include "..."searches the current file's directory first.-Iadds directories.#define NAME textsubstitutes text; function-like macros substitute arguments into the body.- Parenthesize parameters and the whole body; beware double evaluation; wrap statement macros in
do { ... } while (0); preferstatic inlinefunctions when a function will do. #xstringizes an argument,a ## bpastes tokens;__FILE__and__LINE__give the location.
Going further: read the GNU cpp manual's "Macro Pitfalls" section (info cpp on a machine with the documentation installed, or online). Then run gcc -E -P on a file that includes <stdio.h> and search the output for getc to see how real headers use macros.