Nested Header Files in C
A header file, in C language, can include, in turn, other header files. This practice is called nested inclusion.
In this article we will see how to include header files in other header files in C language, and how to solve the multiple inclusion problem that can arise from such practice.
In particular, we will study the schema of inclusion guards, a mechanism that allows protecting header files from multiple inclusion.
Nested Inclusions
The header file inclusion mechanism in C language has no limits.
We can, in fact, include header files in other header files through the precompilation directive #include.
At first glance this practice may seem rather strange. However, in reality it is widely used.
Let's see an example. Suppose we want to create a program that performs geometric calculations, for example a program capable of calculating the areas of various geometric figures given the values of their dimensions as input.
We could create a source file geometry.c that contains the functions for calculating the areas of various geometric figures, and a header file geometry.h that contains the prototypes of the functions defined in geometry.c:
// geometry.h
float square_area(float side);
float regular_polygon_area(float base, float apothem, int n_sides);
float circle_area(float radius);
The source file geometry.c will contain the function definitions:
// geometry.c
#include "geometry.h"
float square_area(float side) {
return side * side;
}
float regular_polygon_area(float base, float apothem, int n_sides) {
return base * apothem * n_sides / 2;
}
float circle_area(float radius) {
return 3.14159 * radius * radius;
}
In the file geometry.c we used, to calculate the circle area, a literal value corresponding to an approximation of
We might want to define another header file whose purpose is to collect all the mathematical constants used in our program, of which the constant
// constants.h
#define PI 3.14159
In this case, we can include the file constants.h in the file geometry.h:
// geometry.h
#include "constants.h"
float square_area(float side);
float regular_polygon_area(float base, float apothem, int n_sides);
float circle_area(float radius);
In this way, the file geometry.h will contain the function prototypes and the mathematical constants used in the file geometry.c:
// geometry.c
#include "geometry.h"
float square_area(float side) {
return side * side;
}
float regular_polygon_area(float base, float apothem, int n_sides) {
return base * apothem * n_sides / 2;
}
float circle_area(float radius) {
return PI * radius * radius;
}
We might want to add a function that verifies if given the sides of a rectangle it is a square:
// geometry.h
#include "constants.h"
float square_area(float side);
float regular_polygon_area(float base, float apothem, int n_sides);
float circle_area(float radius);
int is_square(float side1, float side2);
// geometry.c
#include "geometry.h"
/* ... */
int is_square(float side1, float side2) {
return side1 == side2;
}
But, the function is_square ultimately returns only 1 or 0, that is boolean values that we can define in a new header file boolean.h:
// boolean.h
typedef int BOOL;
#define TRUE 1
#define FALSE 0
// geometry.h
#include "constants.h"
#include "boolean.h"
float square_area(float side);
float regular_polygon_area(float base, float apothem, int n_sides);
float circle_area(float radius);
BOOL is_square(float side1, float side2);
// geometry.c
#include "geometry.h"
/* ... */
BOOL is_square(float side1, float side2) {
return side1 == side2 ? TRUE : FALSE;
}
Finally, we can include the file geometry.h in the main source file of our program:
// main.c
#include <stdio.h>
#include "geometry.h"
int main() {
/* MAIN PROGRAM */
}
The final result is shown in the figure:
As can be observed we have that both source files geometry.c and main.c include the header file geometry.h, which in turn includes the header files constants.h and boolean.h.
In this way, we can define the functions and mathematical constants in separate files, keeping the code well organized and easily maintainable.
The C language places no limits on the number of nested inclusions.
Nested Header Files
A header file can include other header files, which in turn can include other header files, and so on. This practice is called nested inclusion.
Traditionally, nested inclusion was not viewed favorably by C programmers. In fact, the first versions of the language did not allow nested inclusion. However, with the advent of more modern and powerful compilers, nested inclusion has become a common and accepted practice.
The Multiple Inclusion Problem
The nested inclusion seen above exposes, however, our programs to a problem known as multiple inclusion.
To understand what this is about, let's analyze an example.
Suppose we want to create a program that performs analytic geometry calculations. This program uses points in two-dimensional space, and therefore we define a structure Point that represents a pair of coordinates in a header file point.h. Also in this header file we define a function distance that calculates the distance between two points.
// point.h
typedef struct {
float x;
float y;
} Point;
float distance(Point p1, Point p2);
The source file point.c will contain the function definitions:
// point.c
#include "point.h"
float distance(Point p1, Point p2) {
/* Implementation */
}
In another header file rectangle.h we define a structure Rectangle that represents a rectangle in the Cartesian plane, and a function area that calculates the area of the rectangle. This header file includes the header file point.h in order to use the structure Point.
// rectangle.h
#include "point.h"
typedef struct {
Point vertex1;
Point vertex2;
} Rectangle;
float area(Rectangle r);
The source file rectangle.c will contain the function definitions:
// rectangle.c
#include "rectangle.h"
float area(Rectangle r) {
/* Implementation */
}
The main source file main.c will need to use both the functions defined in point.c and in rectangle.c. Therefore, it will include both header files point.h and rectangle.h.
// main.c
#include <stdio.h>
#include "point.h"
#include "rectangle.h"
int main() {
/* MAIN PROGRAM */
}
Written in this way, the program cannot be compiled!
To understand why we need to visualize the joint structure of the various source files and included header files:
The problem is that, if we follow the links between the header files, we see that the file point.h is included twice in the file main.c. The first inclusion occurs directly, just look at the red arrow directed in the image. The second inclusion occurs indirectly, through the inclusion of the file point.h in the file rectangle.h. To realize this, just follow the second red arrow that starts from main.c, reaches rectangle.h and from there arrives at point.h.
As long as the file point.h contains only declarations of variables, functions and macros, there are no problems. The problem arises when header files contain type declarations.
In fact, if we try to compile main.c with the gcc compiler we get the following error:
$ gcc main.c point.c rectangle.c -o main
In file included from rectangle.h:1,
from main.c:5:
point.h:1:8: error: redefinition of 'struct Point'
1 | typedef struct {
| ^~~~~~
point.h:1:8: note: originally defined here
1 | typedef struct {
| ^~~~~~
In other words, the compiler tells us that the structure Point was defined twice, once in the file point.h and once in the file rectangle.h which includes point.h.
To solve the problem header files must be protected from multiple inclusion. Actually, it would not be necessary to protect all header files, but only those that contain type declarations. However, it is good practice to apply protection to all header files so that if type declarations are added at a later time one does not have to worry about adding protections.
To protect a header file it is sufficient to enclose the entire file between two precompilation directives #ifndef and #endif. These directives are called Inclusion Guards.
For example, to protect the file point.h we can write:
// point.h
#ifndef POINT_H
#define POINT_H
typedef struct {
float x;
float y;
} Point;
float distance(Point p1, Point p2);
#endif
When this file is included the first time what happens is:
- The directive
#ifndef POINT_His evaluated as true, because the macroPOINT_Hhas not been defined. - The directive
#define POINT_Hdefines the macroPOINT_H. - The file content is included.
When the file is included the second time:
- The directive
#ifndef POINT_His evaluated as false, because the macroPOINT_Hhas already been defined. - The file content is not included.
In this way, the multiple inclusion problem is solved.
The name of the macro, which in our case was POINT_H does not have particular importance. However, it is good practice to use a name that reflects the name of the header file, in order to avoid conflicts with other macros defined in other header files.
Some common macro schemas used for this purpose are:
FILENAME_HFILENAME_HEADERFILENAME_H_INCLUDED_FILENAME_H_
The important thing is to be consistent in using the macro name.
Multiple Inclusion and Inclusion Guards
Multiple inclusion is a problem that occurs when a header file is included multiple times in a source file or in other header files. This problem manifests when the header file contains type declarations, such as structures or enumerations.
The schema to use to protect a header file from multiple inclusion is to use precompilation directives called Inclusion Guards.
Inclusion guards are defined as follows:
#ifndef FILENAME_H
#define FILENAME_H
/* Header file content */
#endif
where FILENAME_H refers to the name of the header file.
In Summary
- The header file inclusion mechanism in C language has no limits. We can include header files in other header files through the precompilation directive
#include. - Nested inclusion is a common and accepted practice in C. However, it exposes our programs to the multiple inclusion problem.
- Multiple inclusion is a problem that occurs when a header file is included multiple times in a source file or in other header files. This problem manifests when the header file contains type declarations, such as structures or enumerations.
- To protect a header file from multiple inclusion, an inclusion guard schema is used, which consists in using precompilation directives
#ifndef,#defineand#endif.
Now that we know how to include header files in other header files, in the next summary lesson we will see a general schema for creating header files that has application validity in many contexts.