Introduction au C

Le logo du C
SommaireCompiler du C

Travailler avec plusieurs fichiers

Il est souvent utile de diviser son code en plusieurs fichiers, pour bien séparer les différentes parties de son programme.

Le souci avec le C est qu'il y a plusieurs règles pour qu'une fonction, un type ou une constante soit utilisable :

Ces contraintes donnent l'impression que c'est très compliqué voir impossible d'avoir plusieurs fichiers en C. Mais heureusement, il y a des solutions pour chacun des problèmes ci-dessus !

Inclure d'autres fichiers

Les compilateurs C sont capables de « transformer » notre code avant la compilation avec ce qu'on appelle un préprocesseur. Les instructions pour le préprocesseur commencent par un #. Celle qui va nous intéresser ici est include fichierfichier est un nom de fichier.

Cette instruction est remplacée par le contenu du fichier en question. Ça permet donc de définir une fonction dans un fichier et de l'inclure dans un autre qui pourra l'utiliser !

Ainsi, ce code fonctionne :

// Dans main.c
#include "addition.c"

int main() {
    int x = addition(10, 5);
    printf("x = %d\n", x);

    return 0;
}
// Dans addition.c
int addition(int a, int b) {
    return a + b;
}

Si on compile avec clang main.c, Clang va voir qu'il faut inclure addition.c dans main.c et transformer main.c en :

int addition(int a, int b) {
    return a + b;
}

int main() {
    int x = addition(10, 5);
    printf("x = %d\n", x);

    return 0;
}

Notez que le nom de fichier à inclure doit être entre guillemets si il est dans le même dossier que le fichier actuel, et entre chevrons (< et >) si c'est un fichier « global », un module fourni par le système d'exploitation ou par un autre projet.

Mais ça ne règle pas tous nos soucis, imaginons qu'on ai trois fichiers un.c, deux.c et trois.c et que :

Les fonctions de trois.c seraient définies deux fois dans un.c au final, et le compilateur nous râlerait dessus. Pour éviter ça, on va utiliser deux autres instructions de préprocesseur : ifndef et define.

define définit une constante de préprocesseur, une sorte de variable qui n'existe que pendant la compilation et qu'on ne peut pas utiliser pour notre programme. ifndef regarde si une constante donnée est définie. L'idée est donc de faire quelque chose comme :

// dans trois.c
#ifndef TROIS
#define TROIS 

// ici on met le code qu'il y avait déjà dans le fichier

#endif // Un ifndef fini par un endif

Tout ce qui se retrouve entre le ifndef et le endif sera ignoré si jamais TROIS est déjà défini. Sinon, ce code sera bien inclus, et on défini TROIS avec define TROIS. On est sûrs que même si on inclus plusieurs fois trois.c, son code ne sera copié que la première fois !

Les fichiers d'entête

Une pratique assez courante est d'avoir deux fichiers par « module » de notre programme :

Le fichier de définition est un fichier .h (header, « entête »). Il contient les ifndef et define du module, ainsi que les prototypes des fonctions du module, c'est-à-dire les fonctions sans leur code, juste avec un ; après les arguments. On y met souvent les définitions de types s'il y en a en plus. Voici par exemple le fichier addition.h correspondant au fichier addition.c de toute à l'heure :

#ifndef ADDITION
#define ADDITION

int addition(int a, int b);

#endif

Ensuite, on fait un include du fichier .h dans le .c correspondant, et on fait de même dans tous les autres fichiers .c ou .h qui ont aussi besoin de ce module.

L'avantage de cette technique est qu'on peut très facilement savoir ce que contient un module juste en lisant son .h, bien plus compact qu'un .c (qui contient tout le code et pas juste les définitions).

Notez aussi que définir une fonction sans son code, puis la redéfinir avec son code ne compte pas comme une redéfinition, donc le compilateur n'a pas de souci avec cette technique.

Il faudra par contre préciser tous les fichiers .c à Clang avec cette technique, car on ne les inclus plus (vu qu'on n'inclus plus que des .h). En général, clang *.c marche bien pour ça.