C fundamentals · lesson 09

The preprocessor

Before the compiler sees your code, a separate program rewrites it. That program is the preprocessor. Understanding it explains a class of bugs that look impossible until you see what the compiler actually received.

in progress
8 min

The preprocessor is a text transformer

The preprocessor doesn't understand C. It doesn't know about types, scopes, or expressions. It performs text substitutions on your source file before the compiler ever sees it. Every line starting with # is a directive to the preprocessor, not to the compiler.

Run gcc -E file.c to see what the preprocessor produces. A short file with a single #include <stdio.h> expands to 800+ lines. That's what the compiler actually compiles.

#include — file inclusion

#include <stdio.h> is literally replaced by the contents of stdio.h. Angle brackets search system include paths. Quotes search the current directory first.

c
#include <stdio.h> // system header — searches system paths #include "myheader.h" // your header — searches current dir first // Both are text substitution. The preprocessor copies // the entire contents of the file at this exact location.

#define — macros

#define creates a text substitution. Every occurrence of the name in the source is replaced with the defined text — before parsing, before type checking.

c
#define PI 3.14159 #define MAX(a, b) ((a) > (b) ? (a) : (b)) double area = PI * r * r; // preprocessor sees: double area = 3.14159 * r * r; int m = MAX(3, 7); // preprocessor sees: int m = ((3) > (7) ? (3) : (7)); // WHY THE EXTRA PARENS? #define SQUARE(x) x*x SQUARE(1+2) // becomes 1+2*1+2 = 5, not 9! // Always wrap macro arguments and the whole expression in parens.
⚠️
Macros have no type safety and no scope. MAX(x++, y++) increments both arguments twice if x > y. Use static inline functions instead of function-like macros whenever possible. static inline int max(int a, int b) { return a > b ? a : b; }

Conditional compilation

#ifdef, #ifndef, #if, #else, #endif include or exclude blocks of code at compile time. This is how platform-specific code and debug builds work.

c
#ifdef DEBUG printf("debug: x = %d\n", x); // only compiled when -DDEBUG passed #endif #ifndef MYHEADER_H // include guard pattern #define MYHEADER_H // ... header contents ... #endif
Every header file needs an include guard. Without it, including the same header twice causes "duplicate definition" errors. The pattern: #ifndef FILENAME_H / #define FILENAME_H / ... / #endif. Or use #pragma once — supported by all modern compilers.
one-line takeaway

The preprocessor rewrites your source with text substitution before the compiler sees a single token.