C++ / GETTING STARTED
Translation units and why headers exist
Explain what a translation unit is, why the compiler sees only one at a time, and decide what belongs in a header versus a .cpp.
What you will learn
- Describe a translation unit as one .cpp plus every header pasted in by the preprocessor
- Put declarations in a header and keep each definition in exactly one .cpp
- Call a function whose definition lives in another unit using only its declaration
- Read a duplicate-symbol or undefined-reference error as a translation unit problem
Understanding Translation units and why headers exist
A translation unit is what the compiler actually reads: one .cpp file with every #include already pasted in, so a twenty-line main.cpp that includes <iostream> becomes tens of thousands of lines of text before real compilation starts. The compiler works on exactly one such unit at a time and turns it into one object file. While it compiles main.cpp it has never opened math.cpp and will not go looking for it. That isolation is deliberate: it is what lets a large project rebuild only the files that changed.
The isolation is also why declarations exist. To compile total = add(2, 3); the compiler needs to know that add takes two ints and returns an int, which is enough to check the argument types, place the arguments where the calling convention requires, and emit a call to a name it cannot yet resolve. It never needs the body, so a declaration is a promise that the linker collects on later. Types are the exception: to create a Point variable the compiler must know its size and field offsets, so a class needs its full definition in every unit that uses it.
Headers exist because those declarations must be repeated in every unit that uses them, and repeating them by hand is where projects break. If main.cpp declares the function as returning double while math.cpp defines it returning int, nothing warns you: the files are compiled separately, and the return type is not part of the mangled name that g++ and clang hand to the linker, so the program links and then reinterprets an integer as a double. Write the declaration once in a .h and include it everywhere, and the two units cannot disagree, because there is only one copy of the text. The rule that decides .h versus .cpp follows from this: declarations may be repeated across units, definitions may not.
The compiler processes one translation unit at a time and knows nothing about the others, so each unit needs its own copy of the declarations it uses, and a header is how many units share one authoritative copy.
<iostream>
// A declaration: it promises that area() exists somewhere in this program.
// A header file is little more than a collection of lines like this one.
int area(int width, int height);
int main() {
std::cout << "area(3, 4) = " << area(3, 4) << '\n';
std::cout << "one .cpp plus its includes = one translation unit\n";
}
// The definition. It could sit in another .cpp and nothing above would change.
int area(int width, int height) {
return width * height;
}
The compiler compiles one translation unit at a time in complete isolation, so every unit needs its own declarations, and a header is the single shared source of them.
Worked examples
Writing by hand what a header would have given you
Shows that a standard header supplies nothing but declarations, by calling a C library function with no #include at all.
// No #include anywhere: the line below is what <cstdio> would have provided.
extern "C" int puts(const char* text);
int main() {
puts("compiled with a hand-written declaration");
}
Example explained
Line 1extern "C" stops the name from being mangled, so it matches the puts symbol already present in the C runtime that g++ links by default.
Line 2The compiler needs only the signature, one const char* in and an int out, to type-check the call and emit it.
Line 3<cstdio> would supply this exact declaration plus hundreds of others, so including it is convenience, not magic.
Line 4Mistype the signature and this file still compiles, because this unit has no way to cross-check it against the library's real definition.
What may repeat and what may not
Demonstrates that a declaration can appear many times while the definition appears once, and why a struct cannot be reduced to a promise.
<iostream>
struct Point { int x; int y; }; // a type definition, needed in full by every unit
int manhattan(Point p); // declaration
int manhattan(Point p); // repeating it is legal and emits nothing
int main() {
Point p{3, -4};
std::cout << "manhattan = " << manhattan(p) << '\n';
}
int manhattan(Point p) { // the program's single definition
int ax = (p.x < 0) ? -p.x : p.x;
int ay = (p.y < 0) ? -p.y : p.y;
return ax + ay;
}
Example explained
Line 1The duplicated declaration is accepted because it produces no code, which is also why two different headers may declare the same function.
Line 2struct Point cannot be a promise: the compiler needs its size and field offsets to build p and pass it by value, so type definitions live in headers.
Line 3The body below main is the one definition allowed in the whole program; a second one in any other unit would fail at link time.
Line 4If two units described Point with different fields, both would compile and the program would read the wrong offsets, which one shared header prevents.
Important notes
A header is never a translation unit on its own; it only becomes code inside some .cpp, which is why one broken header can break every file that includes it.
Templates and functions marked inline are the deliberate exception: their definitions belong in the header because each unit that uses them must be able to generate the code itself.
Common mistakes
Putting a function body in a header: each .cpp that includes it gets its own definition, so every file compiles cleanly and the linker then stops with 'multiple definition of ...'.
Retyping a declaration in a second .cpp instead of including the header: a wrong parameter type produces an 'undefined reference' at link time, and a wrong return type links silently and hands back a garbage value at runtime.
Assuming a helper defined in one .cpp is visible in another: the second unit is compiled with no knowledge of the first, so it fails with 'was not declared in this scope'.
Try it yourself
Change, predict, then run
In a single-file editor, declare long long cube(int n); above main, print cube(9) from main, and place the definition after main. Then delete the declaration and read the error, and finally restore it with the parameter changed to long to see the failure move from compiling to linking.
Open the C++ workspaceCheck your understanding
You define int add(int, int) in math.cpp and call it from main.cpp, where the only mention of add is the line int add(int, int); at the top. Why does main.cpp compile even though the compiler never reads math.cpp?
- Because both files are passed to g++ in one command, so the compiler reads math.cpp while compiling main.cpp.
- Because the compiler assumes an undeclared function returns int and generates a stub body for it.
- Because the declaration gives the compiler the types it needs to check and emit the call, leaving the linker to attach that call to the real definition.
- Because the declaration makes the preprocessor copy add's body into main.cpp before compilation.
Show answer
Compiling a call requires only the parameter and return types; the compiler emits a reference to an unresolved symbol and the linker later matches it to the body in math.o. Option 1 is tempting because g++ main.cpp math.cpp looks like a single compilation, but g++ compiles each .cpp as a separate translation unit and only links afterwards, which is exactly why main.cpp needs its own declaration.