🎩 The Preprocessor
#include: copy-paste as a language feature
▶ Open the interactive lesson — free, no signupYou've pasted #include <stdio.h> at the top of every program since Hello World — today you find out what that line actually does. Spoiler: it quietly drops thousands of lines of other people's code into your file, which is why one tiny mistake can unleash pages of errors pointing at files you never wrote. It's also step one toward splitting your own programs across many files, the way every real app is built.
You've typed #include <stdio.h> a hundred times by now. Time to learn what it actually does — because the answer is gloriously dumb: it deletes the line and pastes in the entire file. No imports, no modules, no magic. The preprocessor is a text editor that works very, very fast.
Every line starting with # is a preprocessor directive — an instruction to that fast text editor, not to the compiler — handled in a separate pass before the compiler ever reads a single line of your C code. This part of the course covers every last one of them.
Textual inclusion, live
Proof beats belief. Here's a two-file micro-project:
int square(int n); /* a declaration — a promise */#include "square.h"
int main(void) {
return square(6);
}Now ask GCC to stop after preprocessing with -E and show us the text it hands to the compiler:
$ gcc -E main.c
# 1 "main.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "main.c"
# 1 "square.h" 1
int square(int n);
# 2 "main.c" 2
int main(void) {
return square(6);
}The #include "square.h" line is gone — replaced by the header's contents. The # 1 "square.h" lines are linemarkers: notes the preprocessor leaves so error messages can still point at the right file and line. (They'll matter again when we meet #line.)
🧠 Checkpoint: What does #include "square.h" literally do?
- Tells the linker to find square.h
- Replaces the line with the full text of square.h
- Loads square.h at runtime
- Compiles square.h into a separate module
Show answer
Replaces the line with the full text of square.h — Pure textual substitution, performed by the preprocessor before compilation. The compiler never even sees the #include line — only the pasted result.
<angle brackets> vs "quotes"
Both forms include a file; they differ in where the preprocessor looks first:
| form | search order | use for |
|---|---|---|
#include <stdio.h> | system directories only (/usr/include, compiler dirs, paths added with -I) | standard library & installed libraries |
#include "square.h" | the including file's own directory first, then the same places as <> | your project's headers |
The exact search paths are implementation-defined, but every compiler follows this spirit. See yours with gcc -E -v main.c, and add your own directory with gcc -Iinclude/ main.c.
The double-inclusion problem
Copy-paste has a failure mode. Say point.h defines a struct, and both main.c and another header include it:
/* point.h */
struct point { int x, y; };
/* shapes.h */
#include "point.h"
struct circle { struct point center; int r; };
/* main.c */
#include "point.h"
#include "shapes.h" /* pastes point.h AGAIN */
int main(void) { struct point p = {1, 2}; return p.x; }$ gcc main.c
In file included from shapes.h:1,
from main.c:2:
point.h:1:8: error: redefinition of 'struct point'
1 | struct point { int x, y; };
| ^~~~~
point.h:1:8: note: originally defined herestruct point got pasted into main.c twice — once directly, once via shapes.h — and defining the same struct twice is an error. Function declarations can legally repeat, but struct definitions, typedefs (before C11), and initialized variables cannot. In any real project, headers including headers is unavoidable, so every header must defend itself.
Include guards: the standard armor
#ifndef POINT_H /* "if POINT_H is NOT defined..." */
#define POINT_H /* ...define it, so next time it IS */
struct point { int x, y; };
#endif /* POINT_H */Walk it through: the first time point.h is pasted, POINT_H isn't defined, so the #ifndef block is kept and POINT_H gets defined. The second paste sees POINT_H already defined and the preprocessor deletes everything down to #endif:
▶ This spot has an interactive flow widget — open the interactive lesson to play with it.
🧠 Checkpoint: Two different headers both use #ifndef UTILS_H as their guard. What happens when a file includes both?
- A compile error names the clash
- Both work fine — guards are per-file
- The second header silently expands to nothing
- The preprocessor renames one guard
Show answer
The second header silently expands to nothing — The first header defines UTILS_H; the second header sees it already defined and its whole body is skipped. No error, just mysteriously missing declarations — which is why guard names must be unique per header.
The alternative you'll see everywhere: #pragma once as the first line does the same job — shorter, immune to name-collision typos, and supported by every compiler you're likely to meet. But it's not in the C standard, while guards are bulletproof portable. Many codebases use both. Pick one style per project and be consistent.
Guard-name trap: two headers accidentally using the same guard macro (say, both picked UTILS_H) silently make the second one vanish. Use a unique name derived from the path, like MYPROJ_NET_UTILS_H. Names starting with underscores (_POINT_H, __POINT_H) are reserved for the implementation — don't.
What belongs in a header?
Rule of thumb: headers hold promises (declarations), .c files hold fulfillments (definitions). Function prototypes, struct/enum/typedef definitions, macros, and extern variable declarations go in .h. Function bodies and actual variable definitions go in .c — otherwise every file that includes your header gets its own copy, and the linker screams about duplicates. We'll build a full multi-file project in the last lesson of this part.
🧠 Checkpoint: Which of these does NOT belong in a header file?
- A function prototype
- A struct definition
- A non-inline function body
- A typedef
Show answer
A non-inline function body — A function body pasted into five .c files becomes five definitions of the same function — a linker error. Headers declare; .c files define. (Exceptions like inline and static inline come later.)
Try it
▶ This spot has an interactive editor widget — open the interactive lesson to play with it.
Next: the directive that turns the preprocessor from a paste machine into a find-and-replace machine — #define.