Lesson 8 of 8
C++ reads a file top to bottom, once. A name must be declared before it's used:
// WILL NOT COMPILE
#include <iostream>
int main() {
std::cout << helper() << "\n"; // helper not declared yet
return 0;
}
int helper() {
return 42;
}
error: 'helper' was not declared in this scope
The definition is right there — but below. When the compiler reached the call it hadn't seen it.
Two fixes
Reorder — define before use:
#include <iostream>
int helper() {
return 42;
}
int main() {
std::cout << helper() << "\n";
return 0;
}
42
Forward declare — promise now, define later:
#include <iostream>
int helper(); // declaration: this exists, here's its shape
int main() {
std::cout << helper() << "\n";
return 0;
}
int helper() { // definition, further down
return 42;
}
42
This is the declaration/definition split from lesson 5.1 doing real work. The compiler has what it needs to check the call; the linker finds the body later.
Reordering is simpler when it works. Forward declarations become necessary when two functions call each other:
#include <iostream>
bool is_odd(int n); // forward declaration breaks the cycle
bool is_even(int n) {
if (n == 0) return true;
return is_odd(n - 1);
}
bool is_odd(int n) {
if (n == 0) return false;
return is_even(n - 1);
}
int main() {
std::cout << std::boolalpha;
std::cout << is_even(10) << " " << is_odd(7) << "\n";
return 0;
}
true true
Whichever you put first needs the other declared. There's no ordering that avoids it — mutual recursion requires a forward declaration.
Splitting across files
One file stops scaling. The convention is to split each component in two:
- A header (
.hor.hpp) with declarations — the interface. - A source file (
.cpp) with definitions — the implementation.
mathutils.h
#pragma once
int add(int a, int b);
int multiply(int a, int b);
mathutils.cpp
#include "mathutils.h"
int add(int a, int b) {
return a + b;
}
int multiply(int a, int b) {
return a * b;
}
main.cpp
#include <iostream>
#include "mathutils.h"
int main() {
std::cout << add(2, 3) << "\n";
std::cout << multiply(4, 5) << "\n";
return 0;
}
Build it by compiling both sources together:
g++ -std=c++20 -Wall -Wextra main.cpp mathutils.cpp -o program
Forget mathutils.cpp and you get undefined reference to 'add(int, int)' — the linker error
from lesson 2.6, cause number three.
#pragma once
That first line matters. Without it, a header included twice — directly and through another header — would declare everything twice.
#pragma once tells the preprocessor to include this file only once per translation unit. It
isn't in the standard, but every mainstream compiler supports it.
The portable alternative is an include guard:
#ifndef MATHUTILS_H
#define MATHUTILS_H
int add(int a, int b);
#endif
Same effect, more typing, and a risk of two headers accidentally sharing a macro name. Use
#pragma once unless a project tells you otherwise.
"quotes" versus <angles>
#include "mathutils.h"— your files. Searches the current directory first.#include <iostream>— system and library headers. Searches the compiler's include paths.
Use quotes for your own headers and angles for everything else.
What goes where
| Header (.h) | Source (.cpp) |
| --- | --- |
| function declarations | function definitions |
| class definitions | member function bodies |
| constexpr / inline constants | everything else |
| templates (all of it — Module 12) | |
The rule of thumb: a header holds what callers need to know; the source holds how it works.
Two things to avoid in headers:
- Definitions of ordinary functions. Include that header in two
.cppfiles and the linker sees two definitions — a "multiple definition" error. (inlineand templates are exempt; lesson 17.4 explains why.) using namespace std;. It forces itself on every file that includes the header. Bad in a.cpp, genuinely antisocial in a.h.
Why bother
- Build times. Change one
.cppand only that file recompiles. Change a header and everything including it does — which is why lesson 17.5 cares about header size. - Navigation. The header is a table of contents.
- Reuse. Other projects include the header and link the compiled object.
- Encapsulation. Users depend on the interface, not the implementation, so you can rewrite the body freely.
Managing the build command by hand gets old fast once you have a dozen files. That's what CMake is for — Module 17.
Try it
On your own machine, create three files — stringutils.h, stringutils.cpp, and main.cpp —
implementing std::string repeat(const std::string& text, int times). Compile and run them.
Then deliberately omit stringutils.cpp from the compile command and read the error.
Module 5 complete
You can now factor code into functions: declaring and defining them, passing by value or
reference, defaulting const& for anything large, giving parameters defaults, overloading a name
across types, using recursion where it fits and loops where it doesn't, and splitting an
interface from its implementation across files.
Next up: Module 6 — Arrays, Strings, and Your First STL Tools, where you finally get real containers — and Milestone Project 1.
Questions about this lesson
Log in to ask a question.