Header Files

Explore how header files and linkers streamline C++ programming, learning to organize and link our code effectively

Ryan McCombe
Updated

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:

  1. 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.
  2. 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.
  3. Technical Requirements: Sometimes you have no choice. When two classes need to reference each other (like a Player holding a Weapon, and a Weapon knowing which Player 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.
Next Lesson
Lesson 41 of 60

Namespaces

Learn the essentials of using namespaces to organize your code and handle large projects with ease

Questions & Answers

Answers are generated by AI models and may not have been reviewed. Be mindful when running any code on your device.

Header File Syntax: <> vs ""
Why do some header files use angle brackets (<>) while others use quotes ("")?
Including Headers in CPP Files
Why do we need to include the header file in its own cpp file? The cpp file already has all the class code!
Circular Dependencies
Can I have circular dependencies if I use pointers? I noticed the lesson showed a Character with a Sword pointer, and a Sword with a Character pointer. How does that work?
Header File Locations
How does the compiler know where to find my header files? What if they are in different folders?
Understanding #pragma once
Why do we need #pragma once? What does it do exactly?
Multiple Classes Per Header
Can I declare multiple classes in one header file? When should I do this?
Separating Declarations
Should I always move function definitions to cpp files? The lesson mentioned small functions can stay in the header - how do I decide?
Or Ask your Own Question
Get an immediate answer to your specific question using our AI assistant