c'est une question générale quelqu'un peut-il fournir des informations utiles à ce sujet
Pourquoi C++ a-t-il des fichiers d'en-tête et des fichiers .cpp ?
Version imprimable
c'est une question générale quelqu'un peut-il fournir des informations utiles à ce sujet
Pourquoi C++ a-t-il des fichiers d'en-tête et des fichiers .cpp ?
Je dirais qu'il y en a 3 types de fichiers : .cpp (code source), .h/ .hpp (entête) et .tpp (templates)
Mais en réalité, le compilateur s'en cogne de l'extension/ du type.
Si on fait 1 séparation c'est pour plusieurs raisons qui se rejoignent :
- on peut avoir plusieurs déclarations (notamment avec les "forward declarations"), mais 1 seule définition
- le système d'include ne fait que copier le contenu des fichiers. Donc on peut copier les déclarations mais pas les définitions (et en utilisant les "include guards")
- le compilateur a souvent besoin des définitions pour générer le "binaire". L'exemple typique ce sont les templates.
Édit : comme le dit @koala01, si on fait 1 séparation, c'est pour éviter des fichiers trop longs, et repartir au mieux le code pour 1 question de lisibilité et de maintenance.
Sinon on aura 1 amalgamation :?
Le message ci-dessus est très complet mais pour simplifier les fichiers d'en-tête servent aux déclarations et les fichiers .cpp aux définitions.
La documentation de Microsoft explique bien la différence : Déclarations et définitions (C++)
Par exemple, quand tu écris un programme tu as besoin d'un fichier d'en-tête, par exemple string.h qui déclare les fonctions sur les chaînes de caractères, mais elles sont définies ailleurs, tu n'as pas besoin de les réécrire dans un .cpp.
Salut,
Peut être voudrais tu comprendre les mécanismes qui se cache derrière tout cela...
Ce qui se passe, de base, c'est que le compilateur va analyser le fichier "source" (*.cpp) dont il doit générer le code binaire exécutable de la première instruction à la dernière. Un peu à la manière dont tu lis un bon bouquin: de la première page à la dernière.
Le fait est que, tout comme tu sais ce qui s'est passé pendant les neuf premières pages lorsque tu es à la page dix et n'a aucune idée de ce qui va se passer à la page douze, le compilateur n'a connaissance que des neuf premières instructions lorsqu'il est occupé à traiter la dixième, et n'a aucune idée de ce qui va se passer à la douzième.
Seulement, si toi, ca ne te dérange pas de lire un événement qui ne te sera expliqué que deux pages plus loin (ou plus), le compilateur, lui, il n'est vraiment pas content lorsque tu lui parle de quelque chose qu'il ne connait pas.
Si donc, tu lui parle, à la dixième instruction de quelque chose dont il ne prendra connaissance que lors de la douzième instruction, il va tout bonnement afficher sa colère et arrêter le traitement.
C'est pour cela que l'on fait une distinction claire entre la "déclaration" (de types de données, de données et de fonctions) et la "définition"(ou "l'implémentation" pour une fonction: la "déclaration" consiste à dire au compilateur que "quelque chose existe" alors que la définiton consiste à "donner le contenu" de la chose.
L'astuce étant bien sur qu'une définition permet également ... de déclarer ce qui est en cours de définition.
Ainsi, on peut se contenter de dire que quelque chose existe avec une déclaration qui prendra une forme proche de
alors que la définition de ces éléments ressemblera d'avantage à quelque chose commeCode:
1
2
3
4
5
6 //on peut déclarer un type de donnée (classe ou structure) class MaClasse; // on peut déclarer une donnée de type Type Type donnee; // on peut déclarer une fonction nommée foo, renvoyant une donnée de type Type et nécessitant des arguments Type foo(/* liste des arguments);
Voilà donc où l'on en est avec le compilateur.Code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 // on définit une classe ou une structure en indiquant de quoi elle est constituée class MaClasse{ public: // on peut déclarer des fonctions qui constituent la classe void foo(); private: // on peut déclarer les données qui la constituent Type donnee1; AutreType donnee2; }; // la définition d'une donnée consiste à indiquer sa "valeur de départ" Type donnee{/*les valeurs utilisées pour l'initialisation}; // la définition / l'implémentation d'une fonction consiste à indiquer ce qui doit être fait Type foo(/* liste de paramètres*/){ /*on peut déclarer / définir des données */ Type donneeLocale{/*valeurs utilsiées pour l'initialisation*/}; /* on peut faire appel à des fonctions existantes */ fonctionAppelee(/* paramètres*/); /* on peut utiliser des instructions directement comprises par le compilateur */ return donneeLocale; }
Le problème, maintenant, c'est qu'il ne serait vraiment pas pratique de garder toutes les déclarations et toutes les définitions dans un seul et unique fichier.
Penses, par exemple, que certains projets sont composés de plusieurs millions de lignes de code. Si toutes ces lignes de code étaient regroupées dans un seul et unique fichier, et à condition que notre ordinateur soit en mesure de gérer un fichier aussi gros, il deviendrait pour ainsi dire impossible de se retrouver dans ce fichier et d'arriver à retrouver -- selon le cas -- la déclaration ou la définition d'une fonction qui nous intéresse, avant de devoir retrouver l'endroit du code où l'on voulait effectuer une modification.
L'idée générale est donc de ne regrouper dans un fichier "source" (*.cpp) que la définition des fonctionnalités qui "vont bien ensembles". Par exemple, les fonctions membres d'une classe.
Comme il est "relativement rare" d'avoir beaucoup plus d'une grosse dizaine de fonctions, et que l'on essaie généralement de ne pas dépasser "énormément" une limite "arbitraire" comprise entre 25 et 50 lignes par fonction, cela nous permet de garder des fichiers "de taille raisonnable" (mettons de manière tout à fait arbitraire entre 300 et 2 000 lignes au total).
Le problème surviendra lorsqu'il faudra commencer à utiliser des fonctionnalités qui sont définies dans des fichiers différents, car le compilateur tient à savoir que toutes les fonctionnalités auxquelles nous avons recours existent.
Le gros défi consiste donc à ... déclarer toutes les fonctionnalités que nous allons utiliser, sans risquer d'en oublier, et surtout, sans prendre le risque de se tromper lors de leur déclaration.
C'est là que les fichiers d'en-tête (*.hpp) rentrent en jeu. Car si les fichiers source vont essentiellement contenir les définitions (de fonctions), il faut permettre au compilateur de savoir que "tout le reste" (comprend: essentiellement les types de données et les fonctions qui sont utilisées dans d'autre fichiers sources) existe.
Et, bien sur, avant de pouvoir réellement manipuler un type de donnée, il faut que le compilateur sache ... quelle en est sa composition, ne serait-ce que pour qu'il puisse déterminer l'espace mémoire qui sera nécessaire pour en représenter l'intégralité.
Nous allons donc "regrouper" les déclarations (de fonction) et définitions de type de donnée "qui vont bien ensembles" dans les fichiers d'en-tête et nous reposer sur le travail d'un outil particulier -- le préprocesseur -- pour nous assurer que toutes ces déclarations soient ajoutées "en temps utiles" -- quand il y en a besoin -- pour que le compilateur en ait connaissance.
Dans les grandes lignes, à chaque fois que le préprocesseur va rencontrer une instruction #include <un_nom_de_fichier> il va remplacer (de manière récursive) cette instruction par ... le contenu du fichier dont le nom est donné.
Cela devrait -- a priori -- permettre de satisfaire le compilateur ;)
"Toute l'astuce" consistera à ajouter "juste ce qu'il faut" de fichiers d'en-tête que pour s'assurer que toutes les fonctionnalités utilisées soient connues, tout en évitant autant que possible de rajouter ** trop ** de fonctionnalités "inutiles".
Car il faut bien comprendre que le compilateur va devoir analyser l'intégralité de tous les fichiers inclus récursivement grâce à cette instruction #include <un_nom_de_fichier>, et que cela va -- forcément -- lui demander ... un certain temps (pour ne pas dire un temps certain).
NOTA: Depuis C++20, une nouvelle fonctionnalité a été ajoutée au langage. Il s'agit des "modules".
Pour faire simple, les modules vont permettre de "pré traiter" toutes les fonctionnalités qui "vont bien ensemble" et qui sont placées dans un module particulier, en permettant même de "cacher" les fonctionnalités que l'on ne souhaite pas rendre accessibles.
Une fois qu'un module a été généré, cela devrait accélérer énormément le travail du compilateur. Mais les explications les concernant sortent du cadre de cette intervention ;)