Skip to content
  • Compilation & Toolchain · C++

Compilation & Toolchain

LearnViaNotes @learnvianotes· 3 min read

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

  1. Preprocess

    Handles #include (pastes the header text in), #define macros and #if conditionals. Output: one big translation unit.

  2. Compile

    Parses and type-checks the translation unit, instantiates templates, optimises, and emits assembly.

  3. Assemble

    Turns assembly into an object file (main.o) — machine code with unresolved symbol references.

  4. Link

    Combines object files and libraries, resolving every symbol to exactly one definition. Output: the executable.

Terminal
$ 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 files

Useful compiler flags

FlagMeaning
-std=c++20Pick the language standard (c++17, c++23, …)
-Wall -Wextra -WpedanticTurn on warnings — always use them
-WerrorTreat warnings as errors
-O0 / -O2 / -O3Optimisation level (debug → release)
-gInclude debug info (for gdb/lldb)
-fsanitize=address,undefinedCatch memory errors and UB at run time (ASan/UBSan)
-I dir / -L dir / -lnameHeader search path / library path / link a library
-D NAME=valDefine 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.

C++ — header
// 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;
}
C++
#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 provides
Output
49

Include guards

C++ — classic guard
#ifndef POINT_H           // classic guard — works everywhere
#define POINT_H
struct Point { int x, y; };
#endif

Build systems

Real projects don't type compiler commands — CMake generates the build (Make/Ninja/Xcode) from a CMakeLists.txt:

Terminal
# 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 build

What's the difference between a compile error and a linker error?

Why do templates usually live entirely in headers?

Free for students and teachers

Write notes like this one

Cards for every idea, quizzes and flashcards, read aloud — on the web and your phone.

Something wrong with this note? Open it to report