Header Files
Explore how header files and linkers streamline C++ programming, learning to organize and link our code effectively
In a previous section, we saw how we could declare and define a function in two separate steps.
The declaration of a function uses its prototype. The prototype includes the function's return type, its name, and the types of its parameters.
// A function declaration
int Add(int x, int y);
The definition includes all those things, but also includes the body of the function, where its behavior is implemented:
// A function definition
int Add(int x, int y) {
return x + y;
}
Test your Knowledge
Declaration and Definition
What is the difference between a function declaration and a function definition?
We also introduced how we could do something similar with class functions. Let's imagine we have this class:
class Character {
void TakeDamage(int Damage) {
// Implementation
};
};
The above code could instead be written like this:
// Character.cpp
class Character {
void TakeDamage(int Damage);
}
void Character::TakeDamage(int Damage) {
// Implementation
}
Test your Knowledge
Defining Functions
How could we define this function?
class Maths {
int Add(int x, int y);
}
This separation may have seemed like a niche quirk when first introduced. However, it is the most common way C++ code is written. This lesson will explain why.
Multiple Source Files
In our lesson on forward declarations, we introduced the distinction between declarations and definitions.
Forward Declarations
Understand what function prototypes are, and learn how we can use them to let us order our code any way we want.
Functions need to be declared before we use them, so we added a forward declaration, with the function being defined elsewhere. In that lesson, the function was defined later in the same file:
void SomeFunction();
int main() {
SomeFunction();
}
In the previous lesson, we saw an alternative approach where the function could be defined in a different file entirely and then pasted into our main.cpp
file using the #include
directive:
// utils.cpp
int Add(int x, int y) {
return x + y;
}
// main.cpp
#include "utils.cpp"
int main() {
int result = Add(5, 3);
}
In real-world projects, we use a combination of these two approaches. We organize our project into multiple source files as we covered in the previous lesson.
However, if one source file wants to use something that is defined in another source file, we simply forward declare it rather than including the entire source file:
// utils.cpp
void PrintMessage() {
std::cout << "Hello World!";
}
// main.cpp
void PrintMessage(); // Forward declaration
int main() {
PrintMessage();
}
Note for this to work, our compiler needs to be aware that our project now includes this additional source file. If we simply create a file in our file system, the compiler is not going to know that we want this file included in our project.
Instead, we typically want to add additional files through our IDE. In Visual Studio, we do this by opening the Project menu from the top menu bar, and selecting Add New Item (or Add Existing Item if we already created the file outside of Visual Studio)
Class Declarations
This scenario applies to classes too. Below, our Character
class is referencing a Sword
class. Sword
is defined elsewhere, so we need to forward declare it:
// Forward Declaration
class Sword;
class Character {
Sword* mWeapon;
};
With classes, the situation is often a little more complex. Any functions or variables on that class that we want to use need to be forward declared, too:
class Sword {
public:
void Equip();
void Unequip();
int GetDamage();
};
class Character {
public:
void Equip(Sword* Weapon) {
mWeapon->Unequip();
mWeapon = Weapon;
mWeapon->Equip();
}
void Attack(){
int WeaponDamage{mWeapon->GetDamage()};
// ...
}
private:
Sword* mWeapon;
};
Scattering forward declarations like this around our code base any time our class is used gets very messy.
So by convention, we instead declare our class in a single file, and then #include
it wherever it is needed.
The file where we do this declaration is called a header file.
Using Header Files
By convention, we give our header files .h
or .hpp
extensions. Typically, each header file includes the declaration for only one class. So, for example, our Sword
class would be declared in a file called Sword.h
:
// Sword.h
#pragma once
class Sword {
public:
void Equip();
void Unequip();
int GetDamage();
};
We then #include
the file wherever it is needed. This applies to the source file, Sword.cpp
, where we provide definitions for all these functions:
// Sword.cpp
#include "Sword.h"
void Sword::Equip(){
// Implementation here
}
void Sword::Unequip(){
// Implementation here
}
int Sword::GetDamage(){
// Implementation here
}
We also #include
our Sword
header within the header of any other class that needs it:
// Character.h
#pragma once
#include "Sword.h"
class Character {
public:
void Equip(Sword* Weapon);
void Attack();
private:
Sword* mWeapon;
};
Why Not #include
the Source File?
You might wonder why we don't just put everything in .cpp
files and include those directly. After all, it would save us from creating separate header files. There are three important reasons why we use headers instead:
- Compilation Speed: When you include a file, the compiler needs to process all of its contents. Headers typically contain just declarations, which are much faster to process than full implementations. Including source files would dramatically slow down compilation.
- Code Organization: Headers serve as a "contract" or "interface" - they tell other developers what your class can do without showing how it does it. This makes it easier to understand and use your code.
- Technical Requirements: Sometimes you have no choice. When two classes need to reference each other (like a
Player
holding aWeapon
, and aWeapon
knowing whichPlayer
is holding it), you need to separate declarations from definitions to avoid circular dependencies. We cover circular dependencies later in this lesson.
Test your Knowledge
Declarations and Definitions
What is the convention on where to create declarations and definitions?
Linking
Let's imagine we have three files, set up like this:
// main.cpp
#include "Character.h"
int main() {
Character Player;
Player.Greet();
}
// Character.h
#pragma once
class Character {
public:
void Greet();
};
// Character.cpp
#include <iostream>
#include "Character.h"
using namespace std;
void Character::Greet() {
cout << "Hi!";
}
Hi!
This code works, but it might not be entirely obvious why. Specifically, in main.cpp
, why does Player.Greet()
work?
The implementation of this function is never provided in main.cpp
, even after the preprocessor runs.
The implementation is provided in Character.cpp
, but none of our files have an #include
directive that grabs the contents of Character.cpp
.
However, even though main.cpp
isn't aware that Character.cpp
exists, the compiler is aware. By paying attention to the compiler output, we can likely see it report exactly what it is doing:
Compiling...
main.cpp
Character.cpp
The compiler knows that there are two source files because, when we add and remove files from our project, our IDE keeps track of that. Every time we compile our project, our IDE sends that list of files to the compiler.
The result of that process is that the compiler outputs two files - the result of compiling Character.cpp
, and the result of compiling main.cpp
. These are sometimes called object files and typically use an obj
extension.
Once all of our object files are created, they're sent to the linker to be combined into a cohesive package.
Within the main object file, the Character::Greet()
function wasn't available, so the compiler just left a temporary marker there. These markers are sometimes called external symbols.
They are instructions to the linker: "Character::Greet
is probably in another object file - when you find it, link it here".
The linker scans through all our object files for these external symbols and resolves them with the correct connection.
If the definition cannot be found, the linker will throw an error. We can usually tell linker errors and compiler errors apart by their error code. Compiler errors typically are prefixed with C
, whilst linker errors typically use LNK
.
The following program creates a linker error simply by declaring a class function, but never defining it:
class Monster {
public:
void Taunt();
};
int main() {
Monster Enemy;
Enemy.Taunt();
}
From the output, we can see that main.cpp.obj
was successfully created by the compiler. However, the linker was unable to find the external symbol Monster::Taunt()
defined anywhere:
main.cpp.obj : error LNK2019: unresolved
external symbol "Monster::Taunt(void)"
referenced in function main
fatal error LNK1120: 1 unresolved externals
Incomplete Types
At the start of this lesson, we saw that we could forward declare a class with a simple class MyClass;
statement:
class Sword;
class Character {
Sword* mWeapon;
};
Our knowledge of header files doesn't change this. We can still do this if we prefer - we don't need to #include
the full header file.
The header file includes declarations for all the class variables and functions. But in this case, the compiler doesn't need to know all those details, it just needs to know that Sword
is a class
.
As such, we can reduce compilation times even further by not including header files unnecessarily.
When we use this, our class - Sword
in this case - will be an incomplete type within the file we're forward declaring it.
If we're only referencing the class in scenarios like variable types and function return types/parameters, an incomplete type is sufficient:
class Sword;
class Character {
public:
Sword* GetWeapon() {
return mWeapon;
}
void SetWeapon(Sword* Weapon) {
mWeapon = Weapon;
}
private:
Sword* mWeapon;
};
If we need to access members of that type, we need to switch back to including the header:
#include "Sword.h";
class Character {
public:
int GetDamage() {
// This won't work with an incomplete type
return mWeapon->Damage;
}
private:
Sword* mWeapon;
};
Test your Knowledge
Forward Declarations vs #include
What is the benefit of using a forward declaration rather than an #include
directive?
What is the main drawback of using a forward declaration rather than an include?
Circular Dependencies in Header Files
Earlier, we saw where we sometimes need to forward declare a function to resolve circular dependencies.
This applies to header files, too. Below, Character
requires Sword
, and Sword
requires Character
Having them #include
each other's header files would create a circular dependency, and prevent either of them from being used:
// Sword.h
#pragma once
#include "Character.h"
class Sword {
public:
int Damage;
Character* Wielder;
};
// Character.h
#pragma once
#include "Sword.h"
class Character {
public:
int GetDamage() { return Weapon->Damage; }
Sword* Weapon;
};
We can break this by removing the #include
directives and instead forward-declaring them as classes:
// Sword.h
#pragma once
class Character;
class Sword {
public:
int Damage;
Character* Wielder;
};
// Character.h
#pragma once
class Sword;
class Character {
public:
int GetDamage() { return Weapon->Damage; }
Sword* Weapon;
};
This breaks the circular dependency, but has introduced an error in our GetDamage()
function. Within Character.h
, Sword
is now an incomplete type, meaning we can't access the Damage
variable of Sword
objects. With an incomplete type, the compiler knows that Sword
is a class, but it doesn't know what functions and variables that type has.
In this case, we could solve the problem by re-adding the #include "Sword.h"
directive - we only needed to remove one of the includes to break the circular dependency.
However, that's not always an option. More generally, we can always solve problems like this by moving the function definitions that don't work with an incomplete type into a source (.cpp
) file, and include the required header there:
// Character.h
#pragma once
class Sword;
class Character {
public:
int GetDamage();
Sword* Weapon;
};
// Character.cpp
#include "Character.h"
#include "Sword.h"
int Character::GetDamage(){
return Weapon->Damage;
}
As with many things, the preference for header files vs forward declarations and incomplete types is mixed. Recommendations differ:
Avoid using forward declarations where possible. Instead, include the headers you need.
Forward declarations are preferred to including headers.
Either way, this technique is very common and we will see it a lot, particularly in graphics and game engines.
Summary
This lesson explores the use of header files and the linker. It emphasizes the standard practice of declaring classes in header files, while defining their functionalities in source files. The key learnings include:
- Review of function declarations and definitions, differentiating between the two with examples.
- Explanation of class functions, demonstrating separation of declaration in header files and definition in source files.
- Discussion on the use of header files in C++, including the conventions for
.h
or.hpp
file extensions and the#pragma once
directive. - Insights into the linking process in C++, detailing how the linker combines object files and resolves external symbols.
- Exploration of forward declarations and incomplete types, highlighting their role in reducing compilation times and managing dependencies.
Namespaces
Learn the essentials of using namespaces to organize your code and handle large projects with ease