The Life of a Translation Unit
Follow one .c file through preprocessing, compilation and linking โ and learn which errors belong to which stage.
One source file, three stages
Beginner treated "compile" as one step. It is three, and knowing which stage failed is half of debugging C:
- Preprocessing โ textual.
#includepastes a file's text in place, macros are expanded,#ifblocks are pruned. Output: one big translation unit (TU). - Compilation โ the TU is parsed and optimized into an object file
(
.o), machine code with holes where other files' symbols are referenced. - Linking โ the linker merges object files and libraries, filling every hole. If it cannot fill one, you get a linker error, not a compiler error.
/* what #include actually does โ it is copy-paste */
#include "myheader.h" /* pastes myheader.h's full text here */
Why this matters for errors
error: unknown type name 'Foo'โ compile stage (a declaration is missing where the compiler is reading).undefined reference to 'helper'โ link stage (the name was declared but never defined in any object file).multiple definition of 'counter'โ link stage (two TUs both defined it).
The one-definition rule (in C terms)
A program may declare a name many times (that is what headers do) but must define it exactly once across all TUs. A declaration says "this exists somewhere"; a definition creates it.
extern int counter; /* declaration: exists somewhere else */
int counter = 0; /* definition: it lives in THIS object file */
Internal linkage: static at file scope
static int helpers_used = 0; /* private to this TU */
static void tally(void) { helpers_used++; } /* not visible to the linker */
static at file scope means "this name is internal โ other object files
cannot see it, and mine cannot collide with theirs." Two different files may
each have their own static int count; without conflict. Without static,
the name is external and every TU's copy must be one and the same.
Functions are external by default
static int fast_path(int x) { return x + 1; } /* internal: this file only */
int slow_path(int x); /* external: defined elsewhere */
Rule of thumb: give every file-private helper static. You get better
diagnostics (unused-function warnings), no accidental cross-file collisions,
and the optimizer can sometimes inline more aggressively when a name has
internal linkage.
Check your understanding
- Which stage complains about a missing semicolon? (Compile โ preprocessing only reshuffles text; the parser finds the error.)
- Two files both define
int total;at file scope. Which stage fails? (Link โ tentative definitions of the same external name collide.) - What does the preprocessor do with
#if 0 ... #endif? (Deletes the block before the compiler ever sees it.)