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;
}