Pourquoi avoir des fichiers d’en-tête et des fichiers .cpp?

Pourquoi C ++ a-t-il des fichiers d’en-tête et des fichiers .cpp?

Eh bien, la principale raison serait de séparer l’interface de l’implémentation. L’en-tête déclare “ce que” une classe (ou tout ce qui est implémenté) fera, tandis que le fichier cpp définit “comment” il va exécuter ces fonctionnalités.

Cela réduit les dépendances, de sorte que le code qui utilise l’en-tête n’a pas nécessairement besoin de connaître tous les détails de l’implémentation et les autres classes / en-têtes nécessaires uniquement pour cela. Cela réduira les temps de compilation et la quantité de recompilation nécessaire lorsque quelque chose dans l’implémentation change.

Ce n’est pas parfait, et vous utiliseriez généralement des techniques telles que Pimpl Idiom pour séparer correctement l’interface et l’implémentation, mais c’est un bon début.

Compilation C ++

Une compilation en C ++ se fait en 2 phases principales:

  1. Le premier est la compilation de fichiers texte “source” en fichiers “object” binarys: le fichier CPP est le fichier compilé et est compilé sans aucune connaissance des autres fichiers CPP (ou même des bibliothèques), à moins d’être transmis par déclaration brute ou inclusion d’en-tête. Le fichier CPP est généralement compilé dans un fichier .OBJ ou .O “object”.

  2. La seconde est la liaison de tous les fichiers “object”, et donc la création du fichier binary final (soit une bibliothèque ou un exécutable).

Où le HPP s’intègre-t-il dans tout ce processus?

Un pauvre fichier CPP solitaire …

La compilation de chaque fichier CPP est indépendante de tous les autres fichiers CPP, ce qui signifie que si A.CPP a besoin d’un symbole défini dans B.CPP, comme:

// A.CPP void doSomething() { doSomethingElse(); // Defined in B.CPP } // B.CPP void doSomethingElse() { // Etc. } 

Il ne comstackra pas parce que A.CPP n’a aucun moyen de savoir “doSomethingElse” existe … Sauf s’il y a une déclaration dans A.CPP, comme:

 // A.CPP void doSomethingElse() ; // From B.CPP void doSomething() { doSomethingElse() ; // Defined in B.CPP } 

Ensuite, si vous avez C.CPP qui utilise le même symbole, vous copiez / collez ensuite la déclaration …

COPIE / ALERTE DE PÂTE!

Oui, il y a un problème. Les copies / pâtes sont dangereuses et difficiles à entretenir. Ce qui signifie que ce serait cool si nous avions un moyen de ne pas copier / coller, et toujours déclarer le symbole … Comment pouvons-nous le faire? Par l’inclusion d’un fichier texte, qui est généralement suffixé par .h, .hxx, .h ++ ou, mon préféré pour les fichiers C ++, .hpp:

 // B.HPP (here, we decided to declare every symbol defined in B.CPP) void doSomethingElse() ; // A.CPP #include "B.HPP" void doSomething() { doSomethingElse() ; // Defined in B.CPP } // B.CPP #include "B.HPP" void doSomethingElse() { // Etc. } // C.CPP #include "B.HPP" void doSomethingAgain() { doSomethingElse() ; // Defined in B.CPP } 

Comment include travail?

L’inclusion d’un fichier permettra essentiellement d’parsingr puis de copier-coller son contenu dans le fichier CPP.

Par exemple, dans le code suivant, avec l’en-tête A.HPP:

 // A.HPP void someFunction(); void someOtherFunction(); 

… la source B.CPP:

 // B.CPP #include "A.HPP" void doSomething() { // Etc. } 

… deviendra après l’inclusion:

 // B.CPP void someFunction(); void someOtherFunction(); void doSomething() { // Etc. } 

Une petite chose – pourquoi inclure B.HPP dans B.CPP?

Dans le cas présent, ceci n’est pas nécessaire et B.HPP a la déclaration de fonction doSomethingElse et B.CPP a la définition de fonction doSomethingElse (qui est en soi une déclaration). Mais dans un cas plus général, où B.HPP est utilisé pour les déclarations (et le code en ligne), il ne pourrait y avoir de définition correspondante (par exemple, énumérations, structures simples, etc.), donc l’inclusion pourrait être nécessaire si B.CPP utilise ces déclarations de B.HPP. Globalement, il est “bon goût” pour une source d’inclure par défaut son en-tête.

Conclusion

Le fichier d’en-tête est donc nécessaire, car le compilateur C ++ est incapable de rechercher uniquement les déclarations de symboles et vous devez donc l’aider en incluant ces déclarations.

Un dernier mot: vous devez placer des gardes d’en-tête autour du contenu de vos fichiers HPP pour vous assurer que plusieurs inclusions ne casseront rien, mais dans l’ensemble, j’estime que la raison principale de l’existence des fichiers HPP est expliquée ci-dessus.

 #ifndef B_HPP_ #define B_HPP_ // The declarations in the B.hpp file #endif // B_HPP_ 

Parce que C, dont le concept est originaire, a 30 ans, et à l’époque, c’était le seul moyen viable de relier le code de plusieurs fichiers.

Aujourd’hui, c’est un piratage horrible qui détruit totalement le temps de compilation en C ++, provoque d’innombrables dépendances inutiles (car les définitions de classes dans un fichier d’en-tête exposent trop d’informations sur l’implémentation), etc.

Parce qu’en C ++, le code exécutable final ne contient aucune information de symbole, c’est du code machine plus ou moins pur.

Ainsi, vous avez besoin d’un moyen de décrire l’interface d’un morceau de code, distinct du code lui-même. Cette description est dans le fichier d’en-tête.

Parce que les personnes qui ont conçu le format de bibliothèque ne voulaient pas “perdre” d’espace pour des informations rarement utilisées telles que les macros préprocesseurs C et les déclarations de fonctions.

Comme vous avez besoin de cette information pour dire à votre compilateur “cette fonction est disponible plus tard lorsque l’éditeur de liens fait son travail”, ils ont dû créer un second fichier où ces informations partagées pourraient être stockées.

La plupart des langues après C / C ++ stockent ces informations dans la sortie (bytecode Java, par exemple) ou n’utilisent pas du tout un format précompilé, sont toujours dissortingbuées sous forme source et compilées à la volée (Python, Perl).

Parce que C ++ les a hérité de C. Malheureusement.

C’est la manière de préprocesseur de déclarer des interfaces. Vous placez l’interface (déclarations de méthode) dans le fichier d’en-tête et l’implémentation dans le fichier cpp. Les applications utilisant votre bibliothèque ont seulement besoin de connaître l’interface à laquelle elles peuvent accéder via #include.

Vous voudrez souvent avoir une définition d’une interface sans avoir à envoyer le code entier. Par exemple, si vous avez une bibliothèque partagée, vous devez lui envoyer un fichier d’en-tête qui définit toutes les fonctions et tous les symboles utilisés dans la bibliothèque partagée. Sans fichiers d’en-tête, vous devez envoyer la source.

Dans un projet unique, les fichiers d’en-tête sont utilisés, à mon humble avis, pour au moins deux objectives:

  • Clarté, c’est-à-dire en gardant les interfaces séparées de l’implémentation, il est plus facile de lire le code
  • Comstackr le temps En n’utilisant que l’interface, si possible, au lieu de l’implémentation complète, le temps de compilation peut être réduit car le compilateur peut simplement faire une référence à l’interface au lieu d’parsingr le code réel (qui, idéalement, n’aurait qu’à être fait une seule fois).

Répondre à la réponse de MadKeithV ,

Cela réduit les dépendances, de sorte que le code qui utilise l’en-tête n’a pas nécessairement besoin de connaître tous les détails de l’implémentation et les autres classes / en-têtes nécessaires uniquement pour cela. Cela réduira les temps de compilation et la quantité de recompilation nécessaire lorsque quelque chose dans l’implémentation change.

Une autre raison est qu’un en-tête donne un identifiant unique à chaque classe.

Donc, si nous avons quelque chose comme

 class A {..}; class B : public A {...}; class C { include A.cpp; include B.cpp; ..... }; 

Nous aurons des erreurs, quand nous essayons de construire le projet, puisque A fait partie de B, avec les en-têtes nous éviterions ce genre de casse-tête …