Turning .cpp files into an executable takes four stages — knowing them explains most “undefined reference” and “multiple definition” errors.
The build pipeline
main.cpp → Preprocessor → Compiler → Assembler → Linker → ./app
Preprocess
Handles
#include(pastes the header text in),#definemacros and#ifconditionals. Output: one big translation unit.Compile
Parses and type-checks the translation unit, instantiates templates, optimises, and emits assembly.
Assemble
Turns assembly into an object file (
main.o) — machine code with unresolved symbol references.Link
Combines object files and libraries, resolving every symbol to exactly one definition. Output: the executable.
$ clang++ -std=c++20 -Wall -Wextra -O2 main.cpp -o app # all four stages in one go
$ ./app
$ clang++ -E main.cpp -o main.ii # stop after preprocessing
$ clang++ -S main.cpp -o main.s # stop after compiling (assembly)
$ clang++ -c main.cpp -o main.o # stop after assembling (object file)
$ clang++ main.o util.o -o app # link object filesUseful compiler flags
| Flag | Meaning |
|---|---|
-std=c++20 | Pick the language standard (c++17, c++23, …) |
-Wall -Wextra -Wpedantic | Turn on warnings — always use them |
-Werror | Treat warnings as errors |
-O0 / -O2 / -O3 | Optimisation level (debug → release) |
-g | Include debug info (for gdb/lldb) |
-fsanitize=address,undefined | Catch memory errors and UB at run time (ASan/UBSan) |
-I dir / -L dir / -lname | Header search path / library path / link a library |
-D NAME=val | Define a macro (like #define) |
Headers vs source files
A declaration says a name exists; a definition creates it. Headers hold declarations so every .cpp can use them; exactly one .cpp holds each definition.
// math_utils.h — the interface
#pragma once // include guard: paste this header at most once per TU
int square(int x); // declaration only
inline int cube(int x) { // `inline` lets a definition live in a header
return x * x * x;
}#include <iostream>
// In a real project: #include "math_utils.h" and square() lives in math_utils.cpp.
int square(int x); // what the header provides
int main() { std::cout << square(7) << '\n'; }
int square(int x) { return x * x; } // what math_utils.cpp provides49Include guards
#ifndef POINT_H // classic guard — works everywhere
#define POINT_H
struct Point { int x, y; };
#endifBuild systems
Real projects don't type compiler commands — CMake generates the build (Make/Ninja/Xcode) from a CMakeLists.txt:
# CMakeLists.txt
cmake_minimum_required(VERSION 3.20)
project(app CXX)
set(CMAKE_CXX_STANDARD 20)
add_executable(app main.cpp math_utils.cpp)
$ cmake -S . -B build && cmake --build buildWhat's the difference between a compile error and a linker error?
Why do templates usually live entirely in headers?