Header Files & the Preprocessor
Every real C program you've seen starts with a line like #include <stdio.h>. You've been typing it since
Phase 1 without asking what it means. Now that you can write functions (Phase 4) and structs (Phase 7),
it's time to answer that question properly - because the moment your program grows past one file, you
can't avoid understanding it.
The mental model first, before any syntax: C compiles one file at a time, and each file is compiled in
total isolation from every other file. If main.c calls a function defined in math_utils.c, the compiler
building main.c has never seen math_utils.c and never will. Header files and the preprocessor exist
to solve exactly that problem - and nothing more. Once that clicks, the rest of this phase is just
mechanics.
The preprocessor: a text editor that runs before the compiler
Before your compiler reads a single line of C grammar, a separate pass called the preprocessor runs
over your source file and rewrites it as plain text. It doesn't understand types, functions, or scope - it
understands lines starting with #, and it does simple text substitution. Only after the preprocessor is
done does the actual compiler see the result.
You can watch this happen. Take this file:
int
Run just the preprocessor step (most compilers support this):
$ gcc -E main.c
int
PI is gone, replaced everywhere by 3.14159, before the compiler even starts. That's the whole
preprocessor in one example: it's a text substitution pass, not a programming language feature. Keep that
model in your head for everything below.
#define: macros
#define NAME value creates a macro - every later occurrence of NAME gets replaced with value, purely
textually.
int users;
Macros can also take arguments, acting like a function that's expanded inline:
int result = ; // expands to ((5) * (5))
Notice the parentheses around x and around the whole expression. This isn't style - it's a real trap.
Without them:
int result = ; // expands to 2 + 3 * 2 + 3 = 11, not 25!
Because the preprocessor does dumb text substitution, SQUARE(2 + 3) literally becomes 2 + 3 * 2 + 3,
and normal operator precedence takes over. Wrapping every parameter (and the full expression) in
parentheses is the standard defense: ((x) * (x)) expands to ((2 + 3) * (2 + 3)), which is correct.
When to reach for a macro vs. a real function. Prefer a real function almost always - it type-checks
arguments, you can step through it in a debugger, and it doesn't have text-substitution surprises. Macros
still earn their keep for a few things a function can't do: defining constants used in array sizes (like
MAX_USERS above), conditional compilation (next section), and the rare case where you need code to work
across multiple types without templates (C has no generics). If a function would do the job, use a
function.
Conditional compilation
#ifdef, #ifndef, #if, #else, and #endif let the preprocessor include or exclude chunks of code
before compilation even happens - useful for platform-specific code or debug-only logging:
int
If DEBUG isn't defined, the preprocessor deletes the printf("debug: ...") line entirely - it's not
"skipped at runtime," it never reaches the compiler at all. This is exactly the mechanism header guards
use, which brings us to the actual point of this phase.
Why header files exist
Back to the isolation problem. Say you split your code into two files:
/* math_utils.c */
int
/* main.c */
int
When gcc compiles main.c, it hits add(2, 3) with no idea what add is - what it returns, what
arguments it takes, whether it even exists. It's not a linking problem yet; it's that the compiler needs a
declaration of add before it can generate correct code for the call.
A header file is nothing but a place to put those declarations, so any .c file can #include them:
/* math_utils.h */
int ; // declaration only - no body, ends in a semicolon
/* math_utils.c */
int
/* main.c */
int
Remember what #include actually does: it's the preprocessor, so #include "math_utils.h" is replaced,
textually, by the entire contents of math_utils.h, pasted right there. main.c after preprocessing
literally contains the line int add(int a, int b); before main. That's the entire mechanism - no
magic, no special compiler knowledge of "headers." It's copy-paste, done before compilation.
Now main.c compiles fine, because it has add's signature. But main.c alone doesn't have add's
body - that's in math_utils.c. The compiler produces an object file for main.c with a placeholder
that says "something named add goes here," and the linker (Phase 9 covers this) stitches the two
object files together, matching that placeholder to add's real body from math_utils.c's object file.
Build it like this:
$ gcc -c math_utils.c -o math_utils.o
$ gcc -c main.c -o main.o
$ gcc math_utils.o main.o -o program
$ ./program
The rule that falls out of this: a header holds declarations (function signatures, struct
definitions, #define constants) - things a caller needs to know to use your code. The .c file holds
definitions - the actual function bodies. This split is what lets main.c and math_utils.c be compiled
completely separately, in any order, and still work together.
<angle brackets> vs "quotes"
You've used both without thinking about the difference:
<...> tells the preprocessor to search the compiler's standard system directories. "..." tells it to
look in your project's own directory first (then fall back to the system paths). Use quotes for headers
you wrote; angle brackets for the standard library and installed libraries.
Structs and constants belong in headers too
Anything another file needs to know the shape of goes in the header, not just functions:
/* point.h */
typedef struct Point;
Point ;
Every .c file that includes point.h now knows exactly how big a Point is and what fields it has,
which it needs at compile time - the compiler must know a struct's layout to generate code that touches
its fields, even before the linker does its job.
Header guards: the problem of including the same header twice
Here's a real trap: if two headers both #include "point.h", and a .c file includes both of them, the
preprocessor pastes point.h's contents in twice. The compiler then sees typedef struct { ... } Point;
defined twice in the same file and rejects it - a redefinition error.
The fix is the #ifndef / #define / #endif pattern you saw above, called a header guard:
/* ... header contents ... */
Walk through what happens on the second inclusion. First time: POINT_H isn't defined yet, so
#ifndef POINT_H is true, the preprocessor defines POINT_H and includes the body. Second time (in the
same translation unit): POINT_H is now defined, so #ifndef POINT_H is false, and the preprocessor
skips straight to #endif - the body is never pasted in again. The name POINT_H is just a convention
(header name, uppercased, with _ for .//) - pick anything unique in your project, but match that
convention so nobody collides with it by accident.
Most compilers also support #pragma once as a shorter, non-standard alternative that does the same job
in one line at the top of the file. It's supported everywhere that matters in practice, but the classic
#ifndef guard is the one guaranteed by the C standard and the one you'll see in most real codebases, so
it's worth knowing both.
Putting it together
A small multi-file project looks like this:
point.h - declares the Point struct and point_add's signature
point.c - #includes point.h, defines point_add's body
main.c - #includes point.h, calls point_add
/* point.h */
typedef struct Point;
Point ;
/* point.c */
Point
/* main.c */
int
$ gcc -c point.c -o point.o
$ gcc -c main.c -o main.o
$ gcc point.o main.o -o program
$ ./program
(4, 6)
Neither .c file ever saw the other's source code. point.h was the entire contract between them - and
the preprocessor is what made that contract visible to both, by pasting it in before compilation started.
Recap
- The preprocessor runs before the compiler and does text substitution -
#define,#include,#ifdefare all preprocessor directives, not C language features. #include "file.h"literally pastes that file's contents in, right there.- Headers hold declarations (what a caller needs to know);
.cfiles hold definitions (the actual code) - that split is what lets files compile independently and get linked together after. - Wrap macro parameters and expressions in parentheses, or precedence will bite you.
- Header guards (
#ifndef/#define/#endif, or#pragma once) stop the same header from being pasted into a file twice and causing redefinition errors.
Phase 9 covers the tools that turn multiple .c files into one program automatically - Makefiles, and how
to actually debug the thing once it's built.
Quick check
Test yourself on the ideas that make the rest of this guide's multi-file examples make sense:
[
{
"q": "What does `#include \"math_utils.h\"` actually do?",
"choices": [
"The preprocessor pastes the entire text of math_utils.h into that spot, before compilation starts",
"It tells the linker to look for math_utils.c when building the final program",
"It imports the compiled math_utils.o object file into this source file",
"It tells the compiler to search math_utils.c for any function it can't find"
],
"answer": 0,
"explain": "#include is a preprocessor directive, so it's a text substitution: the header's contents are copy-pasted in place before the compiler ever runs, not a special import or linking step."
},
{
"q": "Why does `SQUARE(x)` need to be defined as `((x) * (x))` instead of `x * x`?",
"choices": [
"Because the preprocessor substitutes text literally, so SQUARE(2 + 3) without parentheses expands to 2 + 3 * 2 + 3 and normal operator precedence gives the wrong answer",
"Because C requires all macro arguments to be wrapped in parentheses or it won't compile",
"Because it makes the macro run faster at runtime",
"Because otherwise SQUARE would be treated as a real function call"
],
"answer": 0,
"explain": "The preprocessor does dumb text substitution with no notion of precedence, so unparenthesized macro bodies and arguments can silently combine with surrounding operators in the wrong order."
},
{
"q": "What problem do header guards (`#ifndef`/`#define`/`#endif`) actually solve?",
"choices": [
"They stop the same header's contents from being pasted into one file twice, which would otherwise redefine the same struct or type and fail to compile",
"They prevent two different .c files in the project from both including the same header",
"They make the linker skip duplicate function bodies across object files",
"They speed up compilation by skipping headers that haven't changed"
],
"answer": 0,
"explain": "Without a guard, one file including two headers that both include a third header causes the preprocessor to paste that third header's contents in twice, and the compiler rejects the resulting duplicate struct/type definition."
}
]
← Phase 7: Structs & Typedef · Phase 9: Build Tooling: Makefiles & Debugging →
Check your understanding 3 questions
1. What does `#include "math_utils.h"` actually do?
2. Why does `SQUARE(x)` need to be defined as `((x) * (x))` instead of `x * x`?
3. What problem do header guards (`#ifndef`/`#define`/`#endif`) actually solve?