How Compilation Actually Works
The four stages between your text file and a running program, and which stage each error message comes from.
Why this matters
In Python you run your file and it works. In C++ there is a whole step in between, and if you do not know what happens there, half the error messages will look like nonsense.
Here is the thing that unlocks it: the error tells you which stage failed. Once you can spot that, you know where to look.
Your.cpp file goes through four stages:
your_code.cpp
↓ 1. Preprocessing (handles # lines)
your_code.i
↓ 2. Compiling (C++ → assembly)
your_code.s
↓ 3. Assembling (assembly → machine code)
your_code.o
↓ 4. Linking (join the pieces)
your_program.exe
Stage 1: The preprocessor
The preprocessor is not clever. It does text substitution and nothing else. It does not understand C++ at all.
It handles every line starting with#:
#include <iostream> // paste that whole file in here
#define PI 3.14159 // replace every PI with 3.14159
#include literally copies the file's contents into yours. That four-line hello world becomes tens of thousands of lines after preprocessing.
Because #define is blind text replacement, it produces a classic trap:
#define SQUARE(x) x * x
int result = SQUARE(2 + 3);
You expect 25. You get 11. The preprocessor pasted the text exactly:
int result = 2 + 3 * 2 + 3; // = 2 + 6 + 3 = 11
const and functions over #define.
Errors here say things like No such file or directory. That means a filename is wrong, not your logic.
Stage 2: The compiler
Now the real work. The compiler reads your tokens, checks that everything makes sense, and translates it to assembly.
This is where syntax errors and type errors appear:
int x = "hello"; // error: cannot convert 'const char*' to 'int'
undefined_function(); // error: 'undefined_function' was not declared
int y = 5 // error: expected ';'
The compiler works on one file at a time and knows nothing about your other files. Remember that — it explains the next stage.
Stage 3: The assembler
Assembly becomes actual machine code — an object file (.o or .obj). You will almost never see an error here. It is a mechanical translation.
Stage 4: The linker
The linker takes all your object files plus any libraries and stitches them into one executable.
This stage produces the errors that confuse people most, because the code compiled fine:
// You declared it — promised the compiler it exists somewhere
int calculate(int x);
int main() {
return calculate(5);
}
// But you never wrote the body
calculate exists. The linker then goes looking for it, cannot find it, and reports:
undefined reference to 'calculate(int)'
That message means: "you promised me this function, but nobody actually wrote it." Usual causes:
- You declared a function but forgot to define it
- You misspelled the definition (
calcuatevscalculate) - You forgot
#includefor a library you used - You wrote the definition in another file but never compiled that file
The other classic linker error is the opposite problem:
multiple definition of 'x'
Two files both defined the same thing. Usually a variable defined in a header that gets included twice.
Declaration vs definition
This distinction is the heart of understanding linker errors.
A declaration says this exists somewhere:
int add(int a, int b); // no body — just a promise
extern int counter; // this variable lives elsewhere
A definition actually creates it:
int add(int a, int b) { // has a body
return a + b;
}
int counter = 0; // reserves real memory
The compiler only needs the declaration to check your code. The linker needs the definition to build the program. Compiles but will not link? You have a declaration with no definition.
Header guards
Since#include is blind copy-paste, including the same header twice pastes it twice — and duplicate definitions are an error.
Every header needs protection:
#ifndef MY_HEADER_H
#define MY_HEADER_H
struct Point {
int x;
int y;
};
#endif
Read it as: "if MY_HEADER_H is not defined, define it and include this content." The second time round it is already defined, so everything is skipped.
Modern compilers accept a one-line version:
#pragma once
Same effect, far less typing. Use it unless you need to support an unusual compiler.
Reading an error message
Take the fear out of it by reading three things in order:
main.cpp:7:15: error: 'x' was not declared in this scope
↑ ↑ ↑ ↑
file line col what went wrong
- Which file and line? Then check the line above too — missing semicolons blame the next line.
- Is it
errororwarning? Errors stop the build. Warnings do not, but they usually point at a real bug. - Fix the first error only, then recompile. One mistake often cascades into twenty messages. The first is real; the rest are noise.
Try it
This code compiles but will not link. Why?
multiply is declared but never defined. The compiler accepts the promise; the linker cannot find the body. Add the definition and it works:
int multiply(int a, int b) {
return a * b;
}
Common mistakes
- Confusing compiler and linker errors. "Not declared" is the compiler. "Undefined reference" is the linker.
- Function-like
#definemacros.SQUARE(2 + 3)gives 11, not 25. Use a real function. - Forgetting header guards, causing duplicate definitions.
- Fixing errors bottom-up. Always fix the first one and recompile.
- Ignoring warnings. Turn them on with
-Wall -Wextraand treat them as errors while learning. - Forgetting to compile every file in a multi-file project.
Summary
Four stages: preprocess, compile, assemble, link. The preprocessor blindly pastes text. The compiler checks syntax and types one file at a time. The linker joins the pieces and needs real definitions, not just declarations. Match the error to its stage and you will know exactly where to look — and always fix the first error first.