Modular Apps
By the end of this lesson you'll be able to split a C++ program across multiple files — declarations in headers, definitions in source files — understand how translation units compile and link separately, organise code with namespaces, and recognise where C++20 modules are heading. This is how every real-world C++ codebase is built.
Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.
Part of the free C++ course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
A header file is a restaurant menu ; a source file is the kitchen . The menu ( .h ) lists what you can order — the dish names and what comes with them — without revealing the recipes. The kitchen ( .cpp ) holds the actual recipes — how each dish is made. A customer (another file) only needs the menu to place an order; they never walk into the kitchen. This is why you #include a header to use something, but the recipe is compiled just once in its own .cpp . Swap the kitchen's recipe and every customer still orders the same way — the menu didn't change.
1. Headers vs Source Files: the WHAT and the HOW
A real C++ program is rarely one file. You split it into headers ( .h ) and source files ( .cpp ). A header holds declarations — a function's name and the types it takes, written as a single line ending in a semicolon ( ). It tells the compiler what exists. The source file holds the definition — the actual body in {' '} braces — the how . Other files #include the header to learn what's available; only the matching .cpp compiles the body. Read this worked example — each block is labelled with the file it belongs in.
Pro Tip: put only declarations in a header. A definition placed in a header gets compiled into every file that includes it, which bloats compile time and causes "multiple definition" errors at link time. The exceptions are templates and short inline functions, which must live in the header.
Your turn. Below is a finished function definition . Write the matching declaration — the line that would sit in the header so other files can call it. It is just the first line of the definition, ending in a ; instead of {' '} .
2. Translation Units & Linking
When you compile, each .cpp — plus everything its #include s pull in — becomes one translation unit . The compiler turns each translation unit into an object file ( .o ) completely on its own; it never sees the other .cpp files. Then the linker joins all the object files into one program, matching every function call to the single definition of that function. This is separate compilation , and it's the reason a one-line change rebuilds one file in a second instead of the whole project.
3. Namespaces — Organise Code & Avoid Collisions
As a project grows, two modules will eventually want the same name — both a graphics and a physics module might define a Point . A namespace is a named box you wrap code in so those names don't clash. You reach into a namespace with the scope-resolution operator :: , as in . Namespaces are how the standard library keeps everything tidy under std:: .
Common mistake: never put using namespace std; in a header . It silently pulls all of std into every file that includes the header, re-introducing exactly the name collisions namespaces exist to prevent. Inside a .cpp it's a convenience; in a header it's a trap. (The demos here use it only because they're single-file teaching examples.)
Now you try. The greet() function below lives inside the bank namespace, so calling it bare won't find it. Qualify the call with the box name using :: .
4. Putting It Together: a Small Multi-File App
Here is the payoff. This little app is split into logger.h , app.h , and main.cpp . The benefits of this separation are concrete: faster builds (change logger.cpp and only it recompiles), clean APIs (a teammate using App reads only app.h , not its internals), independent testing (you can test Logger on its own), and reuse (drop logger.h into another project). Each block below is labelled with its file and the #include wiring is shown in comments.
5. A Peek Ahead: C++20 Modules
C++20 introduced modules , a modern replacement for the #include system. Instead of pasting header text into every file, you write export module math; in a module-interface file and mark the public bits with export . Other files write import math; — no header, no include guards. Modules parse once (so builds are faster) and only export ed names leak out (so APIs stay clean). Toolchain support is still uneven in 2026, so headers remain the portable default — but it's worth recognising the syntax.
The One Definition Rule says a function or variable may be declared as many times as you like, but defined exactly once across the whole program. Declarations are promises ("this exists somewhere"); the linker needs precisely one body to point each call at.
Two failures break it. If a header has no include guard, including it twice in one .cpp defines its contents twice — the compiler errors with "redefinition". If you put a function body in a header that two .cpp files include, each translation unit gets its own copy and the linker errors with "multiple definition". The fix for the first is #pragma once ; the fix for the second is to keep bodies in a .cpp .
No blanks this time — just a brief and an outline. Build a tiny weather module with a conversion function, then call it through its namespace. This mirrors exactly how you'd carve a real feature into its own header and source file.
Practice quiz
What belongs in a header (.h) file?
- Function and method bodies — the HOW
- Declarations — function signatures, class definitions, constants — the WHAT
- The program's main() definition only
- Compiled object code
Answer: Declarations — function signatures, class definitions, constants — the WHAT. Headers hold declarations (the WHAT); the matching .cpp holds the definitions (the HOW).
What is a translation unit?
- One .cpp file plus everything its #includes pull in, compiled into one object file
- A header file on its own
- The final linked executable
- A single function inside a class
Answer: One .cpp file plus everything its #includes pull in, compiled into one object file. A translation unit is a .cpp plus its includes, compiled independently into one .o file; the linker joins them later.
What is the job of the linker?
- It expands #include directives
- It checks syntax of each file
- It joins object files, matching each call to the one definition of that function
- It runs the program
Answer: It joins object files, matching each call to the one definition of that function. The linker combines the separately compiled object files, resolving each call to its single definition.
Given a definition int square(int n) { return n*n; }, what is the matching header declaration?
- int square(int n) { }
- int square(int n);
- square(int n);
- declare int square(int n);
Answer: int square(int n);. The declaration is the signature followed by a semicolon, with no body.
What does the One Definition Rule (ODR) require?
- Every function may be defined at most once per file
- A function may be declared many times but defined exactly once across the whole program
- Headers may contain no declarations
- Each .cpp must define main()
Answer: A function may be declared many times but defined exactly once across the whole program. Declarations can repeat freely, but exactly one definition must exist program-wide for each function or variable.
Putting a function body in a header that two .cpp files include causes what error?
- A 'multiple definition' linker error
- A run-time crash only
- Nothing — it is fine
- A missing-header error
Answer: A 'multiple definition' linker error. Each translation unit compiles its own copy of the body, so the linker reports a multiple-definition (ODR) error. Keep bodies in a .cpp.
What does #pragma once do?
- Compiles the file only on the first build
- Stops a header being processed more than once per translation unit
- Forces the header to be included exactly once program-wide
- Marks the file as a module
Answer: Stops a header being processed more than once per translation unit. #pragma once prevents double inclusion within a translation unit — the same job as traditional include guards.
Why should you never put using namespace std; in a header?
- It slows compilation by 50%
- It is a syntax error in headers
- It forces the import on every file that includes the header, inviting name clashes
- It disables std entirely
Answer: It forces the import on every file that includes the header, inviting name clashes. A using-directive in a header spreads into every including file, re-introducing the collisions namespaces exist to prevent.
When a.h includes b.h which includes a.h, how do you break the cycle?
- Delete one of the headers
- Use a forward declaration (class B;) when you only need a pointer or reference
- Add using namespace std;
- Compile with -O2
Answer: Use a forward declaration (class B;) when you only need a pointer or reference. Replacing an #include with a forward declaration (when only a pointer/reference is needed) breaks the circular dependency.
What is the C++20 modules syntax for declaring and using a module?
- #module math; and #use math;
- export module math; and import math;
- namespace math; and using math;
- module math {}; and include math;
Answer: export module math; and import math;. A module interface writes export module math; and only export'ed names leak; consumers write import math; with no #include.