Unlike languages like Python or JavaScript that are interpreted line by line at runtime by an engine, C++ is a fully compiled language. Before your computer can run C++ code, it must be brutally translated into raw, machine readable binary instructions.
Let's look at the four distinct stages of the compilation pipeline, compiler optimization flags, and how to debug build errors without losing your sanity.
The Four Stages of Compilation
When you run g++ main.cpp, the compiler runs the source code through a pipeline of four distinct tools: the Preprocessor, the Compiler, the Assembler, and the Linker.
1. Preprocessing
The preprocessor handles directives that begin with a # symbol, such as #include and #define. It strips comments, resolves macros, and copy pastes header contents. It is basically a brute force search and replace engine with zero understanding of C++ syntax. Because it literally copy pastes code, including <iostream> expands your tiny 5 line program to tens of thousands of lines before compilation even begins, the exact count varies by standard library implementation.
2. Compilation
The compiler takes the gigantic expanded preprocessor output and translates it into assembly code. Assembly is a low level language specific to your CPU architecture (like x86_64 or ARM). This is where syntax checking, strict type checking, and optimization happen. This is also where the compiler yells at you.
3. Assembly
An assembler translates the assembly code into raw machine code binary instructions (1s and 0s). The output of this stage is an object file (typically ending in .o or .obj). Object files are machine readable but cannot be run yet because they don't contain external library dependencies.
4. Linking
The linker takes all your compiled object files and glues them together with compiled standard library files into a single, executable binary file (e.g. hello.exe on Windows or a.out on Linux/macOS). It resolves function references across different files. When it fails, you get the infamous 'undefined reference' error, which is the Linker's way of saying 'I have no idea what you are talking about, good luck.'
Under the Hood: Assembly Output
To show you what the CPU actually executes, here is the generated x86_64 assembly for a basic main function writing to std::cout. The compiler translates your beautiful high level streams into raw register loading and stream insertion operators:
Compiler Optimization Levels (-O0, -O2, -O3)
When compiling C++, you choose the level of optimization. These are set using compiler flags in your terminal command:
* -O0 (No Optimization): Usually minimizes optimization and is useful for debugging. It does not mean a literal one-to-one source-to-instruction translation, and defaults vary by compiler and build system.
* -O2 (Common release baseline): Enables a broad set of optimizations. It is a sensible starting point for many production builds, but teams should choose and measure their own release configuration.
* -O3 (More aggressive optimization): May enable additional transformations such as more loop optimization or vectorization. It can increase binary size and is not automatically faster for every workload. Correct multithreaded code remains correct; undefined behavior may become more visible under optimization.
Troubleshooting Common Compiler Errors
If you get build errors, inspect these common culprits before throwing your keyboard:
* Compiler/Syntax Error (expected ';' after expression): You forgot a semicolon at the end of a statement. The compiler will stubbornly refuse to proceed.
* Linker Error (undefined reference to 'main'): You either misspelled main (e.g. Main) or forgot to define it altogether. The compiler succeeded, but the linker couldn't find the entry point.
* Preprocessor Error (iostream: No such file or directory): You misspelled <iostream> or forgot the angle brackets.