Lesson overview
C++ builds source files as translation units. Header files let one translation unit know about declarations from another, but careless headers create duplicate definitions, cyclic includes and name pollution.
Learning objectives
- Explain the difference between declarations in headers and definitions in source files.
- Use include guards or
#pragma onceto prevent repeated inclusion. - Distinguish quoted project includes from angle-bracket library includes.
- Avoid multiple definition errors and
using namespacepollution in headers. - Break cyclic dependencies with forward declarations where appropriate.
Key vocabulary before you start
Declaration
A promise that a name and type exist.
Definition
The full implementation or storage allocation for an entity.
Include guard
A preprocessor pattern preventing repeated inclusion of the same header.
Forward declaration
A declaration that avoids requiring a full type definition immediately.
Translation unit
The post-preprocessing source compiled as one unit.
Header pollution
Unnecessary includes, namespace imports or definitions exposed to every includer.
Header hygiene map
Headers are public promises. Keep them small, guarded and free from unnecessary definitions.
Declare in headers
Headers usually announce interfaces; source files provide ordinary definitions.
Guard every header
Use include guards or #pragma once so repeated inclusion is harmless.
Avoid global pollution
Do not place using namespace or unnecessary globals in headers.
Minimise dependencies
Include what you need, but use forward declarations when a complete type is not required.
Example: declaration in header, definition in source
The header exposes the function; the source file implements it exactly once.
// add.h
#ifndef ADD_H
#define ADD_H
int add(int a, int b);
#endif
// add.cpp
#include "add.h"
int add(int a, int b) { return a + b; }Trace the state
- A caller includes
add.hto know the declaration. add.cppcontains the single definition.- The linker connects call sites to that definition.
- Repeated includes are harmless because the guard prevents duplicate declarations within one translation unit.
| Line or operation | Variable | Value | State role | Invariant or check |
|---|---|---|---|---|
| Read the example before running it | Key names and calls | Known from source | Reasoning setup | Each named element should support the lesson focus: Header Files, Includes and the Preprocessor in C++. |
| Trace the first meaningful operation | Primary state or boundary | Established by initialisation, call or condition | Valid-state checkpoint | Later reasoning must not use the value before this checkpoint. |
| Identify the first decision or dereference | Control or access point | Depends on current state | Failure-mode checkpoint | The condition, pointer, argument or dependency must be valid before use. |
| Record the observable result | Return value, output or mutation | Produced by the example | Evidence checkpoint | The result should match the contract explained in the lesson. |
Visual model: header/source dependency boundary
A good header exposes the minimum contract needed by other translation units.
Expose declarationsHeaders publish the names and types clients need.
Hide definitionsOrdinary function definitions live in one source file unless there is a deliberate exception.
Guard inclusionInclude guards or #pragma once prevent repeated text inclusion.
Reduce couplingForward declare when a full definition is unnecessary.
Link onceExactly one compiled definition satisfies external references.
Boundary task
For the bad header exercise, label which lines belong in the header, which belong in the source file and which should be removed.
What is a header file?
A header file usually contains declarations that other translation units need to compile: function declarations, type declarations, constants and templates.
It should not casually contain ordinary object or function definitions that produce storage or symbols in every includer.
Treat a header as the public surface of a module. Keep it clear, stable and minimal.
Image loader split
The header announces the API; the source file implements it.
// image_loader.h
#pragma once
#include <string>
struct Image;
Image load_image(const std::string& path);
// image_loader.cpp
#include "image_loader.h"
Image load_image(const std::string& path) {
// load bytes and build Image
return Image{};
}Applied task
Look at a header and mark each line as interface, implementation or avoidable dependency.
Include guards and #pragma once
A header may be included through several paths. Guards prevent the same header content being processed more than once in a single translation unit.
Traditional include guards use preprocessor macros. #pragma once is widely supported and concise, though technically non-standard.
Use one style consistently within a project.
Traditional guard
The macro ensures the declarations appear once per translation unit.
#ifndef MATH_ADD_H
#define MATH_ADD_H
int add(int a, int b);
#endifPragma once
A common modern alternative.
#pragma once
int add(int a, int b);Applied task
Choose one guard style for a small project and explain the consistency benefit.
#include paths: quotes vs angle brackets
Quoted includes such as #include "photo_loader.h" are normally used for project headers.
Angle-bracket includes such as #include <vector> are normally used for standard library or configured include-path headers.
The exact search rules depend on the compiler and build system, so keep project include paths deliberate.
Project and library includes
Use the form that communicates where the declaration lives.
#include "photo_loader.h"
#include <vector>
#include <string>Applied task
Explain why a project header should not look like a standard library header.
Worked example: loading vs analysing photos
Separate modules should expose the smallest interface each user needs. Loading a photo and analysing a photo are different responsibilities.
Headers should make those responsibilities visible without dragging implementation details into every caller.
This also keeps rebuilds smaller because changing implementation can avoid forcing every dependent source file to recompile.
Two module interfaces
The analyser can include only what it needs.
// photo_loader.h
#pragma once
#include <string>
struct Photo;
Photo load_photo(const std::string& path);
// photo_analyser.h
#pragma once
struct Photo;
double brightness_score(const Photo& photo);
// photo_analyser.cpp
#include "photo_analyser.h"
#include "photo_loader.h"
double brightness_score(const Photo& photo) { return 0.0; }Applied task
Decide which header a file needs if it only stores a pointer or reference to Photo.
Forward declarations vs includes
A forward declaration tells the compiler that a type exists without giving the full definition.
This is enough for pointers and references, but not enough when you need object size, member access or inheritance details.
Forward declarations help break cyclic includes and reduce compile-time coupling.
Forward declare for references
The full Image definition can live in a source file if the header only uses a reference.
class Image;
void process(Image& image);Applied task
Identify which declarations require a complete type and which can use a forward declaration.
Interesting cases and common pitfalls
Putting int x = 42; in a header defines an object in every translation unit that includes it, causing multiple definitions.
Use extern declarations plus a single source-file definition for shared objects, or inline constexpr for suitable C++17 constants.
Templates usually live in headers because the compiler needs the definition to instantiate them.
Bad global in a header
Every includer defines x, so the linker sees duplicates.
// bad.h
int x = 42; // wrong in a headerExtern plus single definition
Declare in the header, define once in a .cpp.
// good.h
#pragma once
extern int x;
// good.cpp
#include "good.h"
int x = 42;Header-only template
Template definitions are commonly kept in headers.
template <typename T>
T add(T a, T b) {
return a + b;
}Applied task
For a header that defines globals, decide whether each item should become extern, inline constexpr, a function, or a source-file detail.
Applied case lab
Case 1: The duplicate symbol
A header defines int counter = 0; and is included by two source files. Explain the multiple-definition error and repair it.
Case 2: The include cycle
A.h includes B.h and B.h includes A.h. Decide where a forward declaration is sufficient.
Case 3: Header pollution
A header uses using namespace std;. Explain how that affects every includer and remove it.
Applied task: fix the header
Repair a bad header/source split and explain the build-system effect.
Stage 1: read and classify
A. Move a definition out of the header
This header will define add in every includer.
// bad_math.h
#pragma once
int add(int a, int b) { return a + b; }
// a.cpp
#include "bad_math.h"
// b.cpp
#include "bad_math.h"- Keep the declaration in the header.
- Move the definition to
math.cpp. - Explain why the linker error disappears.
Reveal one possible refactor
// math.h
#pragma once
int add(int a, int b);
// math.cpp
#include "math.h"
int add(int a, int b) { return a + b; }Model reasoning
A strong answer explains why the change preserves behaviour, what failure mode it removes, and which trade-off remains acceptable.
Stage 2: trace or repair
B. Clean a header
Remove namespace pollution and unnecessary includes.
// report.h
#pragma once
#include <iostream>
using namespace std;
void print_report(const string& title);- Replace broad namespace use with qualified names.
- Include only the declaration dependencies.
Reveal one possible refactor
// report.h
#pragma once
#include <string>
void print_report(const std::string& title);Model reasoning
A strong answer explains why the change preserves behaviour, what failure mode it removes, and which trade-off remains acceptable.
Stage 3: justify the design
C. Break a cycle
A pointer or reference can often use a forward declaration.
// A.h
#include "B.h"
class A { B* b; };
// B.h
#include "A.h"
class B { A* a; };- Use forward declarations.
- Move includes into
.cppfiles where full definitions are needed.
Reveal one possible refactor
// A.h
#pragma once
class B;
class A { B* b; };
// B.h
#pragma once
class A;
class B { A* a; };Model reasoning
A strong answer explains why the change preserves behaviour, what failure mode it removes, and which trade-off remains acceptable.
Quick checks
1. What should a header usually contain for an ordinary function?
2. Why are include guards used?
3. Which is unsafe in a public header?
AI-augmented practice notes
Use these as prompts for disciplined support, not as permission to outsource judgement.
Header Hygiene Coach
- Ask AI to classify each header line as declaration, definition, include, forward declaration or pollution.
- Ask AI to propose a
.h/.cppsplit and list which files should include which header. - Ask for a duplicate-symbol diagnosis map showing which translation units define the same symbol.
Include Cycle Repair
- Provide two cyclic headers and ask where a forward declaration is enough.
- Ask AI to remove
using namespacefrom headers and update signatures.
Assessment tasks
- Repair one bad header by moving definitions to a
.cppfile. - Draw a dependency diagram for three headers and two source files.
- Explain one case where a template definition belongs in a header.
Judgement questions
What makes a header public design rather than private implementation?
Describe one declaration that belongs in a header and one implementation detail that should stay in a source file.
Which include could you remove from a header?
Find a header that includes more than it needs and explain the compile-time or coupling cost.
