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;
}
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
}
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.
In our lesson on forward declarations, we introduced the distinction between declarations and definitions.
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)
This scenario applies to classes too. Below, our Character
class is referencing a Weapon
class, which 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 is called a header file.
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;
};
It’s not necessary to go "all in" on the separation of declarations and definitions. It’s common for the definition of large functions to be moved out of the header, but smaller functions are sometimes left in place.
This is typical of functions that can be defined within 1-2 lines of code, such as simple getters:
// Sword.h
#pragma once
class Sword {
public:
void Equip();
void Unequip();
int GetDamage(){ return Damage };
private:
int Damage{10;
};
#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:
It’s fairly uncommon for programming languages to split code across header files and source files in this way.
Those coming from other programming languages are likely to be resistant to the practice, but it’s worth noting that any IDE with good C++ support will eliminate a lot of the pain here. It’s worth familiarising ourselves with those features, and their keybindings.
For example, in Visual Studio, Ctrl + Click on a function name lets us navigate between definition and declaration.
We can also "peek" (Alt+F12) at the definition from the declaration, or vice versa. This allows us to open and edit both in the same tab.
Right-clicking a function name opens a menu of additional tools that make working with the declaration/definition separation much easier, such as renaming the function or changing its parameter types in both locations at once.
What is the convention on where to create declarations and definitions?
In previous lessons, we saw how we could add specifiers like virtual
and override
to our class methods.
When splitting the declaration and definition of functions into separate steps, these specifiers only need to be used with the declaration.
The definition doesn’t need to specify override
, virtual
or similar. It also doesn’t need to specify where the function is public
, private
, or protected
. Those details are just within the declaration.
Using these specifiers outside of a class definition will result in a compilation error.
// Character.h
#pragma once
class Character : public Actor {
public:
virtual void FunctionA();
private:
void FunctionB() override;
};
// Character.cpp
#include "Character.h"
// No specifiers are used in the definition
void Character::FunctionA() {}
void Character::FunctionB() {}
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 is aware 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 to the compiler.
vcxproj
FilesWithin Visual Studio, the files in our project are tracked by vcxproj
files within our project directory.
These are plain-text files in the XML format, and among other things, they keep track of the group of files to be sent to the compiler every time we build our project:
<ItemGroup>
<ClCompile Include="Character.cpp" />
<ClCompile Include="main.cpp" />
</ItemGroup>
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
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;
};
#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?
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 forward-declaring one (or both) of them instead:
// 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 change has introduced an error in our GetDamage()
function, as we can’t access the Damage
variable through an incomplete type.
We could re-add the #include
directive - we only needed to remove one of them to break the circular dependency.
However, that’s not always an option, and incomplete types are never an unsolvable problem in a header file anyway.
We can just move the definition to a source file, and import the full 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.
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:
.h
or .hpp
file extensions and the #pragma once
directive.In the next chapter, we will delve into the concepts of Namespaces, Enums, and using
statements in C++.
These lessons will explore how these features enhance code organization and readability, and prevent naming conflicts in larger projects.
We will also examine practical examples to demonstrate their application in real-world programming scenarios.
Key Topics to be Covered:
using
statements effectively to simplify code and reduce verbosity, especially in the context of namespaces and enums.using
statements in various programming scenarios.using
statements in C++ projects.Explore how header files and linkers streamline C++ programming, learning to organize and link our code effectively
Become a software engineer with C++. Starting from the basics, we guide you step by step along the way