web-dev-qa-db-fra.com

Pourquoi devons-nous inclure le .h alors que tout fonctionne lorsque nous incluons uniquement le fichier .cpp?

Pourquoi devons-nous inclure à la fois le .h et .cpp fichiers alors que nous pouvons le faire fonctionner uniquement en incluant .cpp fichier?

Par exemple: créer un file.h contenant des déclarations, puis créant un file.cpp contenant des définitions et incluant les deux dans main.cpp.

Alternativement: création d'un file.cpp contenant la déclaration/définitions (pas de prototypes) l'incluant dans main.cpp.

Les deux fonctionnent pour moi. Je ne vois pas la différence. Peut-être qu'un aperçu du processus de compilation et de liaison peut être utile.

19
reaffer

Alors que vous pouvez inclure .cpp fichiers comme vous l'avez mentionné, c'est une mauvaise idée.

Comme vous l'avez mentionné, les déclarations appartiennent aux fichiers d'en-tête. Ceux-ci ne posent aucun problème lorsqu'ils sont inclus dans plusieurs unités de compilation car ils n'incluent pas d'implémentations. L'inclusion de plusieurs fois la définition d'une fonction ou d'un membre de classe causera normalement un problème (mais pas toujours) car l'éditeur de liens sera confus et générera une erreur.

Ce qui devrait arriver c'est que chaque .cpp le fichier comprend des définitions pour un sous-ensemble du programme, comme une classe, un groupe de fonctions organisé logiquement, des variables statiques globales (à utiliser avec modération le cas échéant), etc.

Chaque unité de compilation (.cpp file) inclut alors toutes les déclarations dont il a besoin pour compiler les définitions qu'il contient. Il garde une trace des fonctions et des classes qu'il référence mais ne contient pas, de sorte que l'éditeur de liens peut les résoudre plus tard lorsqu'il combine le code objet dans un exécutable ou une bibliothèque.

Exemple

  • Foo.h -> contient une déclaration (interface) pour la classe Foo.
  • Foo.cpp -> contient la définition (implémentation) de la classe Foo.
  • Main.cpp -> contient la méthode principale, le point d'entrée du programme. Ce code instancie un Foo et l'utilise.

Tous les deux Foo.cpp et Main.cpp doit inclure Foo.h. Foo.cpp en a besoin car il définit le code qui soutient l'interface de classe, il doit donc savoir ce qu'est cette interface. Main.cpp en a besoin car il crée un Foo et invoque son comportement, il doit donc savoir quel est ce comportement, la taille d'un Foo en mémoire et comment trouver ses fonctions, etc. mais il n'a pas besoin de l'implémentation réelle juste encore.

Le compilateur générera Foo.o de Foo.cpp qui contient tout le code de la classe Foo sous forme compilée. Il génère également Main.o qui inclut la méthode principale et les références non résolues à la classe Foo.

Vient maintenant l'éditeur de liens, qui combine les deux fichiers objets Foo.o et Main.o dans un fichier exécutable. Il voit les références non résolues de Foo dans Main.o mais voit que Foo.o contient les symboles nécessaires, donc il "relie les points" pour ainsi dire. Un appel de fonction dans Main.o est maintenant connecté à l'emplacement réel du code compilé. Au moment de l'exécution, le programme peut passer à l'emplacement correct.

Si vous aviez inclus le Foo.cpp fichier dans Main.cpp, il y aurait deux définitions de la classe Foo. L'éditeur de liens verrait cela et dirait "Je ne sais pas lequel choisir, c'est donc une erreur." L'étape de compilation réussirait, mais pas la liaison. (À moins que vous ne compiliez simplement pas Foo.cpp mais alors pourquoi est-ce dans un _ .cpp fichier?)

Enfin, l'idée de différents types de fichiers n'est pas pertinente pour un compilateur C/C++. Il compile des "fichiers texte" qui, espérons-le, contiennent du code valide pour la langue souhaitée. Parfois, il peut être en mesure de dire la langue en fonction de l'extension de fichier. Par exemple, compilez un .c fichier sans options de compilation et il supposera C, tandis qu'un .cc ou .cpp l'extension lui dirait de supposer C++. Cependant, je peux facilement dire à un compilateur de compiler un .h ou même .docx fichier en C++, et il émettra un objet (.o) s'il contient du code C++ valide au format texte brut. Ces extensions sont davantage destinées au programmeur. Si je vois Foo.h et Foo.cpp, Je suppose immédiatement que le premier contient la déclaration de la classe et le second la définition.

30
user22815

En savoir plus sur le rôle du préprocesseur C et C++ , qui est conceptuellement la première "phase" du compilateur C ou C++ (historiquement, c'était un programme distinct /lib/cpp; maintenant, pour des raisons de performances, il est intégré dans le compilateur proprement dit cc1 ou cc1plus). Lisez en particulier la documentation du pré-processeur GNU cpp . Ainsi, dans la pratique, le compilateur procède en principe au prétraitement de votre unité de compilation (ou traduction unit ) puis travaillez sur le formulaire prétraité.

Vous devrez probablement toujours inclure le fichier d'en-tête file.h s'il contient (comme dicté par les conventions et les habitudes ):

  • définitions de macro
  • définitions de types (par exemple typedef, struct, class etc, ...)
  • définitions de static inline les fonctions
  • déclarations de fonctions externes.

Notez que c'est une question de conventions (et de commodité) de les mettre dans un fichier d'en-tête.

Bien sûr, votre implémentation file.cpp besoin de tout ce qui précède, donc veut #include "file.h" en premier.

Il s'agit d'une convention (mais très courante). Vous pouvez éviter les fichiers d'en-tête et copier et coller leur contenu dans des fichiers d'implémentation (c'est-à-dire des unités de traduction). Mais vous ne voulez pas cela (sauf peut-être si votre code C ou C++ est généré automatiquement; alors vous pourriez faire en sorte que le programme générateur fasse ce copier-coller, imitant le rôle du préprocesseur).

Le fait est que le préprocesseur effectue uniquement des opérations textuelles . Vous pouvez (en principe) l'éviter entièrement par copier-coller, ou le remplacer par un autre "préprocesseur" ou générateur de code C (comme gpp ou m4 ).

Un problème supplémentaire est que les normes C (ou C++) récentes définissent plusieurs en-têtes standard. La plupart des implémentations implémentent réellement ces en-têtes standard sous forme de fichiers (spécifiques à l'implémentation) , mais je pense qu'il serait possible pour une implémentation conforme d'implémenter des inclusions standard (comme #include <stdio.h> pour C ou #include <vector> pour C++) avec quelques astuces magiques (par exemple en utilisant une base de données ou des informations à l'intérieur du compilateur).

Si vous utilisez GCC compilateurs (par exemple gcc ou g++) vous pouvez utiliser le -H flag pour être informé de chaque inclusion, et le -C -E drapeaux pour obtenir le formulaire prétraité. Bien sûr, il existe de nombreux autres indicateurs du compilateur affectant le prétraitement (par exemple -I /some/dir/ ajouter /some/dir/ pour rechercher les fichiers inclus et -D pour prédéfinir une macro de préprocesseur, etc, etc ....).

NB. Les futures versions de C++ (peut-être C++ 2 , peut-être même plus tard) pourraient avoir modules C++ .

9

En raison du modèle de construction à plusieurs unités de C++, vous avez besoin d'un moyen pour avoir du code qui apparaît dans votre programme une seule fois (définitions), et vous avez besoin d'un moyen d'avoir du code qui apparaît dans chaque unité de traduction de votre programme (déclarations).

De là naît l'idiome d'en-tête C++. C'est une convention pour une raison.

Vous pouvez vider l'intégralité de votre programme dans une seule unité de traduction, mais cela pose des problèmes de réutilisation du code, de test unitaire et de gestion des dépendances entre modules. C'est aussi juste un gros gâchis.

4

La réponse sélectionnée de Pourquoi avons-nous besoin d'écrire un fichier d'en-tête? est une explication raisonnable, mais je voulais ajouter des détails supplémentaires.

Il semble que le rationnel des fichiers d'en-tête a tendance à se perdre dans l'enseignement et la discussion du C/C++.

Le fichier d'en-tête fournit une solution à deux problèmes de développement d'applications:

  1. Séparation de l'interface et de la mise en œuvre
  2. Amélioration des temps de compilation/liaison pour les grands programmes

C/C++ peut évoluer de petits programmes à de très gros programmes de plusieurs millions de lignes et plusieurs milliers de fichiers. Le développement d'applications peut évoluer d'équipes d'un développeur à des centaines de développeurs.

Vous pouvez porter plusieurs chapeaux en tant que développeur. En particulier, vous pouvez être l'utilisateur d'une interface pour des fonctions et des classes, ou vous pouvez être l'auteur d'une interface de fonctions et de classes.

Lorsque vous utilisez une fonction, vous devez connaître l'interface de la fonction, quels paramètres utiliser, ce que les fonctions renvoient et vous devez savoir ce que fait la fonction. Ceci est facilement documenté dans le fichier d'en-tête sans jamais regarder l'implémentation. Quand avez-vous lu l'implémentation de printf? Achetez nous l'utilisons tous les jours.

Lorsque vous êtes le développeur d'une interface, le chapeau change de direction. Le fichier d'en-tête fournit la déclaration de l'interface publique. Le fichier d'en-tête définit ce dont une autre implémentation a besoin pour utiliser cette interface. Les informations internes et privées à cette nouvelle interface ne sont pas (et ne doivent pas) être déclarées dans le fichier d'en-tête. Le fichier d'en-tête public devrait être tout ce dont tout le monde a besoin pour utiliser le module.

Pour un développement à grande échelle, la compilation et la liaison peuvent prendre beaucoup de temps. De plusieurs minutes à plusieurs heures (voire à plusieurs jours!). La division du logiciel en interfaces (en-têtes) et implémentations (sources) fournit une méthode pour compiler uniquement les fichiers qui doivent être compilés plutôt que de tout reconstruire.

De plus, les fichiers d'en-tête permettent à un développeur de fournir une bibliothèque (déjà compilée) ainsi qu'un fichier d'en-tête. Les autres utilisateurs de la bibliothèque peuvent ne jamais voir l'implémentation appropriée, mais peuvent toujours utiliser la bibliothèque avec le fichier d'en-tête. Vous faites cela tous les jours avec la bibliothèque standard C/C++.

Même si vous développez une petite application, l'utilisation de techniques de développement logiciel à grande échelle est une bonne habitude. Mais nous devons également nous rappeler pourquoi nous utilisons ces habitudes.

0
Bill Door

Vous pourriez tout aussi bien demander pourquoi ne pas simplement mettre tout votre code dans un seul fichier.

La réponse la plus simple est la maintenance du code.

Il y a des moments où il est raisonnable de créer une classe:

  • tous alignés dans un en-tête
  • le tout dans une unité de compilation et pas d'en-tête du tout.

Le moment où il est raisonnable de s'aligner totalement dans un en-tête est lorsque la classe est vraiment une structure de données avec quelques getters et setters de base et peut-être un constructeur qui prend des valeurs pour initialiser ses membres.

(Les modèles, qui doivent tous être alignés, sont un problème légèrement différent).

L'autre fois où vous créez une classe dans un en-tête, c'est quand vous pouvez utiliser la classe dans plusieurs projets et que vous voulez spécifiquement éviter de créer des liens dans les bibliothèques.

Un moment où vous pouvez inclure une classe entière dans une unité de compilation et ne pas exposer du tout son en-tête est:

  • Une classe "impl" qui n'est utilisée que par la classe qu'elle implémente. Il s'agit d'un détail d'implémentation de cette classe et n'est pas utilisé en externe.

  • Une implémentation d'une classe de base abstraite créée par une sorte de méthode d'usine qui renvoie un pointeur/référence/pointeur intelligent) à la classe de base. La méthode d'usine serait exposée par la classe elle-même ne le serait pas. (De plus, si la classe a une instance qui s'enregistre sur une table via une instance statique, elle n'a même pas besoin d'être exposée via une fabrique).

  • Une classe de type "foncteur".

En d'autres termes, lorsque vous ne voulez pas que quelqu'un inclue l'en-tête.

Je sais ce que vous pensez peut-être ... Si c'est juste pour la maintenabilité, en incluant le fichier cpp (ou un en-tête totalement en ligne), vous pouvez facilement éditer un fichier pour "trouver" le code et simplement reconstruire.

Cependant, la "maintenabilité" ne consiste pas seulement à avoir un code bien rangé. C'est une question d'impacts du changement. Il est généralement connu que si vous conservez un en-tête inchangé et modifiez simplement une implémentation (.cpp) fichier, il ne sera pas nécessaire de reconstruire l'autre source car il ne devrait pas y avoir d'effets secondaires.

Cela rend "plus sûr" de faire de tels changements sans se soucier de l'effet d'entraînement et c'est ce que l'on entend vraiment par "maintenabilité".

0
CashCow