Multi-File Programs and Headers
Real programs are never one file. How to split code across files without the compiler getting confused, explained with a working example.
Why one file stops working
Every program so far in this course has lived in a single.cpp file. That's fine for a hundred lines. It falls apart at ten thousand — everyone editing the same file, one typo breaking the whole build, and no way to reuse a function in a different program without copy-pasting it.
The fix is splitting code into multiple files. The tricky part is that C++ compiles files one at a time, so a file has no idea what functions exist in another file unless you tell it.
The three-file pattern
Nearly every piece of reusable C++ code follows this shape: a header (.h) that declares what exists, a source file (.cpp) that defines how it works, and another source file that uses it.
// math_utils.h — the header: WHAT exists
int square(int x);
int cube(int x);
// math_utils.cpp — the source: HOW it works
#include "math_utils.h"
int square(int x) { return x * x; }
int cube(int x) { return x * x * x; }
// main.cpp — uses it
#include <iostream>
#include "math_utils.h"
int main() {
std::cout << square(5) << '\n'; // 25
return 0;
}
main.cpp never sees square's actual code — just its declaration from the header. That's enough. The compiler trusts the declaration, and a separate step (the linker, from the compilation pipeline lesson) matches the call in main.cpp to the real definition in math_utils.cpp at the very end.
Declaration vs. definition, one more time
This distinction from the compilation pipeline lesson is the entire reason headers exist:
int square(int x); // declaration — "this exists, here's its signature"
int square(int x) { return x*x; } // definition — "here's the actual code"
.cpp — holds the definition.
Angle brackets vs. quotes
#include <iostream> // angle brackets: look in system/library folders
#include "math_utils.h" // quotes: look in the current folder first
Use angle brackets for standard library and third-party headers. Use quotes for your own headers living alongside your code. Getting this backward usually still compiles (most compilers check both), but it's a convention worth following so your intent is clear.
The problem headers create, and header guards
Ifmath_utils.h gets #included into two different files that are both compiled into the same program, and something else causes it to be pasted in twice into one file (a common real scenario: a.h includes b.h, and main.cpp includes both a.h and b.h directly), the compiler sees the same declaration twice — and for some declarations, that's a hard error.
#ifndef MATH_UTILS_H
#define MATH_UTILS_H
int square(int x);
int cube(int x);
#endif
MATH_UTILS_H isn't defined yet, so everything between #ifndef and #endif gets included, and MATH_UTILS_H gets defined. The second time this exact header gets pasted into the same file, MATH_UTILS_H is already defined, so the preprocessor skips straight to #endif — the content is silently not duplicated.
Modern compilers also support a shorter equivalent:
#pragma once
int square(int x);
int cube(int x);
#pragma once is shorter and what most new code uses; #ifndef guards are what you'll see in older or more portable codebases, and it's worth recognizing both.
What actually goes in a header
A header should contain declarations, not full function bodies — with one common exception.
// Good: just the declaration
int square(int x);
// Usually wrong: a full function body in a header
int square(int x) { return x * x; }
.cpp file that includes it gets its own copy of that function's code, and if two files both define square, the linker sees the same function defined twice across the whole program and refuses to link — the classic "multiple definition" error.
The common exception: functions marked inline, and template functions (which you'll meet properly in a later STL lesson), are specifically allowed to appear with full bodies in headers, because the language guarantees the linker will only keep one copy even if it's pasted into many files.
Structs and classes in headers
Unlike plain functions, it's completely normal — expected, even — for a struct or class definition to live entirely in a header:
// point.h
#pragma once
struct Point {
int x = 0;
int y = 0;
int distanceFromOrigin() const; // declared here...
};
// point.cpp
#include "point.h"
#include <cmath>
int Point::distanceFromOrigin() const { // ...defined here
return std::sqrt(x * x + y * y);
}
Point:: prefix says "this is the distanceFromOrigin method belonging to Point, whose declaration you already saw in the header." Small, one-line methods are often still written directly inside the struct/class body for convenience — there's no hard rule, just a tendency to move larger method bodies out into the .cpp file to keep the header easy to skim.
Try it
Common mistakes
- Putting a full function definition in a header and including it in more than one
.cppfile, causing a "multiple definition" linker error. - Forgetting a header guard, causing "redefinition" errors the moment two files both end up including the same header.
- Using quotes for standard library headers or angle brackets for your own — works on most compilers, but breaks the convention that signals intent.
- Editing the
.cppfile and expecting other files to see the change without recompiling them — every.cppfile compiles independently and must be rebuilt. - Forward-declaring a struct's methods without matching
ClassName::prefixes when defining them separately, leading to confusing "not declared in this scope" errors.
Summary
Splitting a program across files means separating declarations (what exists, in a header) from definitions (how it works, in a.cpp file), so other files can call a function without needing to see its implementation. Header guards (#pragma once or #ifndef) stop the same declarations from being pasted into one file twice. Struct and class definitions normally live entirely in headers, with larger method bodies split out into a matching .cpp file using the ClassName::method syntax.