C++ Programming
Lesson 07 of 24

Lesson 07 · 24 lesson course

7. Header Files

Structuring your source code

Programming glossary 55 terms mentioned in this lesson

Select any term for a clear definition. The return button brings you back to the exact term link you used.

Abstract layered interface plates separating visible declarations from a protected implementation.

 

 

 

Lesson: Header Files, Includes, and the Preprocessor (C++)

Programmers organise large programs across multiple files. For example, one file may contain functions for loading photographs, while another contains functions for analysing them. If the analysis code needs the loader’s functions, we must link these files logically. In C++, this is done via header files: files that expose declarations (types, function signatures, constants) for other translation units to use.


1) What is a Header File?

  • A header (.h/.hpp) reiterates declarations (like forward declarations) so other files can call into code implemented elsewhere (.cpp).
  • Typical pattern: mylib.h (declarations) + mylib.cpp (definitions).
  • Use headers when a simple forward declaration is insufficient (e.g., you need full type information, inline/template definitions, constants, or interfaces used widely).
// image_loader.h  (header: declarations only)
#ifndef IMAGE_LOADER_H_INCLUDED
#define IMAGE_LOADER_H_INCLUDED

#include <string>
#include <vector>

std::vector<unsigned char> load_image(const std::string& path);

#endif // IMAGE_LOADER_H_INCLUDED
// image_loader.cpp (definitions)
#include "image_loader.h"
#include <fstream>
// ... definition of load_image(...)
Programming Insight (AI) — Deciding Header vs Forward Declaration
  • Paste your snippet; ask AI whether a forward declaration suffices or a header include is needed.
  • AI can split a monolithic file into .h (API) and .cpp (implementation) with correct includes.

2) Include Guards & #pragma once

The preprocessor runs before compilation and processes directives beginning with #. To prevent multiple inclusion of the same header (which would cause redefinition errors), use an include guard:

// add.h
#ifndef ADD_H_INCLUDED
#define ADD_H_INCLUDED

int add(int a, int b);

#endif // ADD_H_INCLUDED

Modern compilers often support #pragma once as a concise alternative:

// add.h
#pragma once
int add(int a, int b);

Naming tip: Make guard names unique and consistent (e.g., PROJECT_ADD_H_INCLUDED).

Programming Insight (AI) — Auto-Guard Your Headers
  • Ask AI to add or normalise include guards across your project.
  • Have AI convert guards ↔ #pragma once depending on your toolchain policy.

3) #include Paths: "file.h" vs <header>

  • #include "add.h" — search current project paths first (your local headers).
  • #include <iostream> — search compiler/standard library include paths (system or well-known libraries).

Keep the order of includes irrelevant by writing self-sufficient headers:

  • Each header must include what it needs (no hidden dependence on include order).
  • Avoid using namespace in headers.
  • Prefer forward declarations to reduce heavy includes when possible.
Programming Insight (AI) — Include What You Use
  • Have AI audit a header for missing/extra includes and suggest forward declarations.
  • Ask AI to break cyclical dependencies by refactoring to interfaces or pimpl.

4) Worked Example: Loading vs Analysing Photos

// photo_loader.h
#pragma once
#include <string>
#include <vector>
std::vector<unsigned char> load_photo(const std::string& path);

// photo_analyser.h
#pragma once
#include <string>
double sharpness_score(const std::string& path);

// photo_analyser.cpp
#include "photo_analyser.h"
#include "photo_loader.h"    // we USE load_photo, so include its header

double sharpness_score(const std::string& path) {
    auto pixels = load_photo(path);
    // ... compute a score ...
    return 0.0;
}

Note that photo_analyser.cpp includes photo_loader.h because it uses its API. The analyser header does not include the loader header because it doesn’t expose that dependency in its own interface.

Programming Insight (AI) — Minimise Public Dependencies
  • Ask AI to ensure headers expose only what’s necessary; move extras to .cpp.
  • AI can suggest pimpl to hide implementation details and cut compile time.

5) Forward Declarations vs Includes

Use a forward declaration when you only need a name, not its size/layout:

// ok to forward-declare a class
class Image;

void process(Image&);  // we only use a reference; no full definition needed here

Must include the header when you need the full definition:

Programming Insight (AI) — Forward-Decl Decision Helper
  • Paste a header; ask AI which includes can become forward declarations safely.
  • AI can check for ODR/multiple definition risks after refactors.

6) Interesting Cases & Common Pitfalls

A) Multiple Definition Errors

// bad.h
#pragma once
int x = 42;        // ❌ definition in a header — each .cpp that includes this defines x again

// fix: declaration in header, definition in one .cpp
// good.h
#pragma once
extern int x;      // declaration only

// good.cpp
#include "good.h"
int x = 42;        // single definition

B) Inline Variables (C++17+) & Constants

// header-only constants
#pragma once
inline constexpr int MaxSize = 1024; // ✅ safe to define in header (one definition rule friendly)

C) Templates & Header-Only Code

// templates must have definitions visible to all TUs
#pragma once
template <typename T>
T add(T a, T b) { return a + b; } // definition stays in the header

D) Cyclic Includes

// A.h
#pragma once
class B;            // forward declaration
class A { B* b; };  // ok: pointer only

// B.h
#pragma once
#include "A.h"      // B needs full A? if not, prefer forward declare to break cycle
class B { A* a; };

E) Precompiled Headers (PCH)

Large projects sometimes use a precompiled header to speed builds (e.g., pch.h), but keep headers clean: include what you use, don’t rely on PCH to smuggle dependencies.

Programming Insight (AI) — Diagnose Build & ODR Issues
  • Ask AI to explain linker errors (duplicate symbols, unresolved externals) and propose header/definition fixes.
  • AI can produce an “include map” to show where heavy or cyclical headers are coming from.

7) Mini Exercise — Fix the Header

Problem: The following compiles slowly and sometimes fails to link.

// bad_math.h
#pragma once
#include <vector>
#include <iostream>
int add(int a, int b) { return a + b; } // ❌ definition in header
int mult(int a, int b);                  // declaration

// bad_math.cpp
#include "bad_math.h"
int mult(int a, int b) { return a * b; }

Target: Move add definition to .cpp, keep header clean and self-sufficient.

// math.h
#pragma once
int add(int a, int b);
int mult(int a, int b);

// math.cpp
#include "math.h"
int add(int a, int b) { return a + b; }
int mult(int a, int b) { return a * b; }
Programming Insight (AI) — Refactor Plan
  • Ask AI to propose a patch: move definitions out of headers, keep only declarations, add guards.
  • Have AI generate a quick unit test that includes math.h from multiple TUs to confirm no duplicate symbols.

8) Summary Checklist

  • Headers expose declarations; definitions live in .cpp (except templates/inline/constexpr).
  • Protect headers with include guards or #pragma once.
  • Include what you use; make header order irrelevant.
  • Avoid globals defined in headers; use extern or inline constexpr instead.
  • Break cycles with forward declarations or pimpl.
  • No using namespace in headers.

Here is a video of Graham having a go at using header files (look for the point he deletes the #pragma once!).

Lesson 7 extension · follow the declaration through the build

Which translation unit knows the promise, and where is it kept?

Dividing a project into neatly named files is visible organisation. It is not yet evidence that the files form a correct program. The stronger test is whether each separately compiled source file sees the declarations it needs and whether the complete build supplies the required definitions without contradiction.

An #include directive does not link a header to an implementation file. During preprocessing, the header's tokens become available within the including source. That result is compiled as one translation unit. Other source files are processed and compiled separately, and the linker later combines their object files and resolves references between them.

This model explains why adding a header can remove a compiler error while leaving a linker error untouched. The declaration may now be visible, so the compiler can check the call. If no linked object file contains the required definition, the program is still incomplete. Different stage, different evidence.

Build one function through three files

The header states the interface shared by its users:

#ifndef CPP_PROGRAMMING_SCORE_H_INCLUDED
#define CPP_PROGRAMMING_SCORE_H_INCLUDED

int add_bonus(int score, int bonus);

#endif

The implementation source includes that same declaration and supplies the ordinary function definition:

#include "score.h"

int add_bonus(int score, int bonus) {
    return score + bonus;
}

The application source includes the interface and calls the operation:

#include "score.h"

#include <iostream>

int main() {
    std::cout << add_bonus(40, 2) << '\n';
}

Preprocess and compile score.cpp, and one object file contains the definition of add_bonus. Preprocess and compile main.cpp, and another object file contains a reference to that operation after checking the call against the declaration from score.h. Link the two object files, and the reference can be matched to the definition.

The header is not compiled once as a third implementation unit. Its guarded contents are processed inside score.cpp's translation unit and again inside main.cpp's translation unit. Both source files therefore check against the same written declaration, while only score.cpp supplies the ordinary definition.

StageInput being consideredWhat success establishesTypical failure
PreprocessingOne source file and its included headersThe required tokens can be formed for this translation unitMissing header or malformed directive
CompilationOne preprocessed translation unitNames, types and expressions are valid in that unitUndeclared name or type error
LinkingObject files and librariesRequired definitions can be assembled into a programUndefined reference or multiple definition

An include guard controls repetition within one translation unit

The macro guard in score.h prevents its guarded contents from being processed repeatedly during one translation unit's preprocessing. This matters when several include paths lead back to the same header. It does not make the header appear only once across the entire program. main.cpp and score.cpp each perform their own preprocessing.

That boundary explains what guards can and cannot fix. A repeated class definition inside one translation unit may be prevented by the guard. An ordinary non-inline function definition placed in a guarded header can still be reproduced in several translation units, because each unit gets one copy. The guard has worked and the program may still violate the One Definition Rule.

#pragma once is a convenient and widely implemented alternative for preventing repeated processing of a header. It is not a directive required by the C++ standard. A conventional macro guard remains the portable course mechanism. Whichever form a project chooses, consistency is useful; neither form repairs a misplaced definition.

The interface must include what its own declarations require

A header should support its declarations without depending on a fortunate include order in some unrelated source file. If a declaration requires a complete standard-library type, include the standard header that provides it. Compiling a tiny source file that includes only the header is a practical test of that self-sufficiency.

A forward declaration is appropriate when the incomplete type is sufficient for the declaration being written. A function declaration taking a class by reference can often name a forward-declared class. A data member stored by value requires the complete class definition because its size and layout must be known. The decision is not "fewer includes are always better". The decision is whether the interface needs the complete definition.

The same restraint applies to definitions. An ordinary non-inline function usually belongs in one implementation source file. The inline rules permit suitable identical definitions in multiple translation units; the keyword does not promise that the compiler will substitute the body at each call. Template definitions are commonly kept visible in headers because their instantiation model requires that visibility. Lesson 23 will deal with that mechanism rather than using it as an exception that obscures today's rule.

Classify the failure before changing the boundary

Remove #include "score.h" from main.cpp and the call has no declaration in sight. That is a compilation problem in the main.cpp translation unit. Restore the header but omit the object file built from score.cpp, and both source files may compile before the linker reports that the definition is missing. Move the function definition into the header and include it from both source files, and the build can acquire competing definitions.

These failures may all mention add_bonus, but they do not ask for the same repair. Adding another include to an undefined-reference error does not manufacture the missing linked definition. Adding an include guard to an ordinary definition reproduced across translation units does not make that definition unique. Read the stage first, then account for the declaration and definition.

Dependencies are part of the design

Every include creates a source dependency. A large public header that exposes implementation details makes more translation units depend on those decisions and can increase the amount of code rebuilt after a change. Build time is visible, but it is still a consequence. The architectural question is whether callers genuinely need the exposed information.

A cyclic include is likewise a symptom before it is a diagnosis. Forward declarations can break some textual cycles when incomplete types are sufficient. They cannot decide whether two components should know about each other. If type A's public interface requires type B and type B's public interface requires type A, draw both arrows and explain the responsibilities. The preprocessor can be made quiet while the dependency direction remains confused.

Programming Insight (AI): classify the build stage before accepting a repair

Give an AI tool the exact diagnostic, the build command and the three relevant files. Require it to identify the failed stage, the translation unit being compiled or the objects being linked, the visible declaration and the expected definition. Compare that account with the actual build inputs before applying a change. A proposal to add includes everywhere may remove one undeclared-name error while increasing coupling or creating a different definition problem. Finish with a clean build, because an incremental success can preserve stale evidence.

Run three deliberate failures

Use the three-file example and change one fact at a time. First remove the include from main.cpp. Next restore it and leave score.cpp's object file out of the link. Finally place the ordinary definition in the header and include that header from both source files. Before each build, predict the first failing stage and the declaration or definition account that will be incomplete.

Record the actual diagnostic beside the prediction, then restore the correct boundary and perform a clean build. If your explanation is only "the header was wrong", continue. State which translation unit saw which declaration, where the definition existed, and what the linker was asked to combine. Headers organise source, but their real value is that they make a shared promise available wherever that promise must be checked.

Reveal answer

The precise wording of a diagnostic belongs to the compiler and linker used for the exercise. It should be copied from the actual build rather than invented. The stage and the declaration-definition account, however, can be predicted.

ChangeFirst failing stageReasonTypical diagnostic category
Remove #include "score.h" from main.cppCompilation of the main.cpp translation unitUnqualified lookup at the call has no visible declaration for add_bonus.add_bonus was not declared, or an equivalent compiler message
Restore the include but omit the object built from score.cpp from the linkLinkingmain.cpp saw a valid declaration and compiled a reference, but the linker received no object containing the definition.Undefined reference or unresolved external symbol
Put the ordinary non-inline definition in the guarded header and include it from both source filesThe program violates the One Definition Rule; a conventional build normally fails while linkingEach translation unit acquires its own externally linked definition. The include guard prevents repetition inside one translation unit, not across the program.Multiple definition or already defined symbol

After restoring the correct boundary, score.h contains one declaration. Preprocessing makes that declaration visible in both translation units. The score.cpp translation unit sees the declaration and supplies the sole ordinary definition. The main.cpp translation unit sees the same declaration and contains a checked call that refers to the function.

The linker is then asked to combine the object built from main.cpp with the object built from score.cpp. It can match the reference in the former to the definition in the latter. A clean build should succeed, and the example prints 42.

The third failure is not repaired by adding another guard. Marking a deliberately header-defined function inline changes the definition rules, but that would answer a different design question. This exercise requires one ordinary definition in the implementation source.