C++ • Getting Started and Tooling

Compilation vs. Linking — Explain compilation vs linking at a high level (conceptual)

Understanding how source code becomes an executable through the two pivotal stages of the C++ build process.

Programming braces with a command terminal and binary digits.
Inside this lesson 10 sections 5 questions 5 figures

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

IBM's Fortran I compiler for the 704 introduced the concept of translating high-level code to machine instructions, though compilation and linking were not yet cleanly separated.

1972 • C Language & Separate Compilation

Dennis Ritchie's C language at Bell Labs embraced separate compilation by design. Each .c file compiled independently to an object file, and the linker (ld) resolved cross-file references.

1979 • C with Classes → C++

Bjarne Stroustrup extended C with object-oriented features. C++ inherited C's compilation model, adding name mangling to support function overloading across translation units.

1986 • GNU Toolchain Emerges

GCC (GNU Compiler Collection) provided a free, portable compiler and linker that standardized the compile-then-link workflow across Unix-like platforms, making the two-phase model universal.

2017 • C++20 Modules Proposed

The C++ standards committee proposed modules as a modern alternative to header-based separate compilation, aiming to reduce build times while preserving the compile-and-link architecture.

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.

1

Translation Unit

A single source file (.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.
2

Object File

The output of compiling a single translation unit—typically a .o or .obj file. Contains machine code, a symbol table, and relocation information, but is not yet a runnable program.
3

Symbol Table

A data structure inside an object file that maps identifiers (function names, global variables) to their addresses or marks them as 'undefined'—needing resolution by the linker.
4

Linker

A tool that takes one or more object files (and optionally libraries), resolves all undefined symbols, performs relocation, and produces a final executable or shared library.
5

Executable

The final binary (e.g., ELF on Linux, PE on Windows) with all symbols resolved and addresses fixed. The operating system's loader can map it into memory and begin execution.
✦

KEY TAKEAWAY

Think of the build process like assembling a textbook. Compilation is like having each chapter author independently draft and typeset their chapter. Linking is the editor who collects all chapters, resolves every cross-reference ('see Chapter 5'), assigns final page numbers, and binds the book. If an author references a chapter that was never written, the editor catches the error—just as the linker reports an unresolved symbol.

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.

COMPILATION PHASE (per translation unit)LINKING PHASEC++ Build Pipeline: Compilation → Linkingmain.cppmath.cppio.cppPreprocessorPreprocessorPreprocessorCompilerCompilerCompilermain.omath.oio.oLinker(ld)libstdc++.aa.outSource Files#include, #defineSyntax/SemanticAnalysis + CodegenObject FilesExecutable
The dashed violet region covers the compilation phase, where each source file is processed independently through the preprocessor and compiler. The dashed cyan region shows the linking phase, where all object files and libraries converge into a single executable.

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

A useful diagnostic heuristic: compiler errors refer to syntax mistakes, type mismatches, or missing declarations—they cite a specific line and column in a single source file. Linker errors refer to missing or duplicate symbol definitions—they cite mangled symbol names and object files, not source lines. Knowing which phase produced an error immediately narrows your debugging strategy.

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.

Symbol Resolution During Linkingmain.oDEFINED SYMBOLS:✓ mainUNDEFINED SYMBOLS:? add(int,int)? print(string)? coutMANGLED NAMES:_Z3addii_Z5printNSt..._ZSt4coutmath.oDEFINED SYMBOLS:✓ add(int,int)✓ multiply(int,int)MANGLED NAMES:_Z3addii_Z8multiplyiilibstdc++.aDEFINED SYMBOLS:✓ cout✓ print(string)✓ operator<<✓ ... (thousands)All symbols resolved → Executable produced ✓Linker patches final addresses into machine code
The linker matches undefined symbols (red, marked with '?') in 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.

FeatureC LinkageC++ Linkage
Name manglingNone — symbol name matches function nameFull — encodes name, params, namespace
Overloading supportNot possible (names collide)Fully supported
Declared withextern "C"Default in C++ files
Use caseInterfacing with C libraries, OS APIsNormal 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. 1

    Step 1 — Write the source files

    We have three files: math_utils.h (declares int add(int, int);), math_utils.cpp (defines the function body), and main.cpp (includes the header and calls add). The header provides the declaration so the compiler knows add exists without seeing its implementation.
  2. 2

    Step 2 — Compile each source file independently

    Run g++ -c main.cpp -o main.o and g++ -c math_utils.cpp -o math_utils.o. The -c flag 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) and math_utils.o (with defined symbol _Z3addii).
  3. 3

    Step 3 — Inspect the symbol tables (optional diagnostic)

    Running nm main.o on Linux reveals a line like U _Z3addii where 'U' means undefined. Running nm math_utils.o shows T _Z3addii where 'T' means the symbol is defined in the text (code) section. The tool c++filt can demangle names: c++filt _Z3addii outputs add(int, int).
  4. 4

    Step 4 — Link the object files

    Run g++ main.o math_utils.o -o program. Although we invoke g++, it recognizes that the inputs are already object files and delegates directly to the linker (ld), which resolves _Z3addii in main.o using the definition in math_utils.o, patches the addresses, and writes the executable.
    Output: program — a fully linked executable ready to run.
  5. 5

    Step 5 — Run and verify

    Execute ./program. The call to add(3, 4) now jumps to the actual machine code address of the add function, which was unknown during compilation of main.cpp but was resolved by the linker.
    Program executes successfully, outputting 7.

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.

DimensionCompilationLinking
ScopeOne translation unit at a timeAll object files and libraries together
InputPreprocessed source code (expanded .cpp)Object files (.o), static/shared libraries
OutputObject file (.o / .obj)Executable or shared library
Key operationsLexing, parsing, type checking, code generationSymbol resolution, relocation, section merging
Typical errorsSyntax errors, type mismatches, missing declarationsUndefined references, multiple definitions, missing libraries
Error location infoSpecific file, line, and columnSymbol names and object file names (no line numbers)
Invoked byg++ -c file.cppg++ file1.o file2.o -o exe
Can be parallelizedYes — each file compiles independentlyPartially — must wait for all object files

Compilation vs. Linking at a glance

🔧

DEBUGGING HEURISTIC

If your error message cites a file name and line number, it is a compilation error — check your syntax, types, and #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.

ConceptThis Lesson (Basics)Advanced Version
Linking modelAll symbols resolved at build time (static linking)Dynamic linking defers resolution to load time or even run time (dlopen)
Library typeStatic archives (.a / .lib)Shared objects (.so / .dll) loaded at runtime
Header modelTextual #include (copy-paste by preprocessor)C++20 modules provide pre-compiled interface units, eliminating redundant parsing
OptimizationPer-translation-unit optimization by the compilerLink-Time Optimization (LTO) allows the linker to re-optimize across all translation units
Build systemsManual g++ commandsCMake, 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

Explain why the compiler can successfully compile a file that calls a function defined in a different source file, even though the compiler only sees one translation unit at a time. What mechanism makes this possible, and what could go wrong if that mechanism is missing?
Show answer
The compiler succeeds because of declarations (typically provided via header files included with #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 project has 5 source files: 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
Only 1 compilation step must be re-run: recompiling 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

You compile a project and see the following error: 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
(a) This is a linker error—the phrase 'undefined reference' and the mangled symbol name are definitive indicators. (b) Using 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

You are maintaining a scientific computing project with 200 source files. A full rebuild takes 15 minutes, of which compilation accounts for 14 minutes and linking accounts for 1 minute. Your build system supports 8-way parallel compilation. (a) Estimate the wall-clock time for a full parallel build. (b) If you modify 3 files, estimate the incremental build time. (c) Why is the linking step harder to parallelize than compilation?
Show answer
(a) With 8-way parallelism, the 14-minute compilation phase drops to roughly 14 ÷ 8 ≈ 1.75 minutes (assuming equal-sized files), plus 1 minute for linking, totaling approximately 2.75 minutes. (b) Recompiling 3 of 200 files takes roughly 3 × (14 ÷ 200) ≈ 0.21 minutes per file sequentially, or even less in parallel, plus the 1-minute link step, for a total of about 1.2 minutes. (c) Linking is harder to parallelize because it must merge symbol tables and resolve cross-references across all object files—a fundamentally global operation. The linker cannot assign final addresses to one object file's symbols without knowing the sizes and layouts of all other sections.

Question 5: CRITICAL THINKING

C++20 introduced modules as an alternative to the traditional header/#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
Modules do not eliminate the linker. They primarily optimize the compilation phase by replacing textual inclusion with pre-compiled Binary Module Interfaces (BMIs), which avoids redundant parsing of the same headers across many translation units. Each module still compiles to an object file, and those object files still contain defined and undefined symbols that require linking. The conceptual distinction absolutely still holds: the compiler translates each module's implementation into machine code (compilation), and the linker resolves references between modules (linking). What changes is that the compiler gains richer information at compilation time—module interfaces can encode more semantic information than headers—potentially enabling better diagnostics and optimizations. However, the fundamental two-phase architecture of 'translate independently, resolve globally' remains the backbone of the C++ build model.

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.

Varsity Tutors • C++ • Compilation vs. Linking