10. Headers, Namespaces & Multi-File Programs¶
What & why¶
Real programs don't fit in one file. C++ splits code into source files (.cpp) that are compiled separately and header files (.hpp or .h) that share declarations between them. Understanding how the compiler and linker cooperate explains a whole family of confusing errors — and it's where C++ differs most from languages with a module system.
How a build actually works¶
- Preprocessing —
#includeliterally pastes the contents of a file in place.#defineand#ifare handled here. - Compiling — each
.cpp(a translation unit) is compiled on its own into an object file. The compiler only needs declarations of things defined elsewhere. - Linking — the linker glues object files together and resolves every call to its definition.
So:
- "
'foo' was not declared in this scope" → a compiler error: you forgot to#includethe header. - "
undefined reference to 'foo'" → a linker error: it's declared, but you didn't compile/link the.cppthat defines it.
Splitting a program¶
math_utils.hpp — the interface¶
#pragma once // include this file at most once per translation unit
#include <vector>
namespace mathx {
double mean(const std::vector<double>& values);
double median(std::vector<double> values);
constexpr double square(double x) { return x * x; } // small inline definitions are OK
} // namespace mathx
math_utils.cpp — the implementation¶
#include "math_utils.hpp" // quotes: your files; angle brackets: system/library files
#include <algorithm>
#include <numeric>
namespace mathx {
double mean(const std::vector<double>& values) {
if (values.empty()) return 0.0;
return std::accumulate(values.begin(), values.end(), 0.0) / values.size();
}
double median(std::vector<double> values) {
if (values.empty()) return 0.0;
std::ranges::sort(values);
auto n = values.size();
return n % 2 == 1 ? values[n / 2] : (values[n / 2 - 1] + values[n / 2]) / 2.0;
}
} // namespace mathx
main.cpp — the user¶
#include <iostream>
#include "math_utils.hpp"
int main() {
std::vector<double> data{3, 1, 4, 1, 5, 9, 2, 6};
std::cout << "mean: " << mathx::mean(data) << '\n';
std::cout << "median: " << mathx::median(data) << '\n';
}
Build it¶
# all at once
g++ -std=c++20 -Wall -Wextra main.cpp math_utils.cpp -o app
# or separately — only recompile what changed
g++ -std=c++20 -Wall -Wextra -c math_utils.cpp # → math_utils.o
g++ -std=c++20 -Wall -Wextra -c main.cpp # → main.o
g++ main.o math_utils.o -o app # link
Once you have more than a few files, a build system does this for you — CMake in the intermediate track.
What goes where¶
Header (.hpp) |
Source (.cpp) |
|---|---|
| function declarations | function definitions |
| class/struct definitions | member function definitions |
constexpr / inline functions, templates |
everything else |
#includes the header itself needs |
#includes only the .cpp needs |
The One Definition Rule (ODR)¶
Each non-inline function or global variable must be defined exactly once across the whole program. Put a normal function body in a header that two .cpp files include, and you get:
Fix: move the body to a .cpp, or mark it inline (the linker then accepts identical copies).
Include guards¶
If a header is included twice in one translation unit, its contents appear twice → redefinition errors. Every header needs a guard:
or the portable classic:
Namespaces¶
Namespaces prevent name collisions between libraries:
namespace geometry {
double area(double r);
}
namespace finance {
double area(double w, double h); // no clash
}
geometry::area(2.0);
Pulling names in:
using std::cout; // one name — fine in a .cpp
using namespace std::chrono; // everything — OK inside a function or .cpp
namespace fs = std::filesystem; // alias — very common
Never write using namespace ...; at the top level of a header — it leaks into every file that includes it.
Things only one .cpp needs should be hidden from the linker using an anonymous namespace:
A tour of useful standard headers¶
| Header | Gives you |
|---|---|
<iostream> <fstream> <sstream> |
streams: console, files, strings |
<string> <string_view> <format> |
text |
<vector> <array> <map> <unordered_map> <set> |
containers |
<algorithm> <numeric> <ranges> |
algorithms |
<cmath> <numbers> <random> |
math |
<chrono> |
time, durations, clocks, calendars |
<filesystem> |
paths, directories, file metadata |
<optional> <variant> <utility> <tuple> |
vocabulary types |
<memory> |
smart pointers |
<thread> <mutex> <atomic> |
concurrency |
A quick taste of <chrono>:
#include <chrono>
auto start = std::chrono::steady_clock::now();
// ... work ...
auto elapsed = std::chrono::steady_clock::now() - start;
std::cout << std::chrono::duration_cast<std::chrono::milliseconds>(elapsed).count() << " ms\n";
Command-line arguments¶
main can receive the arguments passed to your program:
#include <iostream>
#include <string_view>
#include <vector>
int main(int argc, char* argv[]) {
std::vector<std::string_view> args(argv + 1, argv + argc); // skip program name
for (auto arg : args) {
std::cout << "arg: " << arg << '\n';
}
}
argc is the count (including the program name at argv[0]).
Common mistakes¶
undefined reference— you forgot to compile/link a.cpp, or the signatures in the header and source don't match exactly.multiple definition— a non-inlinefunction or global defined in a header.- Missing include guard.
using namespace std;in a header.- Circular includes —
a.hppincludesb.hppincludesa.hpp. Use forward declarations (class B;) to break the cycle. - Relying on transitive includes — include what you use, even if it "happens to work" through another header.
Exercises¶
- Split the functions from lesson 9's exercises into
stats.hpp/stats.cppand amain.cpp. Build it with separate-csteps. - Deliberately cause each error: delete an
#include(compiler error), leavestats.cppoff the command line (linker error), put a function body in the header and include it from two.cppfiles (ODR error). Read each message. - Put your functions in a namespace and create a namespace alias for it in
main.cpp. - Write a program
argsthat prints each command-line argument with its index, and says whether it looks like a flag (starts_with("--")). - Time how long it takes to sort a vector of 1,000,000 random ints using
<chrono>. Compare-O0and-O2.
Recap¶
- Headers declare, sources define; the preprocessor pastes, the compiler compiles each
.cppalone, the linker connects them. - "not declared" = missing include; "undefined reference" = missing definition at link time.
- Every header needs
#pragma once. - One Definition Rule: define non-inline functions exactly once.
- Namespaces group names; never
using namespacein a header; anonymous namespaces hide helpers. main(int argc, char* argv[])receives command-line arguments.
Next: Files & error handling →