One of the peculiar things and hardest to understand when working with C++ is that code files are divided into two types of files.
On one hand we have:
- Header Files (
.h
) that contain the declarations (what classes, functions, or constants exist) - Implementation Files (
.cpp
) that contain the definitions (how those classes and functions work)
This can be shocking (besides being a hassle), since most modern languages combine declarations and definitions in a single file.
However, when computers were much less powerful, this division helped to reduce compilation times ⌛, especially in large projects (nowadays it doesn’t make much sense anymore).
This is a direct inheritance from C, where this separation of code into headers (.h
) and sources (.c
) was already done.
Although they are not always strictly necessary, .h
and .cpp
files are still a daily occurrence in any C++ project (let’s face it, you will have to deal with them one way or another).
Modules are a more modern alternative for managing projects.
Introduction to .h and .cpp Files
As mentioned, generally in a C++ project we will have .h
and .cpp
files.
The .h is like the contract or promise: “I tell you what is available to use.” The .cpp is the implementation: “I tell you how it works.”
Let’s take a closer look at them 👇
The header files .h
(header) contain the definitions of the functions and classes that will be used in a program.
- Class Declarations: Contains the definition of classes, including data members and methods.
- Function Declarations: Defines the functions that are implemented in the
.cpp
files. - Macros and Constants: Declares constants, macros, and other global elements.
The implementation files, with the .cpp
extension, contain the actual implementation of the code. This is where the definitions of the functions and methods declared in the .h
files are written.
- Method Definitions: Implements the methods and functions that have been declared in the
.h
files. - Logic Code: Contains the code that performs the specific logic of the program.
Typical Project Structure
In a traditional C++ project, a convention is followed to separate declarations and definitions into different files.
Logically, everyone organizes their project as they wish (especially the large ones). But, in a simple example, typically a project would look like this.
/project
|-- src/
|-- main.cpp
|-- my_class.cpp
|-- include/
|-- my_class.h
That is, we would have the different elements separated into folders.
src/
: We would put the implementation files (.cpp
).include/
: We would have the header files (.h
).
File Inclusion
The #include
directive is used to include the content of one file in another file.
#include "my_class.h"
In the .cpp
files, we will include the necessary header files to access the class and function declarations.
Code Example
Let’s see everything together in an example. Suppose we have a class called MyClass
.
On one side we would have the header file (my_class.h
).
#ifndef MY_CLASS_H
#define MY_CLASS_H
class MyClass {
public:
MyClass(); // Constructor
void myMethod(); // Public method
private:
int myVariable; // Private variable
};
#endif // MY_CLASS_H
On the other side we would have the implementation file my_class.cpp
.
#include "my_class.h"
#include <iostream>
MyClass::MyClass() : myVariable(0) {
// Constructor: Initializes myVariable to 0
}
void MyClass::myMethod() {
std::cout << "My variable is: " << myVariable << std::endl;
}
Here we have the definition (the code) of the functions that we had declared in the .h
.
Finally, this is how we would use it in our main file main.cpp
.
#include "my_class.h"
int main() {
MyClass object;
object.myMethod();
return 0;
}
Here we would include the file my_class.h
. With that, the main has available the declarations *(in this case of MyClass).
Subsequently, the compiler will put everything together, adding the implementations from the .cpp
file.
Safeguards in C++
A safeguard is a mechanism used to ensure that a header file is not included more than once in a project.
Basically, they are #ifndef
, #define
, #endif
directives that are inserted in the .h
header files, which make the compiler include the code only once. Thus,
#ifndef MY_CLASS_H
#define MY_CLASS_H
// Header file content
#endif // MY_CLASS_H
Here,
#ifndef
: Checks if the macro is not defined.#define
: Defines the macro if it is not already defined.#endif
: Ends the inclusion guard.
Without safeguards, the same blocks of code could be included multiple times in a single source file, which would give us an error when compiling.
Pragma Once Directive
Instead of using traditional inclusion guards, a more modern way is to use a directive #pragma once
.
This directive is more concise and easier to read, and also ensures that a file is included only once during compilation (but without the need to manually define macros).
#pragma once
class MyClass {
public:
void myMethod();
};
As we can see, #pragma once
is much more convenient, as there is no need to wrap all our code in safeguard directives. We simply put a single line at the beginning of the file.
To use it, we need to ensure that it is supported by the compiler. It is compatible with most modern compilers (like GCC, Clang, and MSVC).