Introduction to Header Files in C
Header Files or Include Files are files of the C language whose purpose is to share information among multiple source files.
In this introductory lesson we will explore their purpose, how they work and how to include them in source files.
Starting from the next lesson, instead, we will see how to write a header file.
The motivation for header files
When we divide a program into multiple source files, as we saw in the previous lesson, a series of fundamental problems arise:
- How can a source file call a function defined in another source file?
- How can a function access an external variable defined in another source file?
- How can we share the same definition of a macro or a data structure among multiple source files?
To the first problem, in the previous lesson, we found a non-optimal solution that consisted in copying the prototype of a function from one source file to another. This solution, however, is inefficient and not scalable.
To solve these problems, the C language provides a second type of source file, called header file or include file.
Unlike a normal source file, which ends with the .c extension, a header file ends with the .h extension.
The purpose of a header file is not to contain code, which, normally, is contained in a .c source file, but to contain all that information that can be shared among multiple source files, including:
- Function prototypes
- External variable declarations
- Macro definitions
- Data structure definitions
In this way, we can collect this information in a single file. This file can be accessed by multiple source files through a mechanism called inclusion and that, partially, we have already covered when we studied the functioning of the preprocessor.
Header File
A Header File or include file in C language is a source file that ends with the .h extension and that contains all that information that can be shared among multiple source files, including function prototypes, external variable declarations, macro definitions and data structure definitions.
The mechanism by which a header file can be read by a source file is called inclusion.
Content of a Header File
Normally header files are used to contain exclusively declarations. Therefore to contain function prototypes, data structure definitions, external variable declarations and macro definitions.
Header files should not contain actual code, such as for example the definition of functions.
There are, however, exceptions to this rule that we will see later.
Preprocessing directive #include
At the basis of the header file inclusion mechanism there is the preprocessing directive #include.
We have already mentioned this directive in the past, when we talked about preprocessing directives in general. Now the time has come to explore it in depth.
The #include directive comes in two forms. The first, which is the one we have used so far, includes a system or C standard library header file, such as for example <stdio.h>. This form encloses the header file name between angle brackets:
#include <file_name.h>
The second form, instead, includes a user-defined header file. This form encloses the header file name between quotes:
#include "file_name.h"
The difference between the two forms is quite subtle. In practice they are equivalent, what changes is where the compiler searches for the specified file. The rules are as follows:
-
#include <file_name.h>:The compiler searches for the header file in the system paths, such as for example
/usr/includeon Unix orC:\Program Files\Microsoft Visual Studio\VC\includeon Windows. This is because it is in such paths that the system and C standard library header files are located. -
#include "file_name.h":The compiler searches first for the header file in the same directory as the source file that contains the
#includedirective. If it doesn't find it, then it searches for the file in the system paths.
Getting the system path where gcc searches for header files
One way to get the list of directories where the gcc compiler searches for header files on Linux is to use the following command in shell:
$ echo | gcc -E -Wp,-v -
Using this command we get an output very similar to the following:
#include "..." search starts here:
#include <...> search starts here:
/usr/lib/gcc/x86_64-pc-linux-gnu/14.2.1/include
/usr/local/include
/usr/lib/gcc/x86_64-pc-linux-gnu/14.2.1/include-fixed
/usr/include
End of search list.
From the output of this example we can notice that, by default, gcc searches for header files in four main directories:
/usr/lib/gcc/x86_64-pc-linux-gnu/14.2.1/include/usr/local/include/usr/lib/gcc/x86_64-pc-linux-gnu/14.2.1/include-fixed/usr/include
Obviously, the result can change depending on the version of gcc and the Linux distribution.
The order of the directories is important, because gcc searches for header files in these directories in sequence. In the sense that, if, for example, there were two header files with the same name in two different directories, gcc would use the first one it finds.
Header Files and Compilation Process
In the previous lesson we explored the compilation process of programs written in C language.
In particular, we saw that each source file with .c extension must be compiled into an object file with .o extension. These object files are then linked together by the linker to form the final executable.
What we ask ourselves now is where do header files fit in this mechanism?
The answer is: header files do not need to be compiled.
In other words, when we compile a program composed of various .c source files and various .h header files only the .c source files need to be compiled.
The motivation lies precisely in the inclusion mechanism. In fact, since header files are included in source files, all their content is copied into the source file before compilation. Therefore, the compiler already has available all the information contained in the header files.
Header Files do not need to be Compiled
Header files in C language do not need to be compiled. This is because their content is copied into source files before compilation through the #include directive.
For example, if a program is composed of two source files file1.c and file2.c and two header files header1.h and header2.h, the compilation process is as follows:
-
file1.cis compiled intofile1.o;$ gcc -c file1.c -o file1.o -
file2.cis compiled intofile2.o;$ gcc -c file2.c -o file2.o -
file1.oandfile2.oare linked together by the linker to form the final executable;$ gcc file1.o file2.o -o executable
Modifying the header files search path
With the #include directive the compiler searches for include files in system paths or in the same directory as the source file that contains the #include directive depending on the form used.
However it is possible to modify the header files search path in two ways.
The first way consists in specifying the path of the directory where the header files should be searched by the compiler.
In the case of the gcc compiler, we can specify the header files search path with the -I option followed by the directory path. For example:
$ gcc -I/path/to/directory file.c
Note that, in this way we obtain two results:
- The first is that the path
/path/to/directoryis added to the list of system header files search paths; - The second is that header files contained in
/path/to/directorycan be included in source files with the directive#include <file_name.h>, therefore with angle brackets. This is because they become to all effects system header files.
We can add more than one header files search path by specifying the -I option multiple times:
$ gcc -I/path/to/directory1 -I/path/to/directory2 file.c
Adding a Header Files Search Path
All C compilers allow to add other header files search paths different from system ones.
When this mechanism is used, header files contained in these paths become to all effects system header files and can be included in source files with the directive #include <file_name.h>.
The syntax for the gcc compiler is as follows:
$ gcc -I/path/to/directory file.c
The second way consists in specifying directly the path of the header file to include. This path can be either absolute or relative.
Let's take an example. Suppose we have the following directory structure:
/home/user/project
|
├── include
│ └── header.h
└── src
└── main.c
In this case we have two files:
main.cwhose complete path is/home/user/project/src/main.c;header.hwhose complete path is/home/user/project/include/header.h.
We need to include header.h in main.c. We can do it in two ways:
-
Specifying the absolute path of
header.h:#include "/home/user/project/include/header.h" -
Specifying the relative path of
header.h:#include "../include/header.h"
In fact, the #include directive allows to specify both absolute and relative paths.
Moreover, paths enclosed between double quotes, ", are not treated as strings. Therefore, for example, under Windows we can write:
// Absolute inclusion under Windows
#include "C:\path\to\file.h"
Without having to worry about escape characters.
Moreover, also under Windows, most compilers allow to use, as directory separator, both the backslash \ and the slash /. Therefore, we can write:
// Absolute inclusion under Windows
#include "C:/path/to/file.h"
Including Header Files with Absolute or Relative Path
The #include directive allows to specify both absolute and relative paths to include a header file.
The syntax to include a header file with absolute path is as follows:
#include "/absolute/path/file_name.h"
The syntax to include a header file with relative path is as follows:
#include "../relative/path/file_name.h"
Avoiding absolute paths in source files
Although it is allowed, it is always better to avoid using absolute paths in #include directives inside source files.
Absolute paths make code less portable, because the source code becomes dependent on the folder structure of the system in which it was written.
What is advisable, instead, is to use relative paths, so that the source code can be moved from one system to another without having to modify the #include directives.
Third form of the include directive
Above we saw that there are two forms of the #include directive: one with angle brackets, for system header files, and one with double quotes, for user-defined header files.
Actually, there is a third form for this directive useful in some cases. This form is called include with macro.
The syntax of this form is as follows:
#include macro1 macro2 ... macroN
In this case, the macros macro1, macro2, ..., macroN are any macros of the preprocessor that are expanded and replaced. The constraint is that, after expansion, the result must be one of the two standard forms of the #include directive.
The advantage of using this third form is that we can avoid directly encoding the name of header files in source files but define them with appropriate macros. This is useful, for example, for the conditional compilation of a program.
For example, suppose we have an example function that must be implemented differently depending on the operating system. We can implement the three versions of example in three different files with associated different header files:
example_linux.candexample_linux.hfor Linux;example_windows.candexample_windows.hfor Windows;example_macos.candexample_macos.hfor macOS.
At this point, in the source file that needs to use example we can write the code that includes the correct header file depending on the operating system:
#ifdef __linux__
#define HEADER_FILE "example_linux.h"
#elif _WIN32
#define HEADER_FILE "example_windows.h"
#elif __APPLE__
#define HEADER_FILE "example_macos.h"
#endif
#include HEADER_FILE
In this way, depending on the operating system, the correct header file will be included.
Including Header Files with Macro
The #include directive allows to include a header file using preprocessor macros. The important thing is that the expansion of the macros leads to one of the two standard forms of the #include directive.
The syntax to include a header file with macro is as follows:
#include macro1 macro2 ... macroN
Conclusions
In this lesson we introduced header files or include files in C language.
These files are useful to share information among multiple source files, such as function prototypes, external variable declarations, macro definitions and data structure definitions.
We saw that header files end with the .h extension and that they can be included in source files with the preprocessing directive #include.
The #include directive comes in two forms: one with angle brackets, for system header files, and one with double quotes, for user-defined header files.
Finally, we saw that there is a third form of the #include directive that allows to include header files using preprocessor macros.
However, we have not yet seen how to write a header file. In the next lessons, we will see how to:
- Share function prototypes among multiple source files;
- Share external variable declarations among multiple source files;
- Share macro definitions among multiple source files.