SECTION 1
Historical Context & Motivation
The separation of compilation and linking into distinct phases is not an accident of modern tooling but rather a deliberate architectural decision whose roots extend back to the earliest days of systems programming. When computers had kilobytes of memory and programs were loaded from punched cards, engineers needed a way to develop large software systems without reprocessing every line of code after a single change. The solution was to split the translation pipeline so that each source file could be processed independently, producing a relocatable intermediate form that a separate tool—the linker—would later stitch together into a runnable program.
This two-phase model enabled separate compilation, a concept that became foundational to languages like Fortran, C, and ultimately C++. Without it, every edit to a single function would require retranslating millions of lines—a cost that would be devastating in both development time and computational resources. The two-phase design thus solved a critical scalability problem in software engineering.
1957 • Fortran Compiler Ships
1972 • C Language & Separate Compilation
.c file compiled independently to an object file, and the linker (ld) resolved cross-file references.1979 • C with Classes → C++
1986 • GNU Toolchain Emerges
2017 • C++20 Modules Proposed
The central question this lesson addresses is: what exactly happens during compilation versus linking, and why does the distinction matter? Understanding this boundary is essential for diagnosing build errors, structuring multi-file projects, and appreciating why C++ build systems behave the way they do.
SECTION 2
Core Principles & Definitions
Before examining each phase in detail, it is useful to define the foundational concepts that underpin the entire build pipeline. Every C++ program begins as human-readable source code and ends as a binary executable; the journey between those endpoints involves multiple precisely defined stages, each of which transforms the code into a progressively lower-level representation.
Translation Unit
.cpp) after all #include directives have been expanded by the preprocessor. The compiler sees exactly one translation unit at a time—it has no knowledge of other source files.Object File
.o or .obj file. Contains machine code, a symbol table, and relocation information, but is not yet a runnable program.Symbol Table
Linker
Executable
KEY TAKEAWAY
SECTION 3
Visual Explanation — The Build Pipeline
The following diagram illustrates the complete build pipeline from source code to executable. Notice how each .cpp file travels through the preprocessor and compiler independently, producing separate object files that converge at the linker stage. This visual makes clear why compilation errors are scoped to a single file, while link errors involve cross-file relationships.
The diagram reveals a key architectural principle: the compiler never sees more than one translation unit at a time. When you compile main.cpp, the compiler has no access to the contents of math.cpp. It relies on declarations (typically via #include directives that pull in header files) to trust that the referenced functions and variables exist somewhere. The linker's job is to honor those promises by matching every undefined symbol with a concrete definition from another object file or library. This separation of concerns is what makes incremental builds possible: if only math.cpp changes, only math.o needs to be recompiled, and the linker re-runs using the updated object file alongside the unchanged ones.
SECTION 4
How It Works — Inside Each Phase
The Compilation Phase
Compilation itself is composed of several sub-stages. First, the preprocessor expands macros (#define), includes header file contents (#include), and evaluates conditional compilation directives (#ifdef). The result is a single, expanded translation unit. Next, the lexical analyzer (lexer) tokenizes the text into a stream of tokens—keywords, identifiers, literals, and operators. The parser then constructs an abstract syntax tree (AST) according to the C++ grammar, and semantic analysis checks type correctness, resolves overloads, and instantiates templates. Finally, the code generator translates the validated AST into machine-code instructions, producing an object file.
Crucially, the object file's code may contain placeholder addresses for functions and variables defined in other translation units. The compiler records these as entries in the relocation table, effectively saying: 'I need the address of symbol sqrt to be patched in later.' This is the fundamental handoff point from compilation to linking.
The Linking Phase
The linker performs three core tasks. First, symbol resolution: it scans every object file's symbol table and matches each undefined reference with a definition from another object file or library. If a symbol is referenced but never defined, the linker emits the familiar undefined reference to ... error. If a symbol is defined in multiple places (violating the One Definition Rule), the linker reports a multiple definition error.
Second, the linker performs relocation: it assigns final virtual addresses to each section of code and data, then patches all the placeholder addresses in the machine code. Third, it organizes the merged output into the correct executable format (ELF sections like .text, .data, .bss, and .rodata on Linux, for instance), writing the entry point address and producing the final executable.
Compiler Errors vs. Linker Errors
SECTION 5
Symbol Resolution & Name Mangling
Because C++ supports function overloading, two functions can share the same name while having different parameter types. The linker, however, needs globally unique identifiers to resolve symbols. The solution is name mangling (also called name decoration): the compiler encodes the function's full signature—name, parameter types, namespace, and class—into a unique string stored in the object file's symbol table. For example, a function void draw(int x, float y) might be mangled to _Z4drawif under the Itanium ABI used by GCC and Clang.
main.o against defined symbols (green, marked with '✓') in math.o and libstdc++.a. Mangled names (amber) ensure overloaded functions resolve correctly.The diagram above illustrates the linker's resolution process. Green check marks indicate defined symbols—addresses the linker can supply. Red question marks indicate undefined references—symbols that must be found elsewhere. The arrows trace the resolution: add(int,int) is resolved from math.o via its mangled name _Z3addii, while cout and print are resolved from the C++ standard library. The extern "C" linkage specification, frequently encountered in mixed C/C++ projects, instructs the compiler to suppress name mangling for designated functions, preserving C-compatible symbol names.
| Feature | C Linkage | C++ Linkage |
|---|---|---|
| Name mangling | None — symbol name matches function name | Full — encodes name, params, namespace |
| Overloading support | Not possible (names collide) | Fully supported |
| Declared with | extern "C" | Default in C++ files |
| Use case | Interfacing with C libraries, OS APIs | Normal C++ code |
C vs. C++ linkage conventions
SECTION 6
Worked Example — Building a Two-File Project
Consider a small project with two source files and one header. We will trace the exact sequence of commands and conceptual transformations from source code to executable, highlighting where compilation ends and linking begins.
Building a Two-File C++ Project
- 1
Step 1 — Write the source files
We have three files:math_utils.h(declaresint add(int, int);),math_utils.cpp(defines the function body), andmain.cpp(includes the header and callsadd). The header provides the declaration so the compiler knowsaddexists without seeing its implementation. - 2
Step 2 — Compile each source file independently
Rung++ -c main.cpp -o main.oandg++ -c math_utils.cpp -o math_utils.o. The-cflag tells the compiler to stop after producing the object file—do not link. Each file is preprocessed (headers expanded), parsed, semantically checked, and translated into machine code.Output:main.o(with undefined symbol_Z3addii) andmath_utils.o(with defined symbol_Z3addii). - 3
Step 3 — Inspect the symbol tables (optional diagnostic)
Runningnm main.oon Linux reveals a line likeU _Z3addiiwhere 'U' means undefined. Runningnm math_utils.oshowsT _Z3addiiwhere 'T' means the symbol is defined in the text (code) section. The toolc++filtcan demangle names:c++filt _Z3addiioutputsadd(int, int). - 4
Step 4 — Link the object files
Rung++ main.o math_utils.o -o program. Although we invokeg++, it recognizes that the inputs are already object files and delegates directly to the linker (ld), which resolves_Z3addiiinmain.ousing the definition inmath_utils.o, patches the addresses, and writes the executable.Output:program— a fully linked executable ready to run. - 5
Step 5 — Run and verify
Execute./program. The call toadd(3, 4)now jumps to the actual machine code address of theaddfunction, which was unknown during compilation ofmain.cppbut was resolved by the linker.Program executes successfully, outputting7.
SECTION 7
Compilation vs. Linking — Side-by-Side Comparison
With both phases now described in detail, we can compare them systematically. The following table highlights the fundamental differences in scope, inputs, outputs, and error characteristics, providing a reference you can consult when diagnosing build failures.
| Dimension | Compilation | Linking |
|---|---|---|
| Scope | One translation unit at a time | All object files and libraries together |
| Input | Preprocessed source code (expanded .cpp) | Object files (.o), static/shared libraries |
| Output | Object file (.o / .obj) | Executable or shared library |
| Key operations | Lexing, parsing, type checking, code generation | Symbol resolution, relocation, section merging |
| Typical errors | Syntax errors, type mismatches, missing declarations | Undefined references, multiple definitions, missing libraries |
| Error location info | Specific file, line, and column | Symbol names and object file names (no line numbers) |
| Invoked by | g++ -c file.cpp | g++ file1.o file2.o -o exe |
| Can be parallelized | Yes — each file compiles independently | Partially — must wait for all object files |
Compilation vs. Linking at a glance
DEBUGGING HEURISTIC
#include directives. If the error cites a mangled symbol name and uses phrases like 'undefined reference,' it is a linker error — check that the object file or library containing that symbol's definition is included in the link command.SECTION 8
Connection to Advanced Theory — Static vs. Dynamic Linking and Beyond
The conceptual model of compile-then-link extends naturally into several advanced topics that you will encounter as you write larger systems. Two of the most important are static linking versus dynamic linking, and the emerging C++ modules system.
| Concept | This Lesson (Basics) | Advanced Version |
|---|---|---|
| Linking model | All symbols resolved at build time (static linking) | Dynamic linking defers resolution to load time or even run time (dlopen) |
| Library type | Static archives (.a / .lib) | Shared objects (.so / .dll) loaded at runtime |
| Header model | Textual #include (copy-paste by preprocessor) | C++20 modules provide pre-compiled interface units, eliminating redundant parsing |
| Optimization | Per-translation-unit optimization by the compiler | Link-Time Optimization (LTO) allows the linker to re-optimize across all translation units |
| Build systems | Manual g++ commands | CMake, Meson, Bazel automate dependency tracking, parallel compilation, and correct link ordering |
Basic vs. advanced build concepts
Link-Time Optimization (LTO) is particularly worth noting because it blurs the boundary between compilation and linking. With LTO enabled (e.g., g++ -flto), the compiler emits an intermediate representation (IR) instead of final machine code into the object file. The linker then performs a second round of optimization across all translation units simultaneously, enabling inlining of functions defined in separate files and whole-program dead-code elimination. This illustrates that while the conceptual distinction between compilation and linking remains, modern toolchains increasingly enable cross-phase cooperation.
SECTION 9
Practice Problems
Question 1: CONCEPTUAL
Show answer
#include). A declaration tells the compiler the function's name, return type, and parameter types without providing the body. The compiler trusts that the definition exists elsewhere and emits an undefined symbol in the object file's symbol table. If the declaration (header) is missing, the compiler will report an error such as 'use of undeclared identifier' because it has no information about the function's signature. It is the linker's responsibility to later match that undefined symbol with the actual definition.Question 2: BASIC CALCULATION
a.cpp through e.cpp. If you modify only c.cpp, how many compilation steps and how many linking steps must be re-run in an optimally incremental build? Why?Show answer
c.cpp to produce a new c.o. The other four object files (a.o, b.o, d.o, e.o) remain valid. However, the linking step must always be re-run because the final executable must incorporate the new c.o. So the answer is 1 compilation + 1 linking. This is the core efficiency of separate compilation: rebuilding scales with the number of changed files, not total files.Question 3: INTERMEDIATE
undefined reference to `_Z8computeRKSt6vectorIdSaIdEE'. (a) Is this a compiler error or a linker error? (b) Using your knowledge of Itanium name mangling, deduce the approximate C++ function signature. (c) Propose two possible causes and how to fix each.Show answer
c++filt, this demangles to approximately compute(std::vector<double> const&). (c) Cause 1: The source file containing the definition of compute was not included in the link command—fix by adding its object file. Cause 2: The function was declared but never defined (the implementation is missing)—fix by writing the function body in the appropriate .cpp file.Question 4: APPLIED
Show answer
Question 5: CRITICAL THINKING
#include model. Modules export a pre-compiled interface rather than textually including header contents. Analyze how modules change the relationship between compilation and linking. Do modules eliminate the linker? Does the conceptual distinction between 'translating code' and 'resolving cross-unit references' still hold? Justify your reasoning.Show answer
SUMMARY
Summary — Compilation vs. Linking
Summary
The C++ build process is divided into two fundamental phases. Compilation operates on one translation unit at a time, transforming source code through preprocessing, lexing, parsing, semantic analysis, and code generation to produce an object file containing machine code and a symbol table with defined and undefined symbols. Compilation errors reference specific lines in specific files.
Linking takes all object files and libraries, performs symbol resolution to match every undefined reference with a definition, executes relocation to assign final addresses, and produces the executable. C++ uses name mangling to encode overloaded function signatures into unique symbols. This two-phase architecture enables separate compilation and incremental builds, and remains the backbone of the C++ build model even as modules and LTO evolve the toolchain.