Ce qu'il faut comprendre, c'est que:
- un compilateur lit le code "de haut en bas" et ne connait donc, lorsqu'il arrive à une ligne donnée, que ce qu'il a déjà rencontré dans les lignes précédentes
- l'ordre de compilation des fichiers n'influe absolument pas... c'est l'édition des liens qui fera correspondre le tout
- Le compilateur "oublie" tous les symboles qu'il a pu définir pour la compilation d'un fichier dés qu'il a fini de traiter ce fichier
Le (1) explique que le code
Code:
1 2 3 4 5 6 7 8
| int main()
{
foo();
return 0;
}
void foo()
{
} |
ne compilera pas parce que, lorsque le compilateur rencontre l'invocation de foo dans la fonction principale, il ne sait pas encore que foo existe, et il ne peut pas vérifier, entre autres, si lui on passe les bons paramètres
Par contre, le code
Code:
1 2 3 4 5 6 7 8 9 10
| void foo() // définition servant aussi de déclaration
// Le compilateur crée un identifiant unique
// et le code machine et assigne l'identifiant au code
{
}
int main()
{
foo();
return 0;
} |
ou
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
void foo(); // déclaration uniquement (indique seulement au compilateur
// que la fonction existe... le compilateur crée un identifiant
// unique pour cette fonction
int main()
{
foo();
return 0;
}
void foo() // définition (implémentation) uniquement
// Le compilateur transforme le code (C++) en code machine
// et assigne l'identifiant unique au code machine obtenu
{
} |
compilera parce que, lorsque le compilateur rencontre l'invocation de foo dans la fonction principale, il a déjà rencontré la déclaration de foo (même si c'est la définition/implémentation dans le premier)
Il faut bien comprendre ici qu'un même nom, une même signature, dans un même espace de noms produira toujours... le même identifiant ;)
Le (2) et le (3) nous font prendre conscience qu'il faut disposer d'un moyen "simple et efficace" pour nous assurer que le compilateur connaisse les différents symboles auxquels il devra recourir pour compiler un nombre indéfini de fichiers...
En effet, nous pourrions envisager d'écire des fichiers sous la forme de
fichier1.cpp
Code:
1 2 3 4 5 6 7 8 9
| /* les fonctions nécessaires, mais définies dans fichier2.cpp et fichier3.cpp*/
void bar();
void foobar();
/* la fonction définie dans fichier1.cpp*/
void foo()
{
bar();
foobar();
} |
fichier2.cpp
Code:
1 2 3 4 5 6 7 8 9
| /* les fonctions nécessaires, mais définies dans fichier1.cpp et fichier3.cpp*/
void foo();
void foobar();
/* la fonction définie dans fichier1.cpp*/
void bar()
{
foo();
foobar();
} |
fichier3.cpp
Code:
1 2 3 4 5 6 7 8 9
| /* les fonctions nécessaires, mais définies dans fichier1.cpp et fichier2.cpp*/
void foo();
void bar();
/* la fonction définie dans fichier1.cpp*/
void foobar()
{
foo();
bar();
} |
Si ce n'est une récursivité "malsaine" introduite par l'exemple lui-même, nous pourrions parfaitement avoir trois fichiers prenant cette forme, et il y a même de fortes chances pour que ca compile sans problème ;)
Il "suffirait" de compiler chaque fichier séparément, et d'effectuer l'édition de liens de manière correcte pour avoir quelque chose de tout à fait juste ;) (ou peu s'en faut)
Nous pourrions donc nous en tenir, pour l'instant, à deux étapes majeurs:
- la transformation du code source en langage machine (création de fichier objet) et
- la création des liaisons entre les différents fichiers objet en vue de créer l'exécutable
Mais vous admettrez qu'il devient rapidement difficile de gérer la déclaration de chaque fonction / structure dans l'ensemble des fichiers qui en ont besoin...
C'est la raison pour laquelle nous avons la possibilité de séparer la déclaration des fonction (et du contenu d'une structure) de l'implémentation.
Cette possibilité nous est donnée par... les fichiers d'en-tête.
Mais cela implique qu'il faille ajouter une étape au travail de création de l'exécutable: l'étape du passage du préprocesseur
En effet, lorsque l'on écrit des lignes commençant par
- #include
- #ifndef
- #define
- #else
- #else if
- #if defined
- #end
- ...j'en oublie sans doute
nous donnons des instructions au... préprocesseur (qui va travailler avant même que le compilateur ne commence à transformer le code source en code machine)
La directive #include va avoir pour résultat de copier le contenu du fichier inclus à la place de la directive elle-même.
trois fichier ressemblant à
fichier1.hpp
Code:
1 2 3 4 5 6 7
| #ifndef FICHIER1_HPP
#define FICHIER1_HPP
class MyClasse
{
son contenu
};
#endif //FICHIER1_HPP |
fichier2.hpp
Code:
1 2 3 4 5
| #ifndef FICHIER2_HPP
#define FICHIER2_HPP
#include "fichier1.hpp"
/* contenu de fichier2.hpp */
#endif //FICHIER2_HPP |
truc.cpp
Code:
1 2 3 4
|
#include "fichier1.hpp"
#include "fichier2.hpp"
/* contenu de truc.cpp */ |
nous aurions, après la seule étape de gestion des inclusions, des fichier correspondant à
fichier2.hpp
Code:
1 2 3 4 5 6 7 8 9 10 11
| #ifndef FICHIER2_HPP
#define FICHIER2_HPP
#ifndef FICHIER1_HPP
#define FICHIER1_HPP
class MyClasse
{
son contenu
};
#endif //FICHIER1_HPP
/* contenu de fichier2.hpp */
#endif //FICHIER2_HPP |
et à
truc.cpp
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
#ifndef FICHIER1_HPP [1]
#define FICHIER1_HPP [2]
class MyClasse
{
son contenu
};
#endif //FICHIER1_HPP [3]
#ifndef FICHIER2_HPP [4]
#define FICHIER2_HPP [5]
#ifndef FICHIER1_HPP [6]
#define FICHIER1_HPP [7]
class MyClasse
{
son contenu
};
#endif //FICHIER1_HPP [8]
/* contenu de fichier2.hpp */
#endif //FICHIER2_HPP [9]
/* contenu de truc.cpp */ |
S'il n'y avait pas les gardes contre l'inclusion multiple ni (surtout) la gestion des directives de compilation conditionnelle, le contenu de fichier1.hpp apparaitrait deux fois et donc, la classe MaClasse a ne respecterait pas la règle de... la définition unique :aie:
C'est pourquoi, après l'étape d'inclusion, il y a une étape gestion des directives de compilation conditionnelle, et cela va fonctionner ainsi (sur le fichier truc.cpp)
- en [1], FICHIER1_HPP n'est pas connu du préprocesseur, il garde donc tout ce qu'il y a entre [1] et [3], c'est à dire que
- en [2] le préprocesseur crée le symbole FICHIER1_HPP
- Le compilateur dispose de la définition de MaClass
- [3]n'est que la fin de la structure de contrôle (ouverte en [1])
- en [4] le préprocesseur ne connait pas encore le symbole FICHIER2_HPP, il garde donc tout ce qu'il y a entre [4] et [9]
- en [5] Le préprocesseur crée le symbole FICHIER2_HPP
- en [6] le préprocesseur connait le symbole FICHIER1_HPP, et il supprime donc tout ce qu'il y a entre [6] et [8]
- [7]est supprimé (cf ci-dessus)
- la deuxième définition de MaClasse est supprimée (cf ci-dessus)
- [8]est supprimé (cf ci-dessus)
- [9]est la fin de la structure de controle (ouverte en [4])
Après cette étape nous avons donc, pour truc.cpp, un code proche de
Code:
1 2 3 4 5 6 7 8 9 10 11 12
| #ifndef FICHIER1_HPP [1]
#define FICHIER1_HPP [2]
class MyClasse
{
son contenu
};
#endif //FICHIER1_HPP [3]
#ifndef FICHIER2_HPP [4]
#define FICHIER2_HPP [5]
/* contenu de fichier2.hpp */
#endif //FICHIER2_HPP [9]
/* contenu de truc.cpp */ |
voire, après un peu de nettoyage (mais je n'en suis pas sur du tout)
Code:
1 2 3 4 5 6 7
|
class MyClasse
{
son contenu
};
/* contenu de fichier2.hpp */
/* contenu de truc.cpp */ |
Et le but recherché est atteint: tout ce qui est déclaré dans les différents fichiers d'en-tête est déclaré avant que le compilateur n'ait besoin d'y accéder et le principe du "one definition rule" est respecté ;)
Le compilateur peut donc créer à son aise les fichier objet (qui contiennent le "code machine") et l'éditeur de lien sait donc faire correspondre les invocations des différents symbole entre les fichiers...
Il faut enfin comprendre qu'un fichier d'en-tête n'a absolument pas pour objectif d'être compilé, et que si l'on parle (de manière un peu maladroite) de définition de structure ou de classe, cette définition correspond en réalité à... la déclaration du contenu de la structure (ou de la classe), mais qu'il n'y a absolument pas de réservation d'espace mémoire lorsque cette définition de classe (ou de structure) survient ;)
Enfin, je prévois l'objection du
ou du
Et je vous répond donc tout de suite que c'est normal...
Lorsque nous utilisons une fonction
inline, nous demandons au compilateur de remplacer l'appel de la fonction par... le code correspondant à cette fonction.
Il doit donc, lorsque nous invoquons cette fonction... disposer du code approprié :aie:...
Mais, la création du code machine correspondant se fait malgré tout toujours... dans le fichier d'implémentation dans lequel l'invocation a lieu ;)
De la même manière, pour les fonction
template ou les méthodes de classes (ou de structures)
template, le compilateur n'est en mesure de créer le code machine correspondant qu'une fois... qu'il sait quel type utiliser, et donc, au moment de l'invocation de la fonction (ou de la méthode).
Autrement dit: il ne peut créer le code machine correspondant à la fonction
template que... dans le fichier d'implémentation dans lequel se trouve l'invocation de cette fonction ;)
NOTA: ici encore, j'ai pris quelques raccourcis pour permettre la compréhension du principe... quelques termes sont peut etre mal choisi, quelques détails peuvent être imprécis ou, à la limite, pas tout à fait juste, mais bon... c'est comme tous les romans du genre: s'il fallait une précision complete, je pourrais en faire un livre ;)