Pourquoi avons-nous besoin d'inclure le .h alors que tout fonctionne en ne retenant que le fichier .cpp?


18

Pourquoi avons-nous besoin d'inclure à la fois les .het les .cppfichiers pendant que nous pouvons le faire fonctionner uniquement en incluant le .cppfichier?

Par exemple: la création d' une file.hdéclaration contenant, puis en créant un file.cppcontenant des définitions et comprenant à la fois dans main.cpp.

En variante: la création d' une file.cppdéclaration / définitions contenant (pas de prototypes) , y compris dans main.cpp.

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


Partager vos recherches aide tout le monde . Dites-nous ce que vous avez essayé et pourquoi cela n'a pas répondu à vos besoins. Cela démontre que vous avez pris le temps d'essayer de vous aider, il nous sauve de réitérer des réponses évidentes, et la plupart de tout ce qu'il vous aide à obtenir une plus spécifique et réponse pertinente. Voir aussi How to Ask
gnat

Je recommande fortement de regarder les questions connexes ici sur le côté. programmers.stackexchange.com/questions/56215/… et programmers.stackexchange.com/questions/115368/… sont de bonnes lectures

Pourquoi est-ce sur les programmeurs, et non sur SO?
Courses de légèreté avec Monica

@LightnessRacesInOrbit parce que c'est une question de style de codage et non une question de "comment faire".
CashCow

@CashCow: On dirait que vous comprenez mal SO, qui n'est pas un site "comment". Il semble également que vous ayez mal compris cette question, qui n'est pas une question de style de codage.
Courses de légèreté avec Monica

Réponses:


29

Bien que vous puissiez inclure des .cppfichiers 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 .cppfichier comprend des définitions pour un sous-ensemble du programme, comme une classe, un groupe de fonctions organisé de manière logique, des variables statiques globales (à utiliser avec modération le cas échéant), etc.

Chaque unité de compilation ( .cppfichier) comprend alors toutes les déclarations dont elle a besoin pour compiler les définitions qu'elle 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.

Les deux Foo.cppet Main.cppdoivent inclure Foo.h. Foo.cppil a besoin parce qu'il définit le code qui sauvegarde l'interface de classe, donc il a besoin de savoir ce que cette interface est. Main.cppen 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 encore besoin de l'implémentation réelle.

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

Vient maintenant l'éditeur de liens, qui combine les deux fichiers objets Foo.oet Main.oen un fichier exécutable. Il voit les références Foo non résolues dans Main.omais voit que Foo.ocontient les symboles nécessaires, il « relie les points » pour ainsi dire. Un appel de fonction Main.oest maintenant connecté à l'emplacement réel du code compilé de façon à l' exécution, le programme peut sauter au bon endroit.

Si vous aviez inclus le Foo.cppfichier 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 pas, Foo.cppmais pourquoi est-ce dans un .cppfichier séparé ?)

Enfin, l'idée des différents types de fichiers est sans rapport avec un compilateur C / C. Il compile « fichiers texte » qui contiennent un code valide , espérons pour la langue souhaitée. Parfois , il peut être en mesure de dire la langue en fonction de l'extension du fichier. Par exemple, compiler un .cfichier avec aucune option de compilateur et il assumera C, tandis qu'une .ccou l' .cppextension raconteraient à assumer C ++. Cependant, je peux facilement dire à un compilateur de compiler un fichier .hou même .docxen C ++, et il émettra un .ofichier object ( ) s'il contient du code C ++ valide au format texte brut. Ces extensions sont plus au profit du programmeur. Si je vois Foo.het Foo.cpp, je suppose tout de suite que la première contient la déclaration de la classe et la seconde contient la définition.


Je ne comprends pas l'argument que vous avancez ici. Si vous incluez simplement Foo.cppdans Main.cppvous n'avez pas besoin d'inclure le .hfichier, vous avez un fichier de moins, vous gagnez toujours à diviser le code en fichiers séparés pour plus de lisibilité, et votre commande de compilation est plus simple. Même si je comprends l'importance des dossiers en- tête , je ne pense pas que ce soit il
Benjamin Gruenbaum

2
Pour un programme trivial, il suffit de mettre le tout dans un seul fichier. Ne pas inclure même les en- têtes de projet (seulement en - têtes bibliothèque). Y compris les fichiers source est une mauvaise habitude qui cause des problèmes pour tout , mais les plus petits programmes une fois que vous avez plusieurs unités de compilation et effectivement les compilez séparément.

2
If you had included the Foo.cpp file in Main.cpp, there would be two definitions of class Foo.Phrase la plus importante concernant la question.
marczellm

@Snowman droit, ce qui est ce que je pense que vous devriez vous concentrer sur - plusieurs unités de compilation. Code coller avec / sans les fichiers d' en- tête pour casser le code jusqu'à plus petits morceaux est utile et orthogonale à l'objet de fichiers d' en- tête. Si tout ce que vous voulez faire est de le diviser en fichiers d'en-tête, les fichiers ne nous achètent rien - une fois que nous avons besoin de liens dynamiques, de liens conditionnels et de versions plus avancées, ils sont soudain très très utiles.
Benjamin Gruenbaum

Il existe de nombreuses façons d'organiser la source d'un compilateur / éditeur de liens C / C ++. Le demandeur n'était pas sûr du processus, j'ai donc expliqué la meilleure pratique pour organiser le code et comment le processus fonctionne. Vous pouvez séparer les cheveux sur la possibilité d'organiser le code différemment, mais le fait demeure, j'ai expliqué comment 99% des projets le font, ce qui est la meilleure pratique pour une raison. Il fonctionne bien, et maintient votre santé mentale.

9

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é à l'intérieur du compilateur proprement dit cc1 ou cc1plus). Lisez en particulier la documentation du cpppréprocesseur GNU . Donc, dans la pratique, le compilateur traite d'abord conceptuellement votre unité de compilation (ou unité de traduction ), puis travaille sur le formulaire prétraité.

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

  • définitions de macro
  • définitions de types (par exemple typedef, struct, classetc, ...)
  • définitions des static inlinefonctions
  • 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 a file.cppbesoin de tout ce qui précède, donc le veut d' #include "file.h" abord.

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 en tant que 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 certains informations à l' intérieur du compilateur).

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

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


1
Si les liens pourrissent, serait-ce toujours une bonne réponse?
Dan Pichelman

4
J'ai édité ma réponse pour l'améliorer, et j'ai soigneusement choisi des liens - vers wikipedia ou vers la documentation GNU - qui, je crois, resteront pertinents pendant longtemps.
Basile Starynkevitch

3

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 sauvegarder tout 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. Il est également juste un grand désordre.


0

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 ++.

En-têtes fichier fournir 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 la mise en œuvre 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

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 qui est 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 intégré) 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 que vous modifiez simplement un (.cpp)fichier d' implémentation , 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» d'effectuer de tels changements sans se soucier de l'effet d'entraînement et c'est ce que l'on entend vraiment par «maintenabilité».


-1

L'exemple de Snowman a besoin d'une petite extension pour vous montrer exactement pourquoi les fichiers .h sont nécessaires.

Ajoutez une autre barre de classe dans le jeu qui dépend également de la classe Foo.

Foo.h -> contient une déclaration pour la classe Foo

Foo.cpp -> contient la définition (impementation) de la classe Foo

Main.cpp -> utilise une variable de type Foo.

Bar.cpp -> utilise également une variable de type Foo.

Maintenant, tous les fichiers cpp doivent inclure Foo.h Ce serait une erreur d'inclure Foo.cpp dans plus d'un autre fichier cpp. L'éditeur de liens échouerait car la classe Foo serait définie plusieurs fois.


1
Ce n'est pas ça non plus. Cela pourrait être résolu de la même manière que dans les .hfichiers - avec les gardes d'inclusion et c'est exactement le même problème que vous avez avec les .hfichiers de toute façon. Ce n'est pas du tout ce que .hsont les fichiers.
Benjamin Gruenbaum

Je ne sais pas comment utiliseriez-vous les gardes inclus car ils ne fonctionnent que par fichier (comme le fait le préprocesseur). Dans ce cas, puisque Main.cpp et Bar.cpp sont des fichiers différents, Foo.cpp ne sera inclus qu'une seule fois dans les deux (protégé ou non ne fait aucune différence). Main.cpp et Bar.cpp compileraient tous les deux. Mais une erreur de lien se produit toujours en raison de la double définition de la classe Foo. Mais il y a aussi un héritage que je n'ai même pas mentionné. Déclaration pour les modules externes (dll), etc.
SkaarjFace

Non, il s'agirait d'une seule unité de compilation, aucune liaison n'aurait lieu du tout puisque vous incluez le .cppfichier.
Benjamin Gruenbaum

Je n'ai pas dit qu'il ne compilerait pas. Mais il ne liera pas à la fois main.o et bar.o dans le même exécutable. Sans lier du tout l'intérêt de la compilation.
SkaarjFace

Euh ... faire exécuter le code? Vous pouvez toujours le lier mais simplement ne pas lier les fichiers les uns aux autres - ce serait plutôt la même unité de compilation.
Benjamin Gruenbaum

-2

Si vous écrivez tout le code dans le même fichier, cela rendra votre code laid.
Deuxièmement, vous ne pourrez pas partager votre classe écrite avec d'autres. Selon l'ingénierie logicielle, vous devez écrire le code client séparément. Le client ne doit pas savoir comment fonctionne votre programme. Ils ont juste besoin de sortie. Si vous écrivez tout le programme dans le même fichier, la sécurité de votre programme sera compromise.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.