Masters-level programming lessons for professional software judgement.

Masters Programmes · Masters Software Engineering · Lesson 7

Header Files, Includes and the Preprocessor in C++

Use headers and includes to share declarations cleanly across C++ translation units without causing duplicate definitions, cycles or global pollution.

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.

LevelMasters
ModeSeminar, build-error diagnosis and refactoring lab
EvidenceA corrected header/source split with include guards, declarations in headers and definitions in one source file
SourceConverted from supplied Canvas lesson HTML

Learning objectives

  • Explain the difference between declarations in headers and definitions in source files.
  • Use include guards or #pragma once to prevent repeated inclusion.
  • Distinguish quoted project includes from angle-bracket library includes.
  • Avoid multiple definition errors and using namespace pollution 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.

H1

Declare in headers

Headers usually announce interfaces; source files provide ordinary definitions.

H2

Guard every header

Use include guards or #pragma once so repeated inclusion is harmless.

H3

Avoid global pollution

Do not place using namespace or unnecessary globals in headers.

H4

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

  1. A caller includes add.h to know the declaration.
  2. add.cpp contains the single definition.
  3. The linker connects call sites to that definition.
  4. Repeated includes are harmless because the guard prevents duplicate declarations within one translation unit.
Line or operationVariableValueState roleInvariant or check
Read the example before running itKey names and callsKnown from sourceReasoning setupEach named element should support the lesson focus: Header Files, Includes and the Preprocessor in C++.
Trace the first meaningful operationPrimary state or boundaryEstablished by initialisation, call or conditionValid-state checkpointLater reasoning must not use the value before this checkpoint.
Identify the first decision or dereferenceControl or access pointDepends on current stateFailure-mode checkpointThe condition, pointer, argument or dependency must be valid before use.
Record the observable resultReturn value, output or mutationProduced by the exampleEvidence checkpointThe 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);

#endif

Pragma 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 header

Extern 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 .cpp files 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 / .cpp split 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 namespace from headers and update signatures.

Assessment tasks

  1. Repair one bad header by moving definitions to a .cpp file.
  2. Draw a dependency diagram for three headers and two source files.
  3. 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.