Header Best Practices
By the end of this lesson you'll be able to write headers that compile cleanly anywhere: you'll guard them against double-inclusion, put the right things in the header versus the source file, slim dependencies with forward declarations, and avoid the linker errors that break the One Definition Rule.
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.
Think of a header ( .h ) as the menu in a restaurant and the source file ( .cpp ) as the kitchen . The menu declares what's available — "Pasta, £9" — so customers (other files) know what they can order, without showing the recipe. The kitchen holds the definition : the actual cooking steps. Many tables can read the same menu, but there's only ever one recipe for each dish — print the full recipe on every menu and the restaurant ends up with conflicting copies. That "one recipe" rule is exactly the One Definition Rule in C++: a thing may be declared in many files, but defined only once.
Rule of thumb: the header says what exists; the source says how it works. Templates and inline functions are the exception — they must live in the header because the compiler needs the full body everywhere they're used.
1. Stop Double-Inclusion with #pragma once
A header often gets #include d through several paths — main.cpp includes app.h , which includes config.h , and main.cpp includes config.h too. Without protection, config.h gets pasted into the same file twice and you get redefinition errors. The fix is an include guard : put #pragma once on the first line and the compiler reads each header at most once per file. The classic portable form is #ifndef NAME / #define NAME / #endif , but #pragma once is one line, can't be mistyped, and every modern compiler supports it.
Both do the same job — they stop the body being included twice:
Pick one per header — you don't need both. Use a unique macro name (often the file path in CAPS) if you go with the #ifndef form.
2. Declarations in the Header, Definitions in the Source
A declaration says a name exists and what its type is (a function prototype, ending in ; ). A definition provides the body. Headers should hold declarations ; the matching .cpp holds the definitions . The exceptions that do belong in the header are things the compiler must see everywhere they're used: inline functions, templates , constexpr values, and member functions written inside the class body. Read this self-contained header, run it, and check the output against the comments.
Now compare it with a header that breaks the rules. The version below has no guard, defines a normal function in the header, and drags in a heavy include — each one a real-world bug. The comments show exactly how to fix each mistake.
Your turn. The header below should be guarded and should only declare its function. Fill in the three blanks marked ___ using the hints, then run it.
3. Forward Declarations Cut Dependencies
Every #include in a header is a dependency : change the included file and everything that includes your header recompiles too. You can avoid many of these. If your header only needs a pointer or reference to a type — Car* or Car& — you don't need its full definition; a forward declaration class Car; is enough. That breaks the include chain and speeds up builds. You only need the full #include where you store the type by value , call its members, take sizeof , or inherit from it.
Now you try. The header below stores only a pointer to Vehicle , so it shouldn't include the heavy vehicle.h — a forward declaration is enough. Fill in the blank:
Include-what-you-use (IWYU): every file should directly include a header for each name it uses, and not rely on getting it "for free" through another header. If you use std::vector , write #include vector yourself — don't assume #include iostream will drag it in. Self-contained files don't break when an unrelated include is removed.
Compile speed: heavy standard headers like iostream , map , or regex pull in thousands of lines. Including them in a widely-used header multiplies that cost across the whole project. Prefer forward declarations and push heavy includes down into the .cpp files that actually need them.
No blanks this time — just a brief and an outline. Apply every rule from the lesson: guard the header, include what you use, declare in the header and define in the source. Build it, run it, and check your output against the example in the comments.
Practice quiz
What problem does #pragma once solve?
- It speeds up the program at runtime
- It links libraries automatically
- It stops a header being pasted into the same file twice (double-inclusion)
- It marks a function inline
Answer: It stops a header being pasted into the same file twice (double-inclusion). #pragma once (or #ifndef include guards) ensures a header is parsed at most once per file, preventing redefinition errors.
What is the classic, fully portable form of an include guard?
- #ifndef NAME / #define NAME / #endif
- #pragma guard
- #once
- #protect NAME
Answer: #ifndef NAME / #define NAME / #endif. The #ifndef/#define/#endif trio is the portable standard guard; #pragma once is the one-line modern equivalent.
What is the difference between a declaration and a definition?
- They are the same thing
- A declaration provides the body; a definition only names it
- A declaration is only for variables, never functions
- A declaration says a name and type exist; a definition provides the body or storage
Answer: A declaration says a name and type exist; a definition provides the body or storage. A declaration (like int add(int,int);) names something; a definition gives its actual body or storage.
What generally belongs in a header (.h) versus the source (.cpp)?
- Definitions in the header, declarations in the source
- Declarations in the header, definitions in the source
- Everything in the header
- Only main() in the header
Answer: Declarations in the header, definitions in the source. Headers hold declarations; the matching .cpp holds the definitions — the header says what, the source says how.
Which of these CAN correctly live (defined) in a header?
- inline functions, templates, and constexpr values
- A normal non-inline function definition
- A global variable definition
- Nothing may ever be defined in a header
Answer: inline functions, templates, and constexpr values. inline functions, templates, and constexpr must be visible everywhere they're used, so they belong in the header.
Why does a non-inline function defined in a header included by two .cpp files cause a linker error?
- The header is too large
- Headers cannot contain functions
- The linker sees two identical definitions — a violation of the One Definition Rule
- The compiler runs out of memory
Answer: The linker sees two identical definitions — a violation of the One Definition Rule. Two .cpp files including the definition give two copies, triggering a 'multiple definition' ODR error. Mark it inline or move the body to a .cpp.
When is a forward declaration (class Car;) enough instead of including the full header?
- When you store the type by value
- When you only need a pointer or reference to the type (Car* or Car&)
- When you call the type's member functions
- When you inherit from the type
Answer: When you only need a pointer or reference to the type (Car* or Car&). A pointer or reference only needs the name; storing by value, calling members, sizeof, or inheriting needs the full type.
You must include the full header (not just forward-declare) when you:
- Only take a pointer to the type
- Only take a reference to the type
- Never use the type at all
- Store the type by value, call its members, inherit from it, or take sizeof
Answer: Store the type by value, call its members, inherit from it, or take sizeof. The compiler needs the type's real size and layout when you store it by value, use members, inherit, or take sizeof.
What does 'include what you use' (IWYU) mean?
- Include as few headers as possible regardless of what you use
- Every file directly includes the header for each name it uses, instead of relying on transitive includes
- Always include <iostream> in every file
- Only include headers in .cpp files, never in headers
Answer: Every file directly includes the header for each name it uses, instead of relying on transitive includes. IWYU keeps files self-contained: if you use std::vector, include <vector> yourself rather than hoping another header drags it in.
How can you break a circular include (a.h includes b.h includes a.h)?
- Add #pragma once to both files only
- Delete one of the headers
- Replace an #include with a forward declaration where only a pointer/reference is needed
- Include each header twice
Answer: Replace an #include with a forward declaration where only a pointer/reference is needed. If a.h only needs a B*, write class B; instead of #include "b.h" to break the cycle.