C++ Compilation: Preprocessing, Compilation, and Linking
C++ compilation is a multi-stage pipeline. Understanding each stage helps you debug errors, optimize build times, and write headers and source files that are correct across translation units.
The Four Stages
Source (.cpp) → Preprocessed (.ii) → Assembly (.s) → Object (.o) → Executable1. Preprocessing
The preprocessor handles directives starting with #. It runs before any actual compilation:
#include— recursively inserts the referenced file into the translation unit#define/#undef— macro substitution#if/#ifdef/#ifndef/#endif— conditional compilation#pragma— compiler-specific instructions (e.g.,#pragma once,#pragma GCC optimize)#line— resets the line number for error reporting
// Before preprocessing
#include <vector>
#define MAX_SIZE 100
#if DEBUG
std::cout << "Debug mode";
#endif
// After preprocessing — <vector> is expanded in place
// MAX_SIZE is replaced with 100
// The #if block is either included or stripped
The preprocessed output can be inspected with g++ -E source.cpp -o source.ii.
2. Compilation
The compiler translates the preprocessed source into assembly or machine code. This stage includes:
- Lexical analysis — tokenization of the source
- Syntax analysis — building an AST and checking grammar rules
- Semantic analysis — type checking, name resolution, template instantiation
- Optimization — inlining, constant propagation, loop unrolling
- Code generation — emitting assembly or machine instructions
This is where most C++ errors surface:
- Syntax errors (missing semicolons, mismatched brackets)
- Type errors (mismatched types, implicit conversions)
- Template errors (failed deduction, substitution failure)
- ODR violations within a single translation unit
Run g++ -c source.cpp -o source.o to produce an object file. Use -S (g++ -S source.cpp) to see assembly output.
3. Assembly
The assembler converts assembly instructions from the compiler into machine code — relocatable object files (.o or .obj). This stage is straightforward: one-to-one mapping of assembly mnemonics to binary opcodes, with relocation entries for symbols whose addresses are not yet known.
4. Linking
The linker combines object files and libraries into a single executable or shared library. It resolves symbol references across translation units:
- Symbol resolution — matching undefined references to their definitions
- Relocation — adjusting addresses in object code to their final positions
- Static linking — embedding library code directly into the binary
- Dynamic linking — recording shared library dependencies for runtime resolution
Linker errors are distinct from compiler errors:
- Undefined reference — a symbol was declared but not defined (e.g., missing function body)
- Multiple definition — the same symbol appears in more than one translation unit (ODR violation)
- Unresolved external symbol — MSVC’s name for undefined reference, often caused by missing library linkage
g++ main.o utils.o -o programTranslation Units
A translation unit is the fundamental unit of compilation: a source file after preprocessing, with all #include directives expanded and all conditional compilation resolved. Each translation unit is compiled independently.
// main.cpp — first translation unit
#include "utils.h"
int main() { return add(1, 2); }
// utils.cpp — second translation unit
#include "utils.h"
int add(int a, int b) { return a + b; }
// utils.h — header, included into both translation units
int add(int a, int b);The header utils.h is compiled twice — once as part of main.cpp and once as part of utils.cpp. This is why the One Definition Rule matters: if utils.h contained a class definition, both translation units would include it, and the linker would need to handle multiple identical definitions (which it does for classes, templates, and inline functions).
Headers vs Source Files
| | Header (.h/.hpp) | Source (.cpp/.cc) | |
The Four Stages of Compilation
C++ compilation follows four distinct stages, each producing output consumed by the next stage:
Preprocessing — The preprocessor handles directives beginning with
#:#include(file inclusion),#define(macro substitution),#ifdef/#ifndef(conditional compilation), and comment removal. The output is a translation unit — a single stream of tokens with all includes expanded and all macros substituted. You can see the preprocessed output withg++ -E source.cpp.Compilation — The compiler translates the preprocessed source into assembly language specific to the target architecture. This stage performs lexical analysis, syntax analysis, semantic analysis, optimization, and code generation. Warnings and errors are produced here. Modern compilers like GCC and Clang perform几十个 optimization passes at this stage.
Assembly — The assembler converts assembly code into machine code, producing relocatable object files (
.oon Linux,.objon Windows). These files contain machine instructions, but addresses are not yet resolved — symbols referenced from other translation units are marked as unresolved.Linking — The linker resolves symbol references across object files and static libraries, relocates addresses, and produces the final executable or shared library. The linker also handles startup code (
_startormainCRTStartup), runtime library inclusion, and section merging.
One Definition Rule (ODR)
The ODR states that every non-inline function and variable must have exactly one definition across the entire program. Violations cause linker errors (duplicate symbols) or undefined behavior (if multiple definitions differ). Use header guards, inline keyword for functions defined in headers, and static or anonymous namespaces for file-local symbols.
Separate Compilation Benefits
C++’s separate compilation model means changing one source file only requires recompiling that file and relinking, not recompiling the entire project. For large codebases, this dramatically reduces rebuild times. Tools like CCache and distributed build systems (distcc, FastBuild) further accelerate compilation by caching or parallelizing compilation across machines.
FAQ
What is the difference between malloc and new in C++? malloc allocates raw memory without calling constructors; new allocates memory and calls the constructor. In C++, prefer new for objects. free vs delete follows the same pattern — delete calls the destructor.
How do I prevent memory leaks in C/C++? Use RAII (Resource Acquisition Is Initialization) in C++ — smart pointers like std::unique_ptr and std::shared_ptr automatically free memory. In C, always pair every malloc with a free and use tools like Valgrind or AddressSanitizer to detect leaks.
What is undefined behavior in C/C++? Undefined behavior occurs when code performs operations that the language standard does not define — dereferencing a null pointer, buffer overflow, signed integer overflow. The compiler may generate any code, including unexpected results or crashes.
Should I learn C or C++ first? Learn C first if you want to understand low-level memory and system programming. Learn C++ first if you want object-oriented features and the STL. Both are valuable; C++ builds on C concepts.
What is the difference between a header file and a source file? Header files (.h) declare interfaces — function prototypes, class definitions, macros. Source files (.c or .cpp) implement the declarations. Headers are #included; source files are compiled separately and linked.
—|—|—|
| Contains | Declarations, inline functions, templates | Definitions, implementations |
| Included by | Other files via #include | The compiler directly |
| Compiled | Multiple times (once per includer) | Once |
| Guards | Header guards needed | Not needed |
Header Guards
Every header must be protected against multiple inclusion within a single translation unit:
#ifndef MYHEADER_H
#define MYHEADER_H
// declarations
#endifOr the simpler #pragma once (supported by all major compilers):
#pragma once
// declarations
The inline Keyword
inline allows a function or variable to be defined in a header without violating the ODR. It tells the linker: “multiple identical definitions are acceptable, pick any one.” This applies to:
- Inline functions (
inline int add(int a, int b) { return a + b; }) - Inline variables (C++17:
inline constexpr int MAX = 100;) - Functions defined inside a class body (implicitly inline)
Object Files
Object files contain compiled machine code structured into sections:
.text— executable code.data— initialized global/static variables.bss— uninitialized global/static variables (zero-initialized at load time).rodata— read-only data (string literals, const variables).symtab— symbol table (exported and undefined symbols).rela.text— relocation entries for code section
nm myobject.o # List symbols
objdump -d myobject.o # Disassemble
readelf -a myobject.o # Full ELF structureBuild Optimization
Precompiled Headers
Compile a header once and reuse the preprocessed result across translation units:
g++ -x c++-header stdafx.h -o stdafx.h.gch
g++ -include stdafx.h main.cpp # Uses the .gch fileUnity Builds
Combine multiple source files into a single translation unit to reduce compilation overhead:
// unity.cpp
#include "src1.cpp"
#include "src2.cpp"
#include "src3.cpp"Unity builds eliminate redundant header parsing and template instantiation but break incremental builds — a change anywhere requires recompiling the entire unit.
Forward Declarations
Use forward declarations instead of #include in headers when only a pointer or reference is needed:
class Widget; // forward declaration — no #include needed for pointer usage
class Container {
Widget* ptr; // OK — pointer only needs declaration
Widget obj; // Error — needs full definition
---;Build Systems
| System | Description | Strength |
|---|---|---|
| CMake | Meta-build system generating Makefiles/Ninja | Cross-platform standard |
| Make | Direct rule-based builds | Simple, well-known |
| Ninja | Low-level build system focused on speed | Fast incremental builds |
| Bazel | Distributed, hermetic builds | Large monorepos |
| Meson | Python-based, fast configuration | Developer-friendly |
CMake is the de facto standard for C++ projects. It handles compiler detection, dependency management, and generator selection for Ninja or Makefiles.
For a comprehensive overview, read our article on C File Io Guide.
For a comprehensive overview, read our article on C Memory Management.