Header Files

As programs grow larger and larger in number of files and number of lines, it becomes increasingly tedious to have to forward declare every function you want to use that is defined in a different file. Would not be nice if you could put all your forward declarations in one place and then import them when you need them?

C++ code files (with a .cpp extension) are not the only files commonly seen in C++ programs. The other type of file is called a header file. Header files usually have .h extension, but you will occasionally see them a .hpp extension or no extension at all. The primary purpose of a header file is to propagate declarations to code (.cpp) files.

Understanding Headers

A header file is a text file containing declarations that are meant to be included in other files. These declarations serve as an interface, providing information about the functions, classes, and constants that a program or module offers without exposing the implementation details. Headers enable code modularization, aiding in the creation of maintainable and scalable software.

Using header files to propagate forward declarations

add.cpp:

int add(int x, int y)
{
	return x + y;
}

main.cpp

#include <iostream>

int add(int x, int y); // forward declaration using function prototype

int main(){
	std::cout<< "The sum of 7 and 1 is " << add(7, 1) << "\n";
	return 0;
}

In this example, we used a forward declaration so that that the compiler will know what identifier add is when compiling main.cpp. As previously mentioned, manually adding forward declarations for every function you want to use that lives in another file can get tedious quickly.

Let's write a header file to relieve us of this burden. A header file only consist of two parts:

  1. A header guard, will discuss it later.
  2. The actual content of the header file, which should be the forward declarations for all of the identifiers we want our files to be able to see.

Here's our completed header file:

add.h

// 1) We really should have a header guard here, but will omit it for simplicity (we'll cover header guards in the next lesson)

// 2) This is the content of the .h file, which is where the declarations go
int add(int x, int y); // function prototype for add.h -- don't forget the semicolon!

In order to use this header file in main.cpp, we have to #include it (using quotes, not angle brackets).

main.cpp

#include "add.h" // Insert contents of add.h at this point.  Note use of double quotes here.
#include <iostream>

int main()
{
    std::cout << "The sum of 3 and 4 is " << add(3, 4) << '\n';
    return 0;
}

add.cpp

#include "add.h" // Insert contents of add.h at this point.  Note use of double quotes here.

int add(int x, int y)
{
    return x + y;
}

When the preprocessor processes the #include “add.h” line, it copies the contents of add.h into the current file at that point. Because our add.h contains a forward declaration for function add(), that forward declaration will be copied into main.cpp. The end result is a program that is functionally the same as the one where we manually added the forward declaration at the top of main.cpp.

IncludeHeader

Anatomy of a Header File

A typical header file consists of several key elements:

1. Include Guards:

To prevent multiple inclusions and potential compilation errors, header files often include include guards. These guards ensure that the contents of the header are only included once in a translation unit.

#ifndef MY_HEADER_H
#define MY_HEADER_H

// Header contents go here

#endif // MY_HEADER_H

2. Function and Class Declarations:

Header files declare functions, classes, and other entities without providing their implementation details. This allows other source files to use these declarations without knowing the internal workings.

// Example function declaration
int add(int a, int b);

// Example class declaration
class MyClass {
public:
    void doSomething();
};

3. Constants and Enumerations:

Headers often contain declarations for constants and enumerations, providing a convenient way to define and share values across multiple files.

// Example constant declaration
const double PI = 3.14159;

// Example enumeration
enum Days { Sunday, Monday, Tuesday, /* ... */ };

4. Inline Functions:

Small, frequently used functions can be defined inline within the header file. This helps inlining the function code at the call site, potentially improving performance.

// Example inline function
inline int square(int x) {
    return x * x;
}

How including definitions in a header file results in a violation of the one-definition rule

For now, you should avoid putting function or variable definitions in header files. Doing so will generally result in a violation of the one definition rule (ODR) in cases where the header file is included into more than one source file.

Let's visualize this:

add.h

// We really should have a header guard here, but will omit it for simplicity

// definition for add() in header file -- don't do this!
int add(int x, int y)
{
    return x + y;
}

main.cpp

#include "add.h" // Contents of add.h copied here
#include <iostream>

int main()
{
    std::cout << "The sum of 3 and 4 is " << add(3, 4) << '\n';

    return 0;
}

add.cpp

#include "add.h" // Contents of add.h copied here

when main.cpp is compiled, the #include “add.h” will be replaced with the contents of add.h and then compiled. Therefore, the compiler will compile something that looks like this:

main.cpp (after preprocessing):

int add(int x, int y)
{
    return x + y;
}
include <iostream>

int main()
{
    std::cout << "The sum of 3 and 4 is " << add(3, 4) << '\n';

    return 0;
}

This will compile just fine.

When the compiler compiles add.cpp, the #include “add.h” will be replaced with the contents of add.h and then compiled. Therefore, the compiler will compiles something like this:

add.cpp (after preprocessing):

int add(int x, int y)
{
    return x + y;
}

This will also compile just fine.

Finally, the linker will run. And the linker will see that there are now two definitions for function add(): one in main.cpp, and one in add.cpp. This is violation of ODR part 2, which states, “Within a given program, a variable or normal function can only have one definition.”

Do not put function and variable definitions in your header file (for now).

Defining either of these in a header file will likely result in a violation of the one-definition (ODR) if that header is then #included into more than one source (.cpp) file.

Header Guards (Include Guard)

Header guards helps in overcoming the problem of multiple inclusion of header file.

  • Header guards serve a singular, crucial purpose: to prevent the same header file from being included more than once within a translation unit. They create a shield around the contents of a header, ensuring that if the file has already been included, subsequent attempts are gracefully thwarted.

Header guards are conditional compilation directives that take the following form:

#ifndef MY_HEADER_H
#define MY_HEADER_H

// Header contents go here

#endif // MY_HEADER_H

Let's dissect this guard:

  • ifndef (If Not Defined): Checks if a macro (in this case, MY_HEADER_H) has not been defined.
  • #define (Define Macro): If the macro is not defined, this line defines it, indicating that the header is not included.
  • Header Contents: The actual declarations and content of the header reside within this protected zone.
  • #endif (End If): Marks the end of the protected zone. If the macro was defined, subsequent attempts to include the header will skip its contents.
  • Comments with Macro Name: While not mandatory, including a comment with the macro name aids in clarity and debugging.

example:

// File: main.cpp

#include "my_header.h"
#include "my_header.h"  // Without header guards, this could cause issues

int main() {
    // Program logic
    return 0;
}

Without the protective embrace of header guards, the second inclusion of “my_header.h” would lead to a compilation error due to multiple declarations. The header guards, however, ensure that the contents are processed only once:

// File: my_header.h

#ifndef MY_HEADER_H
#define MY_HEADER_H

// Header contents go here

#endif // MY_HEADER_H

Angled brackets vs double quotes

Angled Brackets (<>)

Angled brackets are primarily associated with including system or standard library headers. When you use #include <header>, the compiler searches for the specified header file in the standard system directories.

Example:

#include <iostream>
int main() {
    std::cout << "Hello, World!" << std::endl;
    return 0;
}

In this example, <iostream> is a standard C++ header that contains declarations needed for input and output operations. The use of angled brackets indicates that the compiler should look for this header in the system's standard directories.

Double Quotes ("")

Double quotes are typically used for including user-defined or project specific header files. When you use #include “header”, the compiler searches for the specified header file in the current directory first and then looks in the standard system directories if not found locally.

Example:

#include "myheader.h"
int main() {
    // Your code here
    return 0;
}

Here, “myheader.h” is a header file specific to your project. The use of double quotes indicates that the compiler should look for this header in the current directory and then in the standard system directories if not found locally.

Key Differences:

  • Angled brackets searches in standard system directories first and are mostly used for system or standard library headers.
  • Double quotes search in the current directory first and then in the standard system directories and are used for user-defined or project-specific headers.

Best Practice:

1. Order of Inclusion:

  • Establish a consistent order for including header files in your source files. Typically, include system header first, followed by third-party library headers, and finally your project's headers.
// Example of inclusion order
#include <iostream>
#include "my_library.h"
#include "my_module.h"

2. Use Forward Declarations:

  • Prefer forward declarations over including the entire definition when possible. This reduces compilation dependencies and can improve build times.
  • If a class is only used for reference or pointer in a header file, consider using a forward declaration instead of including the full definition.
// Forward declaration
class MyClass;

3. Avoid Using Namespace Directives:

  • Avoid using using namespace directive in header files to prevent namespace pollution. Instead, use the full namespace when necessary or introduces specific items with using statements inside appropriate scopes.