Header files solve a very specific low-level problem: compiler clashes (redefinition errors) and APIs. Once you understand why compilers clash when generating binaries, the need for headers explains itself.
The Problem
Here’s a toy example (not realistic and an anti pattern), but close enough to what happens in large projects.
Suppose you have two library files: a.cpp
and b.cpp
.
a.cpp
float sum(float a, float b) {
return a + b;
}
b.cpp
#include "a.cpp"
float lerp(float a, float b, float t) {
return a * sum((1.0f - t), b * t);
}
Now b.cpp
depends on a.cpp
. That’s fine.
In main.cpp
you can use them like this:
#include "b.cpp"
#include <iostream>
int main() {
float a = 1.0f, b = 3.0f, t = 0.5f;
float result = lerp(a, b, t);
std::cout << result << std::endl;
}
Compile:
g++ .\main.cpp -o h.exe
Run:
.\h.exe
All good so far.
The Clash
But what if you also want to call sum
directly in main.cpp
?
#include "b.cpp"
#include "a.cpp"
#include <iostream>
int main() {
float a = 1.0f, b = 3.0f, t = 0.5f;
float result = lerp(a, b, t);
std::cout << result << std::endl;
std::cout << sum(5, 5) << std::endl;
}
Try to compile this,
g++ .\main.cpp -o h.exe
error:
error: redefinition of 'float sum(float, float)'
Why? Because sum
got compiled twice: once inside b.cpp
, and again in main.cpp
. Unlike high-level languages, C\C++ compilers don't allow this.
This is exactly one of the problems header files solve.
Header Files
There are two flavors of header files:
Modern
#pragma once
Tells the compiler: include this file only once in this translation unit(TU) which is roughly: one .cpp file and everything it includes, no matter how many times it’s imported.
Old School
#ifndef A_H
#define A_H
// declarations
#endif
Both do the same thing.
The key idea: a header file is a promise. It says, “this API exists, don’t worry about the implementation right now.”
Fixing Our Example
Create a.h
:
#pragma once
float sum(float a, float b);
Update a.cpp
to include the header:
#include "a.h"
float sum(float a, float b) {
return a + b;
}
Now in b.cpp
:
#include "a.h" // instead of a.cpp
And in main.cpp
:
#include "a.h" // instead of a.cpp
#include "b.cpp"
#include <iostream>
Compile with:
g++ .\main.cpp .\a.cpp -o h.exe
Now a.cpp
is compiled once, and everything works.
Header-Only Libraries
Headers don’t have to just declare APIs, they can also contain full implementations. That’s why “header-only” libraries exist.
Example a.h
:
#pragma once
float sum(float a, float b) {
return a + b;
}
Since #pragma once
prevents duplicate inclusion in a TU, this works fine too.
When should you do this? It’s up to you. For safety, most people keep headers as pure API declarations and put real implementations in .cpp
files. That way headers act as a shadow, not the actual code.
Wrap Up
Headers:
- Prevent multiple-definition clashes.
- Define APIs that promise “this exists, trust me.”
- Separate declarations from implementations (good practice).
- Can even hold full implementations in header-only libs.
Once you see it, it clicks: header files are the glue that keeps large C++ projects sane.
Top comments (0)