C / PREPROCESSOR
#include, headers and the textual inclusion model
Explain exactly what the preprocessor does with #include, split declarations into headers and definitions into .c files, and debug includes with cc -E.
What you will learn
- Describe #include as literal text pasted at the line where the directive appears
- Pick "quotes" for project headers and <angle brackets> for toolchain headers
- Keep declarations in .h, definitions in one .c, and avoid duplicate-symbol links
- Inspect the real translation unit with cc -E when includes misbehave
Understanding #include, headers and the textual inclusion model
#include <stdio.h> is not an import statement. The preprocessor locates the file, throws the directive line away, and drops that file's entire contents in its place, then does the same for every #include found inside it, recursively. What reaches the compiler is one long stream of tokens with no trace of which file anything came from; that stream is the translation unit. Everything strange about headers follows from this: the position of the directive on the page matters, the file extension is irrelevant, and a header can never hand you anything but text.
The two spellings differ only in where the file is looked for. #include "grid.h" starts in the directory of the file doing the including and then falls back to the same list that angle brackets use; #include <stdio.h> searches only the compiler's list, meaning the -I directories you passed followed by the system directories baked into the toolchain. The standard calls both searches implementation-defined, so the rule is a convention rather than a language guarantee: quotes for files you wrote, angle brackets for anything shipped with the toolchain or a library.
Headers exist because the compiler sees one .c file at a time and refuses to guess. It needs a prototype before a call so it can convert the arguments and know the return type, while the machine code can stay missing until the linker runs. That is why the split is declarations in the .h and definitions in exactly one .c: a compatible declaration can be repeated in fifty translation units harmlessly, but a function body or an initialized global pasted into two of them yields two definitions and a link error. Including a header also pulls in no code at all, which is why <math.h> declares sqrt yet a platform that keeps the math functions in a separate library still demands that you link it.
<stdio.h> /* the preprocessor pastes this file's text right here */
/* No <stdlib.h> anywhere. A header is only text, so the one declaration
this program needs from it can simply be typed out. */
int abs(int j);
int main(void)
{
printf("abs(-7) = %d\n", abs(-7));
printf("the header supplied a promise; libc supplied the code\n");
return 0;
}
#include is textual substitution performed before compilation, so a header can only contribute declarations to a translation unit, never behaviour.
Worked examples
A header that is not valid C
Pastes an incomplete fragment straight into an array initializer to show that inclusion is raw text substitution (two files, compile with cc main.c).
/* ---------- weekdays.inc ---------- */
"Mon", "Tue", "Wed", "Thu", "Fri",
/* ---------- main.c ---------- */
<stdio.h>
static const char *days[] = {
"weekdays.inc"
};
int main(void)
{
int n = (int)(sizeof days / sizeof days[0]);
int i;
for (i = 0; i < n; i++)
printf("%s\n", days[i]);
printf("%d entries, pasted from a file that is not valid C by itself\n", n);
return 0;
}
Example explained
Line 1weekdays.inc holds five string literals and a trailing comma: no declaration, not a legal translation unit on its own.
Line 2The preprocessor replaces the #include line with that text, so the compiler parses an ordinary array initializer.
Line 3sizeof days / sizeof days[0] is evaluated by the compiler after the paste, which is why it reports 5.
Line 4The .inc extension means nothing to the preprocessor; it will paste any file it can open.
One header, two translation units
Shows the normal declaration/definition split and why a header's text is compiled once per .c file (compile with cc main.c geometry.c).
/* ---------- geometry.h ---------- */
double circle_area(double r);
/* ---------- geometry.c ---------- */
"geometry.h"
double circle_area(double r)
{
return 3.14159265358979323846 * r * r;
}
/* ---------- main.c ---------- */
<stdio.h>
"geometry.h"
int main(void)
{
printf("%.4f\n", circle_area(2.0));
return 0;
}
Example explained
Line 1geometry.h carries a declaration only, so both .c files learn circle_area's signature without seeing its body.
Line 2geometry.c includes its own header, so a definition whose types drift from the declaration is reported by the compiler at once.
Line 3The header's text is pasted twice, once per translation unit, and the two files are compiled separately before the linker joins them.
Line 4Move the body of circle_area into geometry.h and the same build fails with a duplicate definition reported by the linker, not the compiler.
Important notes
File extensions are invisible to the preprocessor; .h, .inc and .def are pasted identically, and the naming convention exists for humans and build tools.
Typing a library function's declaration by hand works, as the main example shows, but it is a demonstration rather than a habit: the real header gets the types right on every platform and stays right when the library changes.
Common mistakes
Putting a definition in a header, such as a function body or int counter = 0;, and including it from two .c files: both compile cleanly, then the linker reports multiple definition of 'counter' and the beginner searches the compiler output for a bug that is not there.
Assuming #include <math.h> makes sqrt available to the program: the header only declares it, so the build still ends with undefined reference to 'sqrt' until the math library is linked.
Using a type or function above the #include line that declares it, or leaning on <stdio.h> to drag in <string.h>: the file compiles on one machine and fails on the next, because inclusion is positional and transitive includes are not promised.
Try it yourself
Change, predict, then run
Take a hello-world program, delete its #include <stdio.h>, and make it compile again by typing the single declaration printf needs. Then move that declaration below main and read the exact error the compiler produces.
Open the C workspaceCheck your understanding
util.h contains the line int counter = 0; and is included by both a.c and b.c. Each file compiles without a warning, yet the build fails. What does that reveal about #include?
- Each translation unit received its own copy of the definition, so the linker is handed two counters and rejects the program.
- The second #include of a header in one build is skipped, so b.c never received counter at all.
- A header may hold declarations only, so the compiler ignored the initializer and left counter uninitialized.
- #include must appear before any other code in a file, so the directive in b.c was processed too late.
Show answer
Compilation succeeds because the pasted text is a perfectly legal definition inside each file on its own; the clash exists only once the two object files are combined, which is why the message comes from the linker. Option 3 is the tempting one, but the preprocessor never inspects what it pastes and nothing forbids a definition in a .h file: the problem is that the definition was multiplied, not that it was illegal.