Headers and Compilation
The modules snippet in Level 1 showed a C++ program split across utils.h, utils.cpp, and main.cpp, and promised the mechanics later. This is later. The C++ standard defines translation as a pipeline of phases, and the ones that matter for a working mental model are three: the preprocessor edits text, the compiler translates each source file independently into machine code with gaps, and the linker joins the pieces and fills the gaps. Hold those three stages and both classic build errors, undeclared identifier and undefined reference, become self-explanatory.
Stage one: the preprocessor edits text
Every line starting with # is an instruction to the preprocessor, a pure text-rewriting pass that runs before compilation proper. The workhorse, #include, does something almost embarrassingly simple: it pastes the named file's entire contents into that spot. No importing, no analysis, paste. A header file is therefore not a special file format, it is ordinary C++ text meant to be pasted into many files. After preprocessing finishes, the compiler receives one long stream of text, the original source with every include expanded, which the standard calls a translation unit.
// file: utils.h, the promise: meant to be pasted anywhere it is needed
#pragma once // include guard: paste at most once per file
int double_(int n); // DECLARATION: name, parameters, return type. No body.
// file: utils.cpp, the fulfillment: compiled exactly once
#include "utils.h"
int double_(int n) { return n * 2; } // DEFINITION: the actual code
// file: main.cpp, a user of the promise
#include "utils.h" // paste the declaration in
int main() {
std::cout << double_(21) << "\n"; // compiles fine: the promise suffices
}Declarations versus definitions
The split in that snippet is the load-bearing idea of the whole lesson. A declaration announces that a name exists and what shape it has: double_ is a function taking int, returning int. A definition provides the actual substance, the body. The compiler translating main.cpp never sees utils.cpp and does not need to: to compile a call correctly it only needs the shape, which the pasted declaration provides. It emits a call to a function it trusts will exist somewhere, and moves on. Headers hold declarations so every file can share the promises; .cpp files hold definitions so each body is compiled exactly once. That is the entire .h versus .cpp convention, and it is why editing a function's body touches one file, but changing its signature ripples into every file that includes the header.
A restaurant menu is a fair picture. The menu (header) lists every dish with its name and price, and you can plan a whole order from it, the menu is enough to know your order is valid. The kitchen (the .cpp file) is where dishes are actually made, and there is one kitchen no matter how many copies of the menu circulate. An order for a dish the menu does not list is refused on the spot: that is the compiler error. A dish on the menu that the kitchen never actually cooks is discovered later, when the order arrives: that is the linker error, coming up next.
Stage two and three: separate compilation, then linking
Each translation unit is compiled by itself into an object file, machine code with labeled gaps where external names like double_ are called. Files can even compile in parallel, which is how large projects build in minutes instead of days: main.cpp does not wait for, or know about, utils.cpp. Then the linker takes every object file, matches each labeled gap to the one definition that fills it, and writes the finished executable. The two famous errors now separate cleanly. Undeclared identifier is the compiler, at stage two, refusing a name it has no declaration for, usually a missing include. Undefined reference is the linker, at stage three, holding a promise no object file fulfilled, declared but never defined, or the defining file was never handed to the build.
Two refinements complete the picture. First, include guards: if the same header gets pasted twice into one translation unit, its declarations appear twice, and some contents may not tolerate that, so every header defends itself with #pragma once or the classic #ifndef wrapper, per the Core Guidelines rule to guard all header files. Second, the One Definition Rule: across the whole program each function may have exactly one definition, which is precisely why definitions live in .cpp files. Put a function body in a header, paste it into five translation units, and the linker finds five copies of the same name and stops with a duplicate definition error, the mirror image of undefined reference.
What survives into the binary
This pipeline is also your first honest look at what shipping compiled code means, which Level 3 deepens. The binary the linker writes contains machine code, not your source: comments, parameter names, and local variable names are simply gone, they were only ever for the compiler and for you. But not everything vanishes. String literals sit in the binary byte for byte, exported function names survive in the symbol table unless stripped, and the shape of your control flow survives as the actual branch instructions. Open any executable in the string-reading tools of Level 5 and watch it narrate itself. Compilation is a translation, not a protection, the same honest line this Academy draws everywhere: even machine code is just another representation someone can read, and the disassemblers of Level 5 read it for a living.
#include pastes text; a header is a promise sheet of declarations, a .cpp holds the one definition of each body. Each translation unit compiles alone, trusting declarations and leaving labeled gaps, and the linker matches every gap to exactly one definition. Undeclared identifier means the compiler lacked a promise; undefined reference means the linker lacked a fulfillment. And the binary that comes out still carries its strings, its exported names, and its control flow.
Frequently asked questions
How is this different from import in Python or JavaScript?
Those imports happen at runtime against a live module object: the language loads the module, runs it, and binds real values by name. #include is finished before compilation even starts and shares only text. The practical consequence is that C++ resolves everything, names, types, call targets, before the program exists, which is where its speed and its famously detailed build errors both come from. C++20 added a true module system that replaces text pasting, though header-based code remains the vast installed base.
If comments and local names vanish in compilation, is a C++ binary already obfuscated?
No, and the difference matters commercially. Compilation discards some human-oriented information as a side effect, but it leaves strings, exported symbols, library call patterns, and real control flow intact, and decompilers reconstruct readable logic from exactly those. Obfuscation, where this Academy goes next, is the deliberate transformation of what compilation leaves behind. Losing your comments is not a threat model.