Files and headers
Real programs are split into many files. Here main.cpp calls a function named square, and square.cpp holds its code. The third file, square.h, is a header: a small file that other files include to learn what square looks like.
Line 2 of square.h is a declaration: square's name, its parameter type and its return type, with no body. It promises the function exists somewhere. The body in square.cpp is the definition: the code that really runs.
Compiling
The preprocessor runs first. #include "square.h" is replaced by the header's text. A source file plus everything it includes is called a translation unit. That, not the file on disk, is what the compiler reads.
The compiler reads one translation unit at a time. It never sees square.cpp, yet the pasted declaration is enough to check the call square(7) and write machine code. Where square lives is left blank, to be filled in later.
square.cpp is a separate translation unit, compiled on its own into square.o. It includes square.h too, so the compiler checks that the definition matches the declaration every other file was promised.
Linking
Every object file carries a symbol table: the names it defines and the names it still needs. main.o defines main and needs square. square.o defines square. Nothing joins them yet.
The linker reads both object files and matches every need with a definition: main.o's need for square is met by square.o. It fills in the blank address, adds the standard library and writes one executable, app.
Run ./app. main calls square(7); the call jumps to the address the linker filled in, square returns 49, and main prints it. Two files, compiled apart, now work as one program.
Rules and speed
Leave square.o out of the link. Each file still compiled fine, but now nobody defines square, so the linker stops with "undefined reference to square(int)". A promise with no body is a link error, not a compile error.
A function may be declared in many files but defined only once in the whole program. That is why headers hold declarations. Put a body in a header that two .cpp files include, and the linker finds two definitions: an error.
Because each file compiles on its own, editing square.cpp means recompiling only square.cpp, then linking again. main.o is reused as it is. In projects with thousands of files, this is what keeps builds fast.
What happens
;; step 1/11 · files
square.h
1 #pragma once
2 int square(int x);
main.cpp
1 #include <iostream>
2 #include "square.h"
3 int main() {
4 std::cout << square(7);
5 }
square.cpp
1 #include "square.h"
2 int square(int x) {
3 return x * x;
4 }
The C++ standard describes nine phases of translation and calls a source file together with everything it includes, after preprocessing, a translation unit. The figure shows the preprocessor and compiler as one step, as g++ -c runs them. nm output is shortened: real lines start with an address, and on macOS C++ symbols carry an extra leading underscore (__Z6squarei). -C (demangle) turns _Z6squarei back into square(int).