Shakila Praveen Rathnayake

shakilar.com
~/ blog / C++ / cpp-source-vs-header
#C++ #Header Files #Source Files #Build System #Best Practices

Structuring C++ Projects: Headers vs Source Files

February 5, 2026 | 8 min read

If you’ve hit 2000+ lines in a single file, you’ve already felt the pain: endless scrolling, impossible debugging, and recompiling everything for tiny changes. Breaking a massive, single-file project into smaller, manageable pieces is a critical milestone. It transitions you from “writing scripts” to “building software.”

The solution is fundamental to professional C++ development: separating declarations (headers) from implementations (source files).

The Core Concept: Interface vs. Implementation

Think of it like a restaurant:

Customers (other parts of your code) only need the menu to place an order; they don’t need to know exactly how the chef chops the onions.

Quick Reference: What Goes Where?

Code ElementHeader File (.h / .hpp)Source File (.cpp)
RoleDeclaration (Interface)Definition (Implementation)
ClassesClass definitions & member prototypesMember function implementations
FunctionsFunction prototypes (void run();)Function bodies (void run() { ... })
TemplatesFull definitions (required by compiler)Usually none
Constantsconstexpr, extern constDefinitions if not inline
InlinesInline function definitionsNone
DependenciesMinimal (Forward Declarations)Full inclusions

Detailed Breakdown

1. Header Files (.h / .hpp)

Header files are designed to be shared. When you #include "file.h", the preprocessor literally copies the contents of that header into the file that included it.

What belongs here:

Example:

#pragma once

namespace math {
    // Check if a number is prime (Declaration only)
    bool is_prime(int n);
    
    // Template implementation (Must be in header)
    template <typename T>
    T square(T x) {
        return x * x;
    }
}

2. Source Files (.cpp)

Source files contain the actual executable logic. They are compiled individually into object files (.o or .obj) and then linked together.

What belongs here:

Example:

#include "math_utils.h"
#include <cmath>

namespace math {
    // Implementation of the declared function
    bool is_prime(int n) {
        if (n <= 1) return false;
        for (int i = 2; i <= std::sqrt(n); ++i) {
            if (n % i == 0) return false;
        }
        return true;
    }
}

Why This Architecture Matters

1. Faster Compilation (Incremental Builds)

This is the game-changer. If you change a .cpp file, only that file recompiles. If you change a header used by 50 files, all 50 files recompile. By moving implementation logic to .cpp files, you can modify algorithms without triggering a project-wide rebuild.

2. Solves “Multiple Definition” Errors (ODR)

C++ follows the One Definition Rule (ODR). You can declare a function void f(); many times, but you can only define void f() { ... } once.

3. Encapsulation & Information Hiding

Headers act as a public contract. Users of your class don’t need to see your messy dependencies, platform-specific #ifdefs, or helper functions. This “information hiding” keeps the codebase clean and prevents other developers from relying on internal details that might change.

4. Break Dependecy Chains

If A.h includes B.h, changing B.h recompiles everything that uses A. By using forward declarations (class B;) in A.h and only including B.h in A.cpp, you break this chain.

Practical Example: A Math Library

The Interface (math_utils.h)

#pragma once 

namespace math {
    // Declarations only - tells compiler these exist
    double calculate_mean(const double* values, int count);
    double calculate_stddev(const double* values, int count);
}

The Implementation (math_utils.cpp)

#include "math_utils.h"
#include <cmath> // Implementation detail, user doesn't need to know we use cmath

namespace math {
    double calculate_mean(const double* values, int count) {
        double sum = 0.0;
        for (int i = 0; i < count; ++i) sum += values[i];
        return sum / count;
    }

    double calculate_stddev(const double* values, int count) {
        double mean = calculate_mean(values, count);
        double variance = 0.0;
        for (int i = 0; i < count; ++i) {
            variance += std::pow(values[i] - mean, 2);
        }
        return std::sqrt(variance / count);
    }
}

The Usage (main.cpp)

#include <iostream>
#include "math_utils.h"

int main() {
    double data[] = {1.0, 2.0, 3.0, 4.0, 5.0};
    std::cout << "Mean: " << math::calculate_mean(data, 5) << "\n";
    return 0;
}

Linking Without Headers (Manual Declarations)

It is technically possible to call a function from another .cpp file without including its header. You simply declare the function prototype manually.

File main.cpp:

// No #include "math_utils.h"

// Manual declaration (must match the definition EXACTLY)
namespace math {
    double calculate_mean(const double* values, int count);
}

int main() {
    double data[] = {1.0, 2.0};
    math::calculate_mean(data, 2);
    // ... logic ...
}

Why does this work? The compiler only needs to know that calculate_mean exists and what its signature is. The linker is responsible for finding the actual code at the end. It doesn’t care if that knowledge came from a header file or a manual declaration.

Why is this dangerous? If you change the definition in math_utils.cpp (e.g., change int count to long count), the compiler won’t warn you about main.cpp having the wrong signature. You will likely get a linker error (best case) or a crash at runtime if the signatures are “close enough” (worst case). Headers provide a “single source of truth” to keep declarations consistent.

Best Practices Checklist

  1. Always use Include Guards (#pragma once).
  2. Pair headers and sources: MyClass.h goes with MyClass.cpp.
  3. Include your own header first in the .cpp file (#include "MyClass.h"). This ensures MyClass.h is self-contained and doesn’t accidentally rely on other headers being included before it.
  4. Use Forward Declarations whenever possible in headers to reduce build times.
  5. Never using namespace in a header. It forces that namespace on everyone who includes your file.
  6. Never include .cpp files.
Back to all posts