Header files, C preprocessor and macros (10)
Multiple files
We have never seen how to have multiple files for one program. Let's learn it !
You may already create a project named "hello_world" from this article's instructions. The project structure looks like this :
hello_world/
main.c
To compile, we do gcc main.c -o main
, considering we are in "hello_world" folder.
Well, let's compile two files into one executable. We start by creating a new file named "foo.c". Now, the project structure looks like this :
hello_world/
main.c
foo.c
main.c :
#include <stdio.h>
extern int square(int n);
int main(void) {
printf("5^2 == %i\n", square(5));
return 0;
}
foo.c :
int square(int n) {
return n * n;
}
Then, we compile them gcc main.c foo.c -o program
. Output of ./program
:
5^2 == 25
The extern
keyword means we are using a function from another C file (or from an object file, but we will see that later). It's simply followed by a function prototype (seen in this article)
Further, this is annoying because we have to tell every external function exists for the file using it. Header files solve this problem.
Header files
We can create files called "header files" ending by the ".h" extension to store all the function prototypes. Let's see our new project structure :
hello_world/
main.c
foo.c
foo.h
An header file is supposed to contain all the "public" function's prototypes of a C file. That's why "foo.h" is named like "foo.c", it's the header file for "foo.c".
foo.c :
#include "foo.h"
int square(int n) {
return n * n;
}
foo.h :
// Header file for "foo.c"
int square(int n);
Wait, we can use
include
for local files and not only for the standard library ?
Yes we can. It's because the standard library parts you include are also header files. We use the <...>
syntax for libraries' files and the "..."
syntax for the local files.
Then, we can include the "foo.h" header file in "main.c" :
#include <stdio.h>
#include "foo.h"
int main(void) {
printf("5^2 == %i\n", square(5));
return 0;
}
In this way, we don't have to use the extern
directive.
How an header file really works
The #include
directive is not lying to you. It really includes the content of the targeted file to the file including it.
This file :
#include <stdio.h>
#include "foo.h"
int main(void) {
printf("5^2 == %i\n", square(5));
return 0;
}
Becomes this for the compiler :
#include <stdio.h>
int square(int n);
int main(void) {
printf("5^2 == %i\n", square(5));
return 0;
}
C preprocessor
We already have seen #include
. It's a C preprocessor directive. They can be recognised by the "#" character at the keyword's beginning.
Define directive
This directive permits a lot of things. It replaces the define's identifier by the define's content.
This file :
#include <stdio.h>
// id | content
#define PI 3.14
int main(void) {
printf("pi == %i\n", PI);
return 0;
}
Becomes this for the compiler :
#include <stdio.h>
int main(void) {
printf("pi == %i\n", 3.14);
return 0;
}
We can use the define directive for everything, not only for constant values. Here is an example for something called a macro :
#define MAX(a, b) ((a) > (b) ? (a) : (b))
It's like a function but for very simple things that does not require to create a function.
assert(MAX(3, 5) == 5);
assert(MAX(6, 5) == 6);
Preprocessor conditions
We can create conditions following a macro existence (from a #define
) :
#ifdef MACRO
// Controlled text
#endif /* MACRO */
So, when the macro is defined, the code between the conditional directives is compiled :
#define FOO
#ifdef FOO
printf("foo is defined\n");
#endif /* FOO */
foo is defined
But, when the macro is not, the code between the directives is not compiled :
#ifdef FOO
printf("foo is defined\n");
#endif /* FOO */
The compile shall ignore it.
We can find other preprocessor directives like #if
, #else
or #elif
...
Header files and preprocessor directives
We use the conditional directives for header files to avoid redeclaration of things.
#ifndef FOO_H
#define FOO_H
int square(int n);
#endif // FOO_H
In this way, if we include a file two times :
#include "foo.h"
#include "foo.h"
int main(void) {
return 0;
}
It's like if it had been imported one time. It first becomes this for the compiler :
#ifndef FOO_H // Not declared ! Let's considere the following code :
#define FOO_H // Declares "FOO_H"
int square(int n);
#endif // Ok, done
#ifndef FOO_H // Already declared, don't considere the following code :
#define FOO_H
int square(int n);
#endif // Ok, let's continue now
int main(void) {
return 0;
}
Then, it becomes this for the compiler :
int square(int n);
int main(void) {
return 0;
}
And not :
int square(int n);
int square(int n);
int main(void) {
return 0;
}