Menu

Les fichiers d'en-tête en C : .h contre .c, gardes d'inclusion et programmes multi-fichiers

Comment répartir un programme C sur plusieurs fichiers : ce qui va dans un .h, ce qui va dans un .c, les gardes d'inclusion qui stoppent la double inclusion, la compilation de plusieurs fichiers ensemble, et le partage de variables globales avec extern.

Cette page contient des éditeurs exécutables - modifiez, exécutez et voyez la sortie instantanément.

Un seul fichier .c convient jusqu'à ce qu'il fasse 2 000 lignes. Répartir un programme sur plusieurs fichiers permet de compiler chaque partie séparément, de la réutiliser dans d'autres programmes et de la lire isolément - mais le C n'a aucun système d'import. Ce qu'il a, c'est le préprocesseur qui colle du texte, plus un éditeur de liens qui rassemble les morceaux à la fin.

Un fichier d'en-tête (.h) est le contrat partagé entre ces morceaux : il dit à chaque fichier source ce qui existe ailleurs, sans contenir l'implémentation.

Déclarations contre définitions

Toute la conception repose sur une distinction.

Une déclaration dit ceci existe quelque part et voici sa forme. Elle ne génère aucun code et peut apparaître autant de fois qu'on veut :

int add(int a, int b);        /* declaration de fonction (prototype) */
extern int error_count;       /* declaration de variable */
struct Point { int x, y; };   /* definition de type - repetable par fichier */

Une définition crée la chose. Elle doit apparaître exactement une fois dans tout le programme :

int add(int a, int b) { return a + b; }   /* definition de fonction */
int error_count = 0;                      /* definition de variable */

Les en-têtes contiennent des déclarations. Les fichiers sources contiennent des définitions. Inversez cela et l'éditeur de liens se plaint de « multiple definition of ... » - le message d'erreur qui signifie de façon fiable qu'une définition s'est égarée dans un en-tête.

Un programme en deux fichiers

Voici la plus petite division utile. Un en-tête déclarant deux fonctions :

/* math_utils.h */
#ifndef MATH_UTILS_H
#define MATH_UTILS_H

int add(int a, int b);
int max_of(int a, int b);

#endif

Le fichier source qui les implémente - notez qu'il inclut son propre en-tête :

/* math_utils.c */
#include "math_utils.h"

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

int max_of(int a, int b) {
    return (a > b) ? a : b;
}

Et le programme qui les utilise :

/* main.c */
#include <stdio.h>
#include "math_utils.h"

int main(void) {
    printf("add(3, 4)    = %d\n", add(3, 4));
    printf("max_of(3, 4) = %d\n", max_of(3, 4));
    return 0;
}

Compilez les deux fichiers sources ensemble :

gcc main.c math_utils.c -o app
./app

Deux détails à remarquer. D'abord, math_utils.c inclut son propre en-tête - ce n'est pas redondant. Cela fait vérifier au compilateur que chaque définition correspond à sa déclaration, donc si vous changez le prototype dans l'en-tête et oubliez le fichier .c, vous obtenez une erreur immédiatement plutôt qu'une discordance à l'édition de liens.

Ensuite, math_utils.h n'est pas sur la ligne de commande de gcc. Les en-têtes ne sont jamais compilés ; ils sont collés dans les fichiers .c par #include. Passer un .h au compilateur produit un fichier d'en-tête précompilé égaré et aucun code liable.

Le même programme comprimé en un fichier, pour que vous puissiez l'exécuter ici :

Les déclarations avant main sont ce que l'en-tête fournit dans la vraie version - c'est exactement pourquoi les prototypes de fonctions et les en-têtes sont la même idée à deux échelles.

Les gardes d'inclusion

#include colle du texte, et du texte collé deux fois est du texte dupliqué. C'est inoffensif pour un prototype de fonction et fatal pour une struct :

/* shapes.h SANS garde */
struct Point { int x, y; };

Si main.c inclut à la fois shapes.h et canvas.h, et que canvas.h inclut aussi shapes.h, le compilateur voit struct Point définie deux fois dans une unité de traduction et s'arrête sur « redefinition of 'struct Point' ». Dans un vrai projet, ces chaînes deviennent assez profondes pour que vous ne puissiez pas les suivre à la main.

La solution est une garde d'inclusion : une macro qui enregistre « cet en-tête a déjà été collé ».

/* shapes.h */
#ifndef SHAPES_H
#define SHAPES_H

struct Point { int x, y; };
struct Point origin_point(void);

#endif /* SHAPES_H */

La première inclusion trouve SHAPES_H non définie, donc le corps est conservé - et définit SHAPES_H au passage. Toute inclusion ultérieure dans le même fichier la trouve définie et saute directement à #endif. Le nom de la macro doit être unique dans le projet ; NOMFICHIER_H dérivé du chemin est la convention habituelle.

L'alternative en une ligne est prise en charge par tous les compilateurs courants :

/* shapes.h */
#pragma once

struct Point { int x, y; };

#pragma once ne peut pas subir de collision de nom ni être cassé par une faute de frappe dans le #endif. Son seul défaut est de ne pas être dans la norme C, donc un projet qui doit se construire sur des compilateurs inhabituels devrait préférer la forme #ifndef. Quoi qu'il en soit, chaque en-tête en reçoit une - sans exception, y compris les en-têtes dont vous pensez que rien d'autre ne les inclura.

Ce qui appartient à un en-tête

À mettre dans un .h :

  • Les prototypes de fonctions
  • Les définitions de struct, union et enum
  • Les déclarations typedef
  • Les macros destinées au partage
  • Les déclarations extern des variables globales partagées
  • Les #include dont l'en-tête lui-même a besoin pour être autonome

À garder hors d'un .h :

  • Les corps de fonctions (sauf s'ils sont délibérément static inline)
  • Les définitions de variables - int counter; dans un en-tête définit une variable distincte dans chaque fichier qui l'inclut, ou provoque une erreur d'édition de liens selon le compilateur
  • Les #include d'en-têtes dont l'en-tête n'a pas lui-même besoin - cela impose cette dépendance à tout le monde en aval

« Autonome » mérite d'être une règle : un en-tête devrait compiler quand il est inclus en premier, avant tout le reste. Si shapes.h utilise size_t, il inclut <stddef.h> lui-même plutôt que d'espérer que le fichier incluant l'a fait.

Un en-tête complet et bien formé :

/* inventory.h */
#ifndef INVENTORY_H
#define INVENTORY_H

#include <stddef.h>   /* pour size_t, utilise plus bas */

#define MAX_NAME 64

typedef struct {
    char   name[MAX_NAME];
    int    quantity;
    double price;
} Item;

/* partage dans tout le programme, defini une fois dans inventory.c */
extern int item_count;

void   inventory_add(const Item *item);
double inventory_total(void);
size_t inventory_size(void);

#endif /* INVENTORY_H */

Partager une globale avec extern

Une variable globale doit être définie dans exactement un fichier .c et déclarée partout ailleurs. extern est ce qui fait la déclaration :

/* inventory.h  - declaration, aucun stockage */
extern int item_count;
/* inventory.c  - l'unique definition */
#include "inventory.h"
int item_count = 0;
/* main.c - l'utilise, via l'en-tete */
#include <stdio.h>
#include "inventory.h"

int main(void) {
    printf("%d articles\n", item_count);
    return 0;
}

Retirez l'extern de l'en-tête et chaque fichier incluant définit son propre item_count, ce qui est au mieux une erreur d'édition de liens « multiple definition » et au pire deux compteurs indépendants.

Le besoin inverse est tout aussi courant : une variable ou une fonction auxiliaire qui doit rester privée à un fichier .c. static à portée fichier fait cela - il donne au nom une liaison interne, invisible de l'éditeur de liens et donc de tous les autres fichiers :

/* inventory.c */
static Item storage[256];                  /* prive a ce fichier */
static int  find_slot(const char *name);   /* helper prive */

Deux fichiers peuvent chacun avoir un static int counter; sans collision. C'est la version C d'un membre privé, et c'est le choix par défaut à privilégier - seul ce dont les autres fichiers ont réellement besoin va dans l'en-tête.

Compiler des programmes plus gros

Lister chaque fichier fonctionne et est lent, car chaque fichier est recompilé à chaque fois :

gcc main.c inventory.c report.c -o app

La forme qui passe à l'échelle compile chaque source en un fichier objet et les lie :

gcc -c main.c        # produit main.o
gcc -c inventory.c   # produit inventory.o
gcc -c report.c      # produit report.o
gcc main.o inventory.o report.o -o app

Maintenant, changer report.c ne demande qu'un gcc -c report.c et une nouvelle édition de liens. C'est précisément la comptabilité que make automatise :

app: main.o inventory.o report.o
	gcc main.o inventory.o report.o -o app

%.o: %.c
	gcc -Wall -Wextra -c $< -o $@

Si vos en-têtes vivent dans un sous-répertoire, -Iinclude l'ajoute au chemin de recherche des chevrons.

Les erreurs et ce qu'elles signifient

Les deux modes de défaillance se distinguent facilement une fois qu'on sait quelle étape les a produits.

« undefined reference to 'add' » - une erreur de l'éditeur de liens. La déclaration a été trouvée, pas la définition. Soit vous avez oublié de lister le fichier .c sur la ligne de commande, soit la fonction est static, soit le nom est mal orthographié à l'un des deux endroits.

« multiple definition of 'item_count' » - également une erreur d'édition de liens, l'image miroir : une définition a atterri dans un en-tête, ou dans deux fichiers sources. Déplacez-la dans un seul .c et laissez une déclaration extern dans l'en-tête.

« redefinition of 'struct Item' » - une erreur du compilateur, signifiant qu'un en-tête a été collé deux fois dans un fichier. Ajoutez la garde d'inclusion.

« implicit declaration of function 'add' » - un avertissement du compilateur (une erreur en C99 et modes ultérieurs) signifiant que le prototype n'a jamais été vu. Vous avez oublié le #include, ou l'en-tête ne la déclare pas.

Dès qu'un programme s'étend sur plusieurs fichiers, la chose suivante que l'on veut généralement, c'est faire varier ce qui est compilé selon la plateforme ou le type de build - c'est la compilation conditionnelle.

Questions fréquentes

Que met-on dans un fichier .h et que met-on dans un fichier .c ?

L'en-tête contient les déclarations - prototypes de fonctions, définitions de struct et de typedef, enum, macros, et déclarations extern des globales partagées. Le fichier .c contient les définitions - les corps de fonctions et les variables réelles. La règle : un en-tête dit ce qui existe, un fichier source dit ce que cela fait.

Qu'est-ce qu'une garde d'inclusion et pourquoi en ai-je besoin ?

#include colle du texte, donc inclure un en-tête deux fois colle son contenu deux fois - ce qui redéfinit chaque structure et chaque typedef qu'il contient et ne compile pas. Une garde d'inclusion entoure l'en-tête de #ifndef MONHEADER_H / #define MONHEADER_H / #endif, de sorte que la seconde inclusion voit la macro déjà définie et saute le corps.

Comment compiler un programme C avec plusieurs fichiers ?

Listez chaque fichier .c sur la ligne de commande : gcc main.c math_utils.c -o app. Ne mettez jamais de fichier .h là - les en-têtes sont collés par #include, pas compilés séparément. Pour des projets plus gros, compilez en fichiers objets (gcc -c main.c) et liez-les, ce qu'un Makefile automatise.

Faut-il utiliser #pragma once ou les gardes #ifndef ?

Les deux fonctionnent. #pragma once tient en une ligne et ne peut pas subir de collision de nom, et tous les compilateurs courants le prennent en charge - mais il n'est pas dans la norme C. La forme #ifndef/#define/#endif est standard et marche partout. Choisissez-en une et tenez-vous-y dans un projet ; pour une portabilité maximale, prenez la forme #ifndef.

Coddy programming languages illustration

Apprendre à coder avec Coddy

COMMENCER