Le problème des inclusions multiples trouve sa solution en C comme en C++ par l'utilisation de directives d'inclusion conditionnelle, jointes aux définitions de symboles.
Ce petit texte présente, pas à pas, un exemple concret où les fichiers source du projet effectuent une inclusion multiple d'un même fichier d'en-tête, et démontre comment s'en sortent les programmeuses et les programmeurs.
![]() |
Supposons un petit projet fait de cinq fichiers, dont la relation d'inclusion est décrite par le schéma visible à gauche, où une flèche de A vers B y signifie A est inclus par B. À titre d'exemple, dans ce schéma, le fichier bits.h est inclus par octets.h et par UnProjet.cpp. |
C'est là une situation fort plausible : on n'a qu'à penser que les prototypes de octets.h auraient besoin d'une constante ou d'un type se trouvant dans bits.h, par exemple.
Puisque inclure un fichier signifie en inclure le texte en entier, alors octets.h contient le texte complet de bits.h. De même, si UnProjet.cpp inclut octets.h et bits.h, alors UnProjet.cpp inclut bits.h deux fois. Le compilateur peut ne pas aimer ça du tout (selon ce que contient bits.h).
Le texte qui suit sera notre bits.h avant l'ajout de directives d'inclusion conditionnelle :
using pos_bit_t = unsigned char;
bool lire_bit(unsigned short mot, pos_bit_t pos);
void forcer_bit(unsigned short &mot, pos_bit_t pos, bool valeur);
Selon le schéma plus haut, UnProjet.cpp se retrouverait (une fois les directives #include dans octets.h et dans UnProjet.cpp résolues) avec – entre autres – deux définitions du type pos_bit_t, ce qui est... vilain.
Par souci de clarté, supposons que le fichier octets.h ressemble à ceci :
// octets.h
#include "bits.h"
// contenu de octets.h
... et que celui de UnProjet.cpp, à son tour, ressemble à :
// UnProjet.cpp
#include "octets.h" // <--
#include "bits.h" // contenu de UnProjet.cpp
Maintenant, voici notre bits.h une fois les directives d'inclusion conditionnelle judicieusement insérées :
// bits.h
#ifndef BITS_H
#define BITS_H
using pos_bit_t = unsigned char;
bool lire_bit(unsigned short mot, pos_bit_t pos);
void forcer_bit(unsigned short &mot, pos_bit_t pos, bool valeur);
#endif // not defined BITS_H
À défaut d'être une grande différence lexicale, on vient tout juste de changer radicalement la mécanique de la compilation de fichiers incluant bits.h. Voici comment.
Le premier fichier à être inclus dans "UnProjet.cpp", lu de haut en bas, est "octets.h". Le texte de "octets.h" est donc inséré dans "UnProjet.cpp" :
// UnProjet.cpp
// octets.h
#include "bits.h" // <--
// contenu de octets.h
#include "bits.h"
// contenu de UnProjet.cpp
Ensuite, le préprocesseur devra inclure le fichier bits.h, puisque le texte de octets.h, maintenant inclus dans UnProjet.cpp, le demande.
On obtient donc :
// UnProjet.cpp
// octets.h
// bits.h
#ifndef BITS_H // <--
#define BITS_H
using pos_bit_t = unsigned char;
bool lire_bit(unsigned short mot, pos_bit_t pos);
void forcer_bit(unsigned short &mot, pos_bit_t pos, bool valeur);
#endif // not defined BITS_H
// contenu de octets.h
#include "bits.h"
// contenu de UnProjet.cpp
Poursuivant son travail, le préprocesseur rencontrera le directive #ifndef BITS_H. Puisque le symbole BITS_H n'est pas encore défini, le code qui suit (jusqu'au #endif) sera inclus dans la compilation.
La première ligne de ce code conditionnellement inclus est #define BITS_H, ce qui définit le symbole en question. Par la suite, le texte de octets.h sera traité lui aussi (nous en faisons ici abstraction par souci de simplicité).
Par la suite, le préprocesseur devra inclure à nouveau bits.h, cette fois parce que le texte de UnProjet.cpp le demande. On obtiendra ce qui suit :
// UnProjet.cpp
// octets.h
// bits.h
#ifndef BITS_H
#define BITS_H
using pos_bit_t = unsigned char;
bool lire_bit(unsigned short mot, pos_bit_t pos);
void forcer_bit(unsigned short &mot, pos_bit_t pos, bool valeur);
#endif // not defined BITS_H
// contenu de octets.h
// bits.h
#ifndef BITS_H // <--
#define BITS_H
using pos_bit_t = unsigned char;
bool lire_bit(unsigned short mot, pos_bit_t pos);
void forcer_bit(unsigned short &mot, pos_bit_t pos, bool valeur);
#endif // not defined BITS_H
// contenu de UnProjet.cpp
Poursuivant son travail, le préprocesseur rencontrera pour une deuxième fois #ifndef BITS_H, mais cette fois-ci la condition not defined BITS_H sera fausse, puisque le symbole BITS_H aura été défini précédemment, lors de la première inclusion du fichier d'en-tête bits.h.
Allez hop! Tour de magie! Le code considéré pour la compilation se limitera donc à :
// UnProjet.cpp
// octets.h
// bits.h
#ifndef BITS_H // est VRAI à ce moment-ci
#define BITS_H // BITS_H est maintenant défini!
using pos_bit_t = unsigned char;
bool lire_bit(unsigned short mot, pos_bit_t pos);
void forcer_bit(unsigned short &mot, pos_bit_t pos, bool valeur);
#endif // not defined BITS_H
// contenu de octets.h
// bits.h
#ifndef BITS_H // est FAUX à ce moment-ci
#endif // not defined BITS_H
// contenu de UnProjet.cpp
Cette manoeuvre est tellement commune que les utilitaires « sorciers » (en anglais, les wizards) de VC génèrent les directives appropriées à cette fin spontanément lors de la création de certains fichiers d'en-tête.
Exercice : étant donné les fichiers d'en-tête suivants
a.h | b.h | c.h |
---|---|---|
|
|
|
Pour chacun des fichiers source ci-après, indiquez si sa compilation se fera correctement.
f0.cpp | f1.cpp | f2.cpp |
---|---|---|
|
|
|
f3.cpp | f4.cpp | f5.cpp |
|
|
|