Header Files

Explore how header files and linkers streamline C++ programming, learning to organize and link our code effectively
This lesson is part of the course:

Intro to C++ Programming

Become a software engineer with C++. Starting from the basics, we guide you step by step along the way

Free, Unlimited Access
3D art showing a character in a bar
Ryan McCombe
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.

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 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.

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;
};

Leaving Small Definitions in the Header

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;
};

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 its Player), you need to separate declarations from definitions to avoid circular dependencies. We cover circular dependencies later in this lesson.

Workflow Tips

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.

Test your Knowledge

Declarations and Definitions

What is the convention on where to create declarations and definitions?

Specifiers in Class Function 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() {}

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 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 Files

Within 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

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 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.

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.

Preview

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:

  • Understanding and implementing Namespaces in C++ for organizing code and avoiding name conflicts.
  • Detailed exploration of Enums (Enumerations) and their role in making code more readable and maintainable.
  • Utilizing using statements effectively to simplify code and reduce verbosity, especially in the context of namespaces and enums.
  • Practical examples demonstrating the use of Enums and using statements in various programming scenarios.
  • Best practices and common pitfalls when using Namespaces, Enums, and using statements in C++ projects.

Was this lesson useful?

Next Lesson

Namespaces

Learn the essentials of using namespaces to organize your code and handle large projects with ease
3D art showing a fantasy character
Ryan McCombe
Ryan McCombe
Updated
3D art showing a progammer setting up a development environment
This lesson is part of the course:

Intro to C++ Programming

Become a software engineer with C++. Starting from the basics, we guide you step by step along the way

Free, Unlimited Access
The Preprocessor and the Linker
3D art showing a progammer setting up a development environment
This lesson is part of the course:

Intro to C++ Programming

Become a software engineer with C++. Starting from the basics, we guide you step by step along the way

Free, unlimited access

This course includes:

  • 60 Lessons
  • Over 200 Quiz Questions
  • 95% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

Namespaces

Learn the essentials of using namespaces to organize your code and handle large projects with ease
3D art showing a fantasy character
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved