Header Best Practices

Reviewed & published by Brayan K

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.

Part of the free C++ course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

💡 Real-World Analogy

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.

📊 Header vs Source — What Goes Where

Goes in the header (.h)Goes in the source (.cpp)
Function declarations (prototypes)Function definitions (the bodies)
Class / struct definitionsOut-of-class member function bodies
inline functions & templatesNon-inline helper functions
constexpr / const constantsStatic / global variable definitions
#pragma once at the very topIts own header included first

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 #included 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.

🔎 The two forms of include guard

Both do the same job — they stop the body being included twice:

// Modern, preferred:
#pragma once
// ... header contents ...

// Classic, fully portable:
#ifndef CONFIG_H
#define CONFIG_H
// ... header contents ...
#endif  // CONFIG_H

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.

// ===== point.h  (a GOOD header) =====
#pragma once          // 1) include guard: parse this file at most once
#include <string>     // 2) include-what-you-use: we name std::string below

// Only DECLARATIONS live here — no function bodies (except inline/templates).
struct Point {
    double x;
    double y;
    std::string label;          // we USE std::string -> we INCLUDE <string>
};

// DECLARATION only: the body lives in point.cpp.
double distance(const Point& a, const Point& b);

// An inline function MAY live in a header: 'inline' lets every .cpp that
// includes this file share one definition without breaking the linker.
inline Point origin() { return Point{0.0, 0.0, "origin"}; }


// ===== point.cpp  (the matching source file) =====
// (Shown together here so it runs; in a real project these are 2 files.)
#include <iostream>
#include <cmath>

// DEFINITION: the actual body of the declared function.
double distance(const Point& a, const Point& b) {
    double dx = a.x - b.x;
    double dy = a.y - b.y;
    return std::sqrt(dx * dx + dy * dy);   // length of the line between them
}

int main() {
    Point home = origin();                 // inline fn from the header
    Point shop{3.0, 4.0, "shop"};
    std::cout << home.label << " -> " << shop.label << " = "
              << distance(home, shop) << std::endl;   // origin -> shop = 5
    return 0;
}

// ✅ Expected output:
//    origin -> shop = 5

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.

// ===== maths.h  (a BAD header — three mistakes) =====
// MISTAKE 1: no #pragma once / include guard.
//   If two files include this, it is parsed TWICE -> "redefinition" errors.

// MISTAKE 2: a non-inline DEFINITION (a body) in a header.
//   If two .cpp files include maths.h, the linker sees TWO copies of
//   square() and reports a multiple-definition error (One Definition Rule).
int square(int n) {        // <-- body in a header, and NOT marked inline
    return n * n;
}

// MISTAKE 3: pulling a heavy header you do not need.
//   #include <iostream>   // forces every includer to parse all of <iostream>

// ----- HOW TO FIX IT (the good version) -----
// maths.h:
//   #pragma once          // guard added
//   int square(int n);    // DECLARATION only
// maths.cpp:
//   #include "maths.h"
//   int square(int n) { return n * n; }   // DEFINITION lives here

// Below compiles only because it is a single translation unit:
#include <iostream>
int main() {
    std::cout << "square(5) = " << square(5) << std::endl;   // square(5) = 25
    std::cout << "Now imagine TWO .cpp files including maths.h ..." << std::endl;
    std::cout << "-> linker error: multiple definition of 'square(int)'"
              << std::endl;
    return 0;
}

// ✅ Expected output:
//    square(5) = 25
//    Now imagine TWO .cpp files including maths.h ...
//    -> linker error: multiple definition of 'square(int)'

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.

// ===== greeter.h =====
// 🎯 YOUR TURN — make this header correct. Replace each ___.

___                       // 👉 add the one-line include guard for this header
#include <string>         // we name std::string below -> include it

// This should be a DECLARATION only (no { ... } body in the header).
std::string greet(const std::string& name)___   // 👉 end the line with a ;


// ===== greeter.cpp (DEFINITION lives here) =====
#include <iostream>

std::string greet(const std::string& name) {
    return "Hello, " + name + "!";
}

int main() {
    std::cout << greet("Ada") << std::endl;
    return 0;
}

// ✅ Expected output:  Hello, Ada!
//    (and greeter.h can now be included by many files safely)

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.

// ===== engine.h =====
#pragma once

// FORWARD DECLARATION: "a class named Car exists somewhere."
// We only store a POINTER to Car here, so we do NOT need <car.h>.
// That keeps engine.h light and stops a Car change recompiling everyone.
class Car;                 // <-- no #include "car.h" needed!

class Engine {
    Car* owner = nullptr;  // a pointer -> forward declaration is enough
public:
    void attachTo(Car* c) { owner = c; }
    bool isAttached() const { return owner != nullptr; }
};

// ===== main.cpp =====
#include <iostream>

// You only need the FULL definition where you actually USE Car's members.
struct Car { const char* name; };

int main() {
    Car tesla{"Tesla"};
    Engine e;
    std::cout << "Attached? " << e.isAttached() << std::endl;  // Attached? 0
    e.attachTo(&tesla);
    std::cout << "Attached? " << e.isAttached() << std::endl;  // Attached? 1

    // Rule of thumb:
    //   pointer / reference  -> forward declaration is enough
    //   by value / sizeof / members / inherit -> need the full #include
    return 0;
}

// ✅ Expected output:
//    Attached? 0
//    Attached? 1

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:

// ===== garage.h =====
#pragma once

// 🎯 YOUR TURN — garage.h only stores a POINTER to Vehicle.
// Replace ___ so the header does NOT include heavy "vehicle.h".

___                        // 👉 forward-declare the class:  class Vehicle;

class Garage {
    Vehicle* parked = nullptr;   // just a pointer -> no full type needed here
public:
    void park(Vehicle* v) { parked = v; }
    bool isEmpty() const { return parked == nullptr; }
};

// ===== main.cpp =====
#include <iostream>
struct Vehicle { const char* plate; };   // full definition, used here

int main() {
    Garage g;
    std::cout << "Empty? " << g.isEmpty() << std::endl;   // Empty? 1
    Vehicle v{"ABC-123"};
    g.park(&v);
    std::cout << "Empty? " << g.isEmpty() << std::endl;   // Empty? 0
    return 0;
}

// ✅ Expected output:
//    Empty? 1
//    Empty? 0

🔎 Deep Dive: include what you use, and keep includes light

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.

// In a header: prefer light
class Texture;                 // forward declare, no <texture.h>

// In the .cpp: include the heavy stuff where you use it
#include "texture.h"
#include <vector>              // because this .cpp uses std::vector

Pro Tips

Common Errors (and the fix)

📋 Quick Reference

GoalDo thisWhy
Avoid double-include#pragma onceStops redefinition
Function in headerint f(int);Declaration only
Function in sourceint f(int n){...}One definition (ODR)
Body must be in headerinline / templateVisible everywhere, safe
Only need a pointerclass Car;Forward declare, no include
Use a name#include <string>Include what you use
First include in .cpp#include "this.h"Proves it's self-contained

Mini-Challenge: Design a Clean Logger Header

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.

// 🎯 MINI-CHALLENGE: design a clean Logger header
//
// Imagine two files: logger.h (the header) and logger.cpp (the source).
// Write them so the project follows every rule from this lesson:
//
//   1. logger.h starts with  #pragma once
//   2. logger.h #includes <string> (it names std::string) — include what you use
//   3. logger.h DECLARES:   void logMessage(const std::string& msg);
//      (declaration only — a ';', no body, so the One Definition Rule is safe)
//   4. logger.cpp #includes "logger.h" FIRST, then <iostream>, then DEFINES
//      logMessage to print:  [LOG] <msg>
//   5. main() calls logMessage("server started");
//
// ✅ Expected output:  [LOG] server started

// Sketch both files below (one translation unit is fine to test it here):
#include <iostream>
#include <string>

// your logger declaration + definition here

int main() {
    // logMessage("server started");
    return 0;
}

🎉 Lesson Complete

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.

Continue this course

Frequently asked questions

Should I use #pragma once or include guards?

For learning and almost all real projects, use #pragma once — it is one line, impossible to get wrong, and supported by every modern compiler (GCC, Clang, MSVC). Classic #ifndef/#define/#endif include guards are the portable standard fallback and still appear in older or strict cross-platform code. Both solve the same problem: they stop a header being pasted into the same file twice.

What is the difference between a declaration and a definition?

A declaration tells the compiler a name exists and what its type is — like a function prototype int add(int, int);. A definition actually provides the body or storage — int add(int a, int b) { return a + b; }. Headers should mostly contain declarations; the matching .cpp file holds the definitions. The exceptions that may live in a header are things that must be visible everywhere: inline functions, templates, constexpr values, and class member function bodies written inside the class.

Why does my non-inline function in a header cause a linker error?

If a header defines a normal (non-inline) function and that header is included by two .cpp files, the linker sees two identical definitions and complains about a 'multiple definition' / 'duplicate symbol' error. This is the One Definition Rule. Fix it by either moving the body to a .cpp file (keep only the declaration in the header) or marking the function inline so the linker is allowed to merge the copies.

When can I forward-declare a class instead of including its header?

You can forward-declare (class Widget;) whenever you only need a pointer or reference to the type — Widget* or Widget&. That covers most member variables and function parameters/return types in a header. You must include the full header when you store the type by value, call its members, inherit from it, or take sizeof — because then the compiler needs to know the type's real size and layout.

What does 'include what you use' mean?

Include-what-you-use (IWYU) means every file directly includes the header for every name it uses, and does not rely on getting that name 'for free' through some other header. It makes files self-contained: if you use std::vector, include <vector> yourself rather than hoping <iostream> drags it in. This prevents fragile builds that break the moment an unrelated include is removed.

Related lessons