Source to running process: the translation model
Lesson 00 promised a language with no runtime in the way — nothing between your code and the CPU. That was a slogan. This lesson makes it literal by following your source text through the machinery that turns it into a running process: preprocess → compile → assemble → link → load. The one new idea is that "compiled" is not a vague adjective but a concrete pipeline with concrete intermediate products — object files, symbols, an executable — and understanding that pipeline explains why C++ has a build step at all, what that build step buys you (the absence of an interpreter), and a whole category of errors that have nothing to do with your logic and everything to do with translation.
add and the CPU's add.New capability: trace a multi-file C++ program through preprocess/compile/assemble/link/load; distinguish a declaration from a definition; explain what an object file and a symbol are, what the linker does, and how to read a linker error — and say precisely what the build step buys over an interpreted language.
#include is just textual paste. (4) Declaration vs definition, symbols, and the linker's one job — plus the one-definition rule. (5) A worked two-file-plus-header example traced through every stage, ending in a real linker error and its diagnosis.1 · What a running program physically is
Before the pipeline, fix the destination in your head. Nearly every machine you will run on follows the von-Neumann model: there is one addressable memory — a giant array of bytes, each with a numeric address — and in that single memory live both the program's machine code (the CPU's native instructions, like "add these two registers", "load the bytes at this address") and its data (your variables, arrays, objects). The CPU does nothing but repeat a tiny loop: read the instruction at the address in its program counter, do what it says, advance. Code and data are the same kind of stuff — bytes at addresses — distinguished only by which ones the CPU is currently treating as instructions.
That picture matters because it tells you what the translation pipeline is for: its entire job is to turn the text you typed into exactly those two things — a blob of machine-code instructions and a description of the data they operate on — laid out so the operating system can drop them into memory and point the CPU at the first instruction. There is no third thing. In Python, a separate program (the interpreter) sits in memory too, reading your code as data and deciding what to do; in a compiled C++ program, your code is the instructions. That is the concrete meaning of "no runtime in the way."
2 · The pipeline: five stages, five products
Run g++ main.cpp -o app and a single command quietly drives a multi-stage assembly line. Each stage consumes the previous stage's output and produces a new, lower-level artifact. Knowing the artifacts is what lets you debug at the right stage.
| Stage | Tool | Consumes | Produces | What it does |
|---|---|---|---|---|
| 1 · Preprocess | preprocessor | .cpp + headers | one big .cpp (a translation unit) | obeys #include, #define, #if — pure text manipulation |
| 2 · Compile | compiler | one translation unit | assembly (.s) | parses, type-checks, optimizes, emits target assembly |
| 3 · Assemble | assembler | assembly | object file (.o) | encodes assembly into binary machine code + a symbol table |
| 4 · Link | linker | all .o files + libraries | executable | resolves cross-file symbols, lays out the final binary |
| 5 · Load | OS loader | the executable | a running process | maps code+data into memory, jumps to the entry point |
Stages 1–3 happen per source file, independently — that independence is central, and we return to it in §3. Stage 4 is the only one that sees the whole program at once. Stage 5 happens every time you launch; everything above it happens once, at build time. You can stop the pipeline at any stage to inspect the product: g++ -E stops after preprocessing, g++ -S after compiling (gives you the assembly), g++ -c after assembling (gives you a .o and goes no further).
3 · The translation unit and the preprocessor
The compiler does not work on "your project". It works on one translation unit at a time: a single .cpp file after the preprocessor has expanded it. The compiler sees nothing else — not your other .cpp files, not your project layout. This is the single most important fact about C++ translation, and most build confusion dissolves once you internalize it.
The first stage, the preprocessor, is almost embarrassingly literal: it is a text-substitution engine that runs before the compiler understands any C++ at all. Its main directive, #include "foo.h", does exactly one thing — it deletes that line and pastes the entire contents of foo.h in its place. Nothing more. A header (.h) is therefore not a module or an import in the Python sense; it is just a file of text you arrange to have pasted into many translation units so they can agree on what exists.
// math_utils.h — a header: it DECLARES what exists, defines almost nothing
int square(int x); // declaration: "a function square(int)->int exists somewhere"
// main.cpp
#include "math_utils.h" // preprocessor pastes the line above right here
int main() {
return square(5); // compiler is happy: it has seen the declaration
}
When main.cpp reaches the compiler, the #include is gone and the declaration of square sits literally above main. The compiler can now type-check the call square(5) — it knows the argument and return types — even though it has no idea where the actual code of square lives. It does not need to. It records "I need a thing named square" and moves on. Wiring that need to its supply is the linker's job, not the compiler's.
#include is blind text paste, a header included by 50 translation units is compiled 50 times — slow builds are a layout consequence, not a mystery. And because the same paste happening twice in one translation unit would re-declare everything, headers carry an include guard (#pragma once, or #ifndef X / #define X / #endif) so the second paste expands to nothing. Forget it and you get a flood of "redefinition" errors — a preprocessor problem masquerading as a code problem.4 · Declaration vs definition, symbols, and the linker
The pipeline forces a distinction high-level languages let you ignore. A declaration announces that a name exists and what its type is — enough for the compiler to check uses of it. A definition additionally provides the actual entity: a function's body, or storage for a variable. int square(int x); is a declaration; int square(int x) { return x * x; } is a definition. The rule of thumb: a declaration is a promise; a definition keeps it.
When the compiler finishes a translation unit it emits an object file (.o): the machine code for the definitions it did see, plus a symbol table. A symbol is just a name the linker can match on — the encoded name of a function or global variable. Each symbol in the table is either defined here (this .o provides it) or undefined / external (this .o uses it but expects someone else to provide it). main.cpp's object file defines main and references an undefined square.
.o files (and libraries) and plays matchmaker: for every undefined symbol in any object file, it finds the one object file that defines that symbol and patches the reference to point at the right address. Then it lays everything out into the final executable. That is essentially all it does — match undefined symbols to definitions, and arrange the result. It does not type-check (the compiler already did) and it does not understand C++; it understands names and addresses.Two failure modes follow directly, and they are the two most common build errors in C++:
square and called it, but never compiled+linked a file containing its definition. The compiler was fine (it saw the declaration); the linker fails, because it has a promise nobody kept.The second error is the practical face of the one-definition rule (the ODR). In plain terms: across the whole program, every function and every variable may be declared as many times as you like, but must be defined exactly once. (Certain entities — inline functions, templates (Lesson 12), things defined inside a class — are allowed to appear in multiple translation units precisely because they are designed to be identical everywhere, so the linker is told to keep just one.) The ODR is why headers contain declarations and the .cpp files contain definitions: it keeps each definition in exactly one translation unit, so exactly one object file owns each symbol.
5 · Worked example: a two-file program, traced end to end
Here is the smallest program that exercises the whole pipeline: a header that declares, one .cpp that defines, and one .cpp that uses.
// ── math_utils.h ──────────────────────────────
#pragma once // include guard: paste this header at most once per TU
int square(int x); // DECLARATION only — a promise
// ── math_utils.cpp ────────────────────────────
#include "math_utils.h" // so the definition's signature matches the promise
int square(int x) { // DEFINITION — keeps the promise
return x * x; // compiles to a couple of machine instructions, no runtime
}
// ── main.cpp ──────────────────────────────────
#include "math_utils.h" // pastes the declaration; main.cpp never sees square's body
int main() {
int n = square(5); // a call to an as-yet-undefined symbol
return n; // process exit code = 25
}
Trace it. Build with g++ main.cpp math_utils.cpp -o app and watch the artifacts appear:
Notice that main.o was produced without the compiler ever seeing square's body — only its declaration. The two translation units were compiled in complete isolation; they met for the first time at the linker. That isolation is the whole reason a header exists: it is the contract both sides compile against so the linker's later matchmaking succeeds.
square:
$ g++ main.cpp -o app
/usr/bin/ld: main.o: in function `main':
main.cpp:(.text+0x10): undefined reference to `square(int)'
collect2: error: ld returned 1 exit status
Read it like the pipeline. ld is the linker. It is processing main.o, in function main, and finds an undefined reference to the symbol square(int). The compiler never complained — it saw the declaration from the header and trusted the promise. The promise was never kept because we never compiled math_utils.cpp, so no object file defines square, so the linker has an undefined symbol with no supplier. The fix is not in your logic; it is to feed the definition into the link: g++ main.cpp math_utils.cpp -o app. "Undefined reference" almost always means a missing definition in the link, not a missing declaration.6 · Common mistakes / failure modes
inline definition in a header gets pasted — and thus defined — in every translation unit that includes it. Each .o defines the symbol; the linker reports multiple definition. Keep definitions in .cpp; keep declarations in .h.#pragma once, including a header twice in one TU re-pastes its declarations and triggers redefinition errors — a preprocessor-stage problem, fixable only at the preprocessor stage.int square(int) but the .cpp defines long square(long), these are two different symbols. The call links against the declared one, which nothing defines → undefined reference, even though the file is right there.Checkpoint exercise
g++ -E main.cpp and confirm the #include line has been replaced by the bare declaration int square(int); — you have just watched the preprocessor do its only trick. Next run g++ -c main.cpp to get main.o, then nm main.o (the symbol-table viewer) and find square marked U (undefined) and main as a defined text symbol. Now run g++ main.cpp -o app and reproduce the "undefined reference to square(int)" error. Finally, fix it by adding math_utils.cpp to the command, run ./app; echo $?, and confirm the exit code is 25. You will have driven, stopped, and inspected the pipeline at every stage by hand.Where this points next
We now know what a program is by the time it runs — machine code and data in one addressable memory, with no interpreter between it and the CPU — and how the text we typed became that. But we have been vague about the data half: "your variables live in memory." Where in memory, with what type, of what size, for how long? Lesson 02 makes the data side as concrete as the pipeline made the code side: it defines a C++ object precisely as a typed region of storage, measures the fundamental types with sizeof, and draws the stack frame a function call pushes — the first and cheapest of the three places your data can live. That is where "where does the memory live?" stops being a slogan.
.cpp after the preprocessor blindly pastes its #included headers), so a declaration (a promise a name exists, type-checkable on its own) is fundamentally different from a definition (the actual body or storage). Each translation unit becomes an object file with a symbol table; the linker's one job is to match every undefined symbol to its single definition — which is why the one-definition rule demands exactly one definition program-wide, why headers hold declarations and .cpp files hold definitions, and why "undefined reference" is a missing-definition link error, not a compile error.Interview prompts
- Name the five stages of building a C++ program and the artifact each produces. (§2 — preprocess→translation unit, compile→assembly, assemble→object file (.o), link→executable, load→process.)
- What is a translation unit, and why does the compiler only ever see one at a time? (§3 — a single .cpp after preprocessing; compilation is per-file and independent, so cross-file wiring is deferred to the linker — which is what makes separate compilation and headers necessary.)
- What does
#includeactually do? (§3 — it is pure textual substitution by the preprocessor: delete the directive, paste the named file's contents in its place, before the compiler parses any C++.) - Distinguish a declaration from a definition and give an example of each. (§4 — a declaration announces a name and its type (
int square(int);) so uses type-check; a definition provides the body or storage (int square(int){return x*x;}); a name may be declared many times, defined once.) - State the one-definition rule in plain terms and say what it explains about project layout. (§4 — exactly one definition per function/variable across the whole program; it's why headers carry declarations and .cpp files carry definitions, so each symbol has one owning object file.)
- You get "undefined reference to
foo"; the file definingfooexists. What's happening and how is it different from a compile error? (§4–5 — the compiler saw a declaration and was satisfied; the linker has an undefined symbol with no defining object file in the link, usually because that .cpp wasn't compiled/linked or the signatures differ; it is a link-stage failure, not a type error.) - What does the build step buy you over an interpreted language, in cost-model terms? (§1 — translation to native machine code happens once, ahead of time, so at runtime there is no interpreter loop or bytecode dispatch consuming cycles; your instructions are the CPU's instructions — "no runtime in the way".)