LESSON 01 · BEGINNER4 min read

The Four Stages of Compilation: Preprocessor to Machine Code

Trace how C++ source code transforms through preprocessing, compilation to assembly, assembly to object files, and final linking into an ELF/PE executable binary.

C++20 / C++23Free to readModule 1 of 11
YOUR NEXT SMALL WINS

By the end, you’ll be able to…

  • Inspect preprocessor transformations (#include, #define, #pragma once, conditional compilation) via g++ -E
  • Analyze compiler translation of ASTs into architecture-specific assembly instructions (x86_64 / ARM64)
  • Distinguish between relocatable object files (.o / .obj) and statically/dynamically linked executable binaries

01The Four-Stage C++ Build Pipeline

Unlike interpreted or bytecode-compiled environments like Python or Java, C++ compiles directly down to native machine architecture instructions. This compilation model grants bare-metal hardware control, but it requires developers to understand the four distinct stages that occur every time the compiler is invoked: Preprocessing, Compilation, Assembly, and Linking.

In the Preprocessing stage, the preprocessor (cpp) scans source files for directives starting with the '#' symbol. Directives like #include literally copy and paste the entire contents of header files into the source file. Macros defined via #define undergo text substitution, and conditional blocks (#ifdef, #ifndef, #endif) are evaluated and stripped away.

The output of the preprocessor is a pure, expanded C++ source stream known as a Translation Unit (TU). Passing the -E flag to GCC or Clang (e.g., g++ -E main.cpp -o main.i) allows engineers to inspect the exact textual output produced by the preprocessor before any parsing occurs.

example.js⚙️C++
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.

02From Translation Unit to Assembly and Object Code

In the second stage (Compilation proper), the compiler front-end parses the translation unit into an Abstract Syntax Tree (AST), performs type checking and semantic analysis, and passes intermediate representations (IR) to the optimizer. The optimizer unrolls loops, inlines functions, and eliminates dead code before emitting target-specific assembly instructions (.s).

In the third stage (Assembly), the assembler translates assembly instructions into relocatable machine code, outputting binary object files (.o on Linux/macOS, .obj on Windows). An object file contains executable machine instructions organized into sections (.text for code, .data for initialized globals, .bss for zero-initialized globals), along with an exported symbol table and relocation directives for unresolved external references.

03Compilation Pipeline Architecture Matrix

Understanding the toolchain commands and artifacts produced at each stage is essential for debugging compilation errors and optimizing build performance:

Notice how the preprocessor operates purely on text, while the compiler and assembler operate on language semantics and binary instructions.

notes.txtPlain text
+--------------+------------------+-----------------------+-------------------------+| Stage        | Underlying Tool  | Compiler Flag         | Output Artifact         |+--------------+------------------+-----------------------+-------------------------+| Preprocessing| cpp              | g++ -E main.cpp       | Expanded source (.i)    || Compilation  | cc1plus          | g++ -S main.cpp       | Assembly source (.s)    || Assembly     | as               | g++ -c main.cpp       | Object binary (.o/.obj) || Linking      | ld / lld / mold  | g++ main.o -o binary  | Executable (ELF/PE/Mach)|+--------------+------------------+-----------------------+-------------------------+

04Production Best Practices: Include Guards and Modules

Because header files are literally copied into translation units, including the same header multiple times in a file or transitively across dependencies causes duplicate type definitions and compilation failures. Historically, developers enclosed header files in #ifndef / #define / #endif preprocessor guards.

Modern C++ development standardizes on #pragma once, a non-standard yet universally supported directive that instructs the preprocessor to open and parse the physical file on disk only once per compilation run. Furthermore, C++20 introduced Modules (import std;), which replace textual copy-pasting with compiled binary module interfaces (BMI), reducing enterprise compilation times by up to 80%.

LET IT CLICK

The ideas worth keeping.

  • C++ compilation follows four phases: Preprocessing, Compilation, Assembly, and Linking.
  • Translation units are self-contained source streams produced by expanding all header includes.
  • #pragma once and C++20 Modules prevent duplicate type definitions and accelerate build times.
TURN UNDERSTANDING INTO SOMETHING YOU CAN DO

Make it yours.

Type and test your code right here in the interactive workspace. Run your solution, check the output, and compare with the worked guide when ready.

01

Exercise 1: Write a traditional include guard pattern for

Write a traditional include guard pattern for a header file named network_socket.hpp, and explain how #pragma once achieves the same outcome.

your_turn_1.cpp⚙️C++
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.
Explore a worked solution
one_possible_solution.cppC++
// Traditional include guard:#ifndef NETWORK_SOCKET_HPP#define NETWORK_SOCKET_HPPclass NetworkSocket {    // Declarations};#endif // NETWORK_SOCKET_HPP// #pragma once is placed at the top of the file to inform the compiler// not to re-read or re-parse this header file within the same translation unit.

Include guards use #ifndef, #define, and #endif wrapping the entire header contents.

02

Exercise 2: Explain why function like macros such as def

Explain why function-like macros such as #define SQUARE(x) x * x can produce silent arithmetic bugs, and how C++ inline constexpr functions fix this.

your_turn_2.cpp⚙️C++
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.
Explore a worked solution
one_possible_solution.cppC++
// SQUARE(2 + 3) expands textually to: 2 + 3 * 2 + 3 = 2 + 6 + 3 = 11 (instead of 25).// Safe C++ alternative:template<typename T>[[nodiscard]] constexpr T square(T x) noexcept {    return x * x;}

Macros perform textual substitution without operator precedence grouping.

A SMALL QUESTION. A USEFUL NEXT STEP.

What stayed with you?

Check the ideas while they’re fresh. Your answers stay in this page and feedback appears as you go.

A LITTLE ACTIVE RECALL0 / 3 answered
01Which stage of the C++ build pipeline resolves #include directives by inserting file contents textually?
02What is an object file (.o / .obj)?
03What major advancement in C++20 replaces textual header inclusion with precompiled binary interfaces?
⚡CODING CHALLENGE
🔒Required to Mark Lesson Complete

Exercise 1: Write a traditional include guard pattern for

Write a traditional include guard pattern for a header file named network_socket.hpp, and explain how #pragma once achieves the same outcome.

EXPECTED OUTPUT
Build completed. Binary executed.
[Exit code: 0]
main.cppC++
Ctrl + ↵
Execution Console
Click "Run & Verify" (or press Ctrl + Enter) to test your code against the challenge.

Keep exploring.

The official references behind this lesson, when you want a second angle or the full details.

One lesson closer.

Reading is a start. Trying it yourself makes the connection.

Solve the hands-on coding challenge in the knowledge section to verify your code and unlock lesson completion.