Managing header files
To translate this course in your native langage:
In Chrome: right-click anywhere on the page and select “Translate to…”
Or use this language selector:
Les mots colorés en anglais représentent des mots-clés de la norme du langage C++. Ils sont conservés à l’identique lors de l’utilisation d’une traduction automatique avec Google Translate. Exemple : l’héritage (inheritance) est un principe fondamental de C++.
Rappel
La génération d’un programme (build) consiste à transformer des codes sources en programme exécutable pour une machine donnée. Ce processus repose sur deux actions :
La compilation transforme chaque fichier source (.cpp) en un fichier objet.
La liaison (link) fusionne les différents fichiers objets pour construire le programme exécutable final.
La compilation séparée (sperarate compilation) désigne le fait de compiler chaque fichier source (.cpp) séparément. Ainsi, lors de la compilation d’un fichier source, le compilateur n’a aucune connaissance du contenu présent dans les autres fichiers sources. Cette approche présente plusieurs avantages :
Une meilleure gestion des projets : chaque fichier est dédié à une classe / thématique particulière.
Réduction du temps de compilation par parallélisation.
Améliorer la maintenance et le débogage du code.
Note
Une compilation est déclenchée pour chaque fichier source et non pour les fichiers d’entête.
Organisation
Lorsque le compilateur lit une directive #include, il substitue cette ligne par le contenu du fichier d’en-tête. Le compilateur traite ainsi ces lignes comme si elles appartenaient au fichier source. Si ces nouvelles lignes incluent à leur tour d’autres directives #include alors le mécanisme de substitution se poursuit.
Exporter des fonctions
Dans le fichier de code fnt.cpp, on va trouver l’inclusion du fichier d’en-tête et la définition de la fonction :
// fnt.cpp
#include "fnt.h" // header inclusion
int fnt(...) { ... } // definition
Dans le fichier d’en-tête, on trouve la déclaration de la fonction :
// fnt.h
#pragma once
int fnt(...); // declaration
Ainsi, pour pouvoire utiliser cette fonction dans un autre fichier source, il suffira d’inclure son header.
Note
La directive #pragma once sert à éviter que ce fichier d’en-tête soit inclus une deuxième fois par le jeu des inclusions multiples.
Exporter une classe
Dans le fichier d’en-tête, on trouve la définition de la classe T et la déclaration de ses membres :
// file : T.h
#pragma once
class T
{
int data;
int fnt(...);
};
Dans le fichier source, on trouve la définition de chaque fonction membre :
// T.cpp
#include "T.h"
int T::fnt(...) { .... }
L’opérateur :: permet d’indiquer que fnt est une fonction membre de la classe T.
Exporter une fonction template
Il est d’usage de placer toute la définition d’une fonction template dans le fichier d’en-tête :
// template.h
#pragma once
template<typename T>
T max(T a, T b)
{
if (b > a) return b; else return a;
}
En conclusion
Chaque fichier source doit inclure son fichier d’en-tête.
Chaque fichier d’en-tête doit inclure la directive #pragma once
Lorsqu’un fichier source ou d’en-tête a besoin d’une fonction ou d’une classe, il doit inclure le fichier d’en-tête associé.
Normalement, aucun fichier .h ou .cpp n’inclut un fichier source .cpp.
Les problèmes
La directive #pragma once désactive toute tentative de réinclusion. Cela permet d’optimiser le temps de compilation mais pas uniquement. Nous présentons le problème des inclusions multiples ci-dessous :
Inclusion multiple
Supposons que nous ayons un fichier d’en-tête V2.h inclus dans deux autres fichiers d’en-tête collision.h et path.h. Si un fichier source eleve.cpp, inclut collision.h et path.h, le contenu de V2.h va être finalement inséré deux fois dans eleve.cpp.
Que risque-t-on ?
Pour les fonctions, le fait d’avoir plusieurs déclarations dans le même fichier source ne pose aucun problème.
Pour les structures et les fonctions templates, le compilateur va émettre une erreur de redéfinition.
Heureusement pour nous, la directive #pragma once règle ce problème car elle va bloquer la réinclusion d’un même fichier d’en-tête et donc éviter les problèmes de redéfinition.
Avertissement
Il faut écrire les directives #pragma once sur la première ligne de chaque fichier d’en-tête.
Dépendance cyclique
Ce scénario arrive lorque
Le fichier d’en-tête A.h inclut le fichier d’en-tête B.h
Le fichier d’en-tête B.h inclut le fichier d’en-tête A.h
En compilant le fichier source A.cpp, le compilateur va naturellement trouver l’inclusion du fichier d’en-tête A.h qu’il va copier-coller à l’intérieur du code source. Ensuite, en parcourant les nouvelles lignes issues du fichier d’en-tête A.h, le compilateur va trouver l’inclusion du fichier d’en-tête B.h et copier-coller son contenu dans A.cpp. Mais le contenu du fichier d’en-tête de B.h incluant le contenu de A.h, on ne va jamais sortir du cycle ce qui va finir par lever erreur de compilation.
Avertissement
Cette situation est rare mais vous pourrez la rencontrer. Il n’y a pas de miracle cette fois, aucune directive magique à l’horizon. Il faudra revoir la conception de votre programme et l’organisation de vos fichiers pour éviter cette situation.
Définir une fonction dans un fichier d’en-tête
Nous l’avons déjà dit et il faut absolument l’éviter : définir une fonction dans un fichier d’en-tête.
Supposons que par inadvertance, nous ayons malencontreusement défini une fonction test() dans le fichier fichier d’en-tête test.h. Lors de la compilation, voici ce qu’il se passe :
Dans le fichier source A.cpp, cette fonction est définie une seule fois et donne naissance à la fonction void test(void) dans le fichier A.o.
Idem dans le fichier B.o.
Seulement, au moment de la liaison, le linker se retrouve avec deux fonctions void test(void), une dans A.o et l’autre dans B.o avec exactement la même définition et il émet une erreur de double définition.
Comme les pros
Voici un schéma qui présente une chaîne de compilation complète. Chaque fichier source (.cpp) déclenche une compilation. On remarque que ces fichiers source incluent différents fichiers en-tête, mais toujours sans créer de cycle. L’étape de compilation construit des fichiers .o qui correspondent à des morceaux du programme final. Ils seront ensuite liés pour construire un exécutable.
Tout projet professionnel inclut des librairies annexes : 3D, IHM, BDD,… Ces librairies ne publient pas leurs codes sources, ceci pour diverses raisons, notamment pour préserver leur propriété intellectuelle. Ainsi, chaque librairie fournit :
Des fichiers en-tête contenant les fonctions/classes exportées par la librairie.
Des fichiers .lib équivalents aux fichier .o.
Le linker dispose ainsi de toutes les informations nécessaires pour construire l’exécutable final :
Quizzz
L’étape de liaison se déroule avant l’étape de compilation.
La compilation séparée permet de compiler les fichiers source indépendamment.
L’étape de compilation consiste à compiler chaque fichier source.
On doit définir les fonctions dans les fichiers entête.
Le compilateur sait résoudre les dépendances cycliques.
La directive #pragma once se rencontre dans les fichiers d’entête.
Si l’on inclut plusieurs fois le même fichier d’en-tête dans un même fichier source, cela déclenche une erreur de liaison.
Une librairie externe, si elle ne fournit pas de fichiers source, propose un fichier précompilé .lib à la place ?
Il est d’usage qu’un fichier source inclut son propre fichier d’entête.