Pourquoi ne devrais-je pas inclure de fichiers cpp et utiliser à la place un en-tête?

147

J'ai donc terminé ma première mission de programmation C ++ et j'ai reçu ma note. Mais selon le classement, j'ai perdu des points pour including cpp files instead of compiling and linking them. Je ne suis pas trop clair sur ce que cela signifie.

En repensant à mon code, j'ai choisi de ne pas créer de fichiers d'en-tête pour mes classes, mais j'ai tout fait dans les fichiers cpp (cela semblait fonctionner correctement sans fichiers d'en-tête ...). Je suppose que la niveleuse voulait dire que j'ai écrit '#include "mycppfile.cpp";' dans certains de mes fichiers.

Mon raisonnement pour #includeles fichiers cpp était: - Tout ce qui était censé entrer dans le fichier d'en-tête était dans mon fichier cpp, alors j'ai prétendu que c'était comme un fichier d'en-tête - À la mode monkey-see-monkey do, j'ai vu cet autre les fichiers d'en-tête étaient #includedans les fichiers, j'ai donc fait de même pour mon fichier cpp.

Alors qu'est-ce que j'ai fait de mal exactement, et pourquoi est-ce mauvais?

ialm
la source
36
C'est une très bonne question. Je m'attends à ce que beaucoup de débutants en C ++ soient aidés par cela.
Mia Clarke

Réponses:

175

À ma connaissance, le standard C ++ ne connaît aucune différence entre les fichiers d'en-tête et les fichiers source. En ce qui concerne la langue, tout fichier texte avec code légal est le même que tout autre. Cependant, bien que ce ne soit pas illégal, l'inclusion de fichiers source dans votre programme éliminera à peu près tous les avantages que vous auriez en séparant vos fichiers source en premier lieu.

Essentiellement, ce que #includefait, c'est dire au préprocesseur de prendre tout le fichier que vous avez spécifié et de le copier dans votre fichier actif avant que le compilateur ne mette la main dessus. Ainsi, lorsque vous incluez tous les fichiers source de votre projet ensemble, il n'y a fondamentalement aucune différence entre ce que vous avez fait et la création d'un seul fichier source énorme sans aucune séparation.

"Oh, ce n'est pas grave. Si ça marche, c'est bien," je t'entends pleurer. Et dans un sens, vous auriez raison. Mais pour le moment, vous avez affaire à un tout petit programme et à un processeur agréable et relativement peu encombré pour le compiler pour vous. Vous ne serez pas toujours aussi chanceux.

Si jamais vous plongez dans les domaines de la programmation informatique sérieuse, vous verrez des projets avec des nombres de lignes qui peuvent atteindre des millions, plutôt que des dizaines. Cela fait beaucoup de lignes. Et si vous essayez d'en compiler un sur un ordinateur de bureau moderne, cela peut prendre quelques heures au lieu de quelques secondes.

"Oh non! Cela semble horrible! Mais puis-je empêcher ce terrible destin?!" Malheureusement, vous ne pouvez pas faire grand-chose à ce sujet. Si la compilation prend des heures, la compilation prend des heures. Mais cela n'a vraiment d'importance que la première fois - une fois que vous l'avez compilé une fois, il n'y a aucune raison de le compiler à nouveau.

Sauf si vous changez quelque chose.

Maintenant, si vous aviez deux millions de lignes de code fusionnées en un géant géant et x = y + 1que vous deviez faire une simple correction de bogue telle que, par exemple , cela signifie que vous devez recompiler les deux millions de lignes pour tester cela. Et si vous découvrez que vous vouliez faire un à la x = y - 1place, alors encore une fois, deux millions de lignes de compilation vous attendent. Ce sont de nombreuses heures perdues qui pourraient être mieux consacrées à autre chose.

"Mais je déteste être improductif! Si seulement il y avait un moyen de compiler des parties distinctes de ma base de code individuellement, et de les relier d'une manière ou d'une autre par la suite!" Une excellente idée, en théorie. Mais que faire si votre programme a besoin de savoir ce qui se passe dans un autre fichier? Il est impossible de séparer complètement votre base de code à moins que vous ne souhaitiez exécuter un tas de minuscules fichiers .exe à la place.

"Mais sûrement cela doit être possible! La programmation ressemble à de la pure torture sinon! Et si je trouvais un moyen de séparer l' interface de l'implémentation ? Dites en prenant juste assez d'informations de ces segments de code distincts pour les identifier au reste du programme, et en mettant à la place dans une sorte de fichier d'en- tête ? Et de cette façon, je peux utiliser la #include directive du préprocesseur pour n'apporter que les informations nécessaires à la compilation! "

Hmm. Vous pourriez être sur quelque chose là-bas. Faites-moi savoir comment cela fonctionne pour vous.

orPseudo
la source
13
Bonne réponse, monsieur. C'était une lecture amusante et facile à comprendre. J'aimerais que mon manuel soit écrit comme ça.
ialm
@veol Recherche la série de livres Head First - Je ne sais pas s'ils ont une version C ++. headfirstlabs.com
Amarghosh
2
C'est (certainement) le meilleur libellé à ce jour que j'ai entendu ou envisagé. Justin Case, un débutant accompli, a atteint un projet cadencé à un million de frappes, pas encore livré et un «premier projet» louable qui voit la lumière de l'application dans une véritable base d'utilisateurs, a reconnu un problème résolu par les fermetures. Sonne remarquablement similaire aux états avancés de la définition originale du problème d'OP moins le "codé cela près de cent fois et ne peut pas comprendre quoi faire pour null (en tant qu'objet) vs null (en neveu) sans utiliser la programmation par exceptions."
Nicholas Jordan
Bien sûr, tout cela tombe en panne pour les modèles car la plupart des compilateurs ne prennent pas en charge / n'implémentent pas le mot clé «export».
KitsuneYMG
1
Un autre point est que vous avez de nombreuses bibliothèques de pointe (si vous pensez à BOOST) qui utilisent uniquement des en-têtes de classes ... Ho, attendez? Pourquoi un programmeur expérimenté ne sépare-t-il pas l'interface de l'implémentation? Une partie de la réponse peut être ce que Blindly a dit, une autre partie peut être qu'un fichier vaut mieux que deux quand c'est possible, et une autre partie est que la liaison a un coût qui peut être assez élevé. J'ai vu des programmes s'exécuter dix fois plus vite avec l'inclusion directe de la source et l'optimisation du compilateur. Parce que la liaison bloque principalement l'optimisation.
kriss
45

C'est probablement une réponse plus détaillée que vous ne le souhaitiez, mais je pense qu'une explication décente est justifiée.

En C et C ++, un fichier source est défini comme une unité de traduction . Par convention, les fichiers d'en-tête contiennent des déclarations de fonction, des définitions de type et des définitions de classe. Les implémentations de fonction réelles résident dans des unités de traduction, c'est-à-dire des fichiers .cpp.

L'idée derrière cela est que les fonctions et les fonctions membres de classe / structure sont compilées et assemblées une fois, puis d'autres fonctions peuvent appeler ce code à partir d'un endroit sans faire de doublons. Vos fonctions sont déclarées implicitement comme "extern".

/* Function declaration, usually found in headers. */
/* Implicitly 'extern', i.e the symbol is visible everywhere, not just locally.*/
int add(int, int);

/* function body, or function definition. */
int add(int a, int b) 
{
   return a + b;
}

Si vous souhaitez qu'une fonction soit locale pour une unité de traduction, vous la définissez comme «statique». Qu'est-ce que ça veut dire? Cela signifie que si vous incluez des fichiers source avec des fonctions externes, vous obtiendrez des erreurs de redéfinition, car le compilateur rencontre plusieurs fois la même implémentation. Donc, vous voulez que toutes vos unités de traduction voient la déclaration de la fonction mais pas le corps de la fonction .

Alors, comment tout est-il écrasé à la fin? C'est le travail de l'éditeur de liens. Un éditeur de liens lit tous les fichiers objets générés par l'étape assembleur et résout les symboles. Comme je l'ai dit plus tôt, un symbole n'est qu'un nom. Par exemple, le nom d'une variable ou d'une fonction. Lorsque les unités de traduction qui appellent des fonctions ou déclarent des types ne connaissent pas l'implémentation de ces fonctions ou types, ces symboles sont dits non résolus. L'éditeur de liens résout le symbole non résolu en connectant l'unité de traduction qui contient le symbole non défini avec celle qui contient l'implémentation. Phew. Cela est vrai pour tous les symboles visibles de l'extérieur, qu'ils soient implémentés dans votre code ou fournis par une bibliothèque supplémentaire. Une bibliothèque n'est en réalité qu'une archive avec du code réutilisable.

Il existe deux exceptions notables. Premièrement, si vous avez une petite fonction, vous pouvez la créer en ligne. Cela signifie que le code machine généré ne génère pas d'appel de fonction externe, mais est littéralement concaténé sur place. Puisqu'ils sont généralement petits, la taille des frais généraux n'a pas d'importance. Vous pouvez les imaginer statiques dans leur fonctionnement. Il est donc sûr d'implémenter des fonctions en ligne dans les en-têtes. Les implémentations de fonction dans une définition de classe ou de structure sont également souvent intégrées automatiquement par le compilateur.

L'autre exception concerne les modèles. Puisque le compilateur a besoin de voir toute la définition du type de modèle lors de leur instanciation, il n'est pas possible de découpler l'implémentation de la définition comme avec les fonctions autonomes ou les classes normales. Eh bien, c'est peut-être possible maintenant, mais obtenir un support généralisé du compilateur pour le mot-clé "export" a pris très, très longtemps. Ainsi, sans prise en charge de l '«exportation», les unités de traduction obtiennent leurs propres copies locales de types et de fonctions modèles instanciés, de la même façon que les fonctions en ligne fonctionnent. Avec la prise en charge de «l'exportation», ce n'est pas le cas.

Pour les deux exceptions, certaines personnes trouvent "plus agréable" de mettre les implémentations de fonctions en ligne, de fonctions basées sur des modèles et de types de modèles dans des fichiers .cpp, puis #incluez le fichier .cpp. Que ce soit un en-tête ou un fichier source n'a pas vraiment d'importance; le préprocesseur ne s'en soucie pas et n'est qu'une convention.

Un résumé rapide de l'ensemble du processus depuis le code C ++ (plusieurs fichiers) jusqu'à un exécutable final:

  • Le préprocesseur est exécuté, qui analyse toutes les directives commençant par un '#'. La directive #include concatène le fichier inclus avec inferior, par exemple. Il effectue également le remplacement de macro et le collage de jetons.
  • Le compilateur réel s'exécute sur le fichier texte intermédiaire après l'étape du préprocesseur et émet du code assembleur.
  • L' assembleur s'exécute sur le fichier d'assemblage et émet du code machine, cela s'appelle généralement un fichier objet et suit le format exécutable binaire du système d'exploitation en question. Par exemple, Windows utilise le PE (format exécutable portable), tandis que Linux utilise le format ELF Unix System V, avec des extensions GNU. À ce stade, les symboles sont toujours marqués comme non définis.
  • Enfin, l' éditeur de liens est exécuté. Toutes les étapes précédentes ont été exécutées sur chaque unité de traduction dans l'ordre. Cependant, l'étape de l'éditeur de liens fonctionne sur tous les fichiers objets générés qui ont été générés par l'assembleur. L'éditeur de liens résout les symboles et fait beaucoup de magie comme la création de sections et de segments, qui dépend de la plate-forme cible et du format binaire. Les programmeurs ne sont pas tenus de le savoir en général, mais cela aide sûrement dans certains cas.

Encore une fois, c'était certainement plus que ce que vous aviez demandé, mais j'espère que les détails concrets vous aideront à avoir une vue d'ensemble.

melpomène
la source
2
Merci pour votre explication approfondie. J'avoue que tout cela n'a pas encore de sens pour moi, et je pense que je vais devoir relire votre réponse encore (et encore).
ialm
1
+1 pour une excellente explication. dommage que cela effraie probablement tous les débutants en C ++. :)
goldPseudo
1
Heh, ne te sens pas mal veol. Sur Stack Overflow, la réponse la plus longue est rarement la meilleure.
int add(int, int);est une déclaration de fonction . le partie prototype est juste int, int. Cependant, toutes les fonctions en C ++ ont un prototype, donc le terme n'a vraiment de sens qu'en C. J'ai édité votre réponse à cet effet.
melpomene le
exportfor templates a été supprimé du langage en 2011. Il n'a jamais été vraiment supporté par les compilateurs.
melpomene le
10

La solution typique consiste à utiliser des .hfichiers pour les déclarations uniquement et des .cppfichiers pour l'implémentation. Si vous avez besoin de réutiliser l'implémentation, vous incluez le .hfichier correspondant dans le .cppfichier où la classe / fonction / tout ce qui est nécessaire est utilisé et un lien vers un .cppfichier déjà compilé (soit un .objfichier - généralement utilisé dans un projet - soit un fichier .lib - généralement utilisé pour la réutilisation de plusieurs projets). De cette façon, vous n'avez pas besoin de tout recompiler si seule l'implémentation change.

dents acérées
la source
6

Considérez les fichiers cpp comme une boîte noire et les fichiers .h comme des guides sur la façon d'utiliser ces boîtes noires.

Les fichiers cpp peuvent être compilés à l'avance. Cela ne fonctionne pas chez vous #incluez-les, car il doit réellement "inclure" le code dans votre programme chaque fois qu'il le compile. Si vous incluez simplement l'en-tête, il peut simplement utiliser le fichier d'en-tête pour déterminer comment utiliser le fichier cpp précompilé.

Bien que cela ne fasse pas beaucoup de différence pour votre premier projet, si vous commencez à écrire de gros programmes cpp, les gens vont vous détester parce que les temps de compilation vont exploser.

Lisez également ceci: Le fichier d'en-tête comprend des modèles

Dan McGrath
la source
Merci pour l'exemple plus concret. J'ai essayé de lire votre lien, mais maintenant je suis confus ... quelle est la différence entre inclure un en-tête explicitement et une déclaration avant?
ialm
Cet article est super. Veol, ici, ils incluent des en-têtes où le compilateur a besoin d'une information concernant la taille de la classe. La déclaration directe est utilisée lorsque vous n'utilisez que des pointeurs.
pankajt
déclaration avant: int someFunction (int requiredValue); notez l'utilisation des informations de type et (généralement) pas d'accolades. Ceci, comme indiqué, indique au compilateur qu'à un moment donné, vous aurez besoin d'une fonction qui prend un int et retourne un int, le compilateur peut réserver un appel pour cela en utilisant ces informations. Cela s'appellerait une déclaration anticipée. Les compilateurs plus sophistiqués sont censés être capables de trouver la fonction sans avoir besoin de cela, y compris un en-tête peut être un moyen pratique de déclarer un tas de déclarations forward.
Nicholas Jordan
6

Les fichiers d'en-tête contiennent généralement des déclarations de fonctions / classes, tandis que les fichiers .cpp contiennent les implémentations réelles. Au moment de la compilation, chaque fichier .cpp est compilé dans un fichier objet (généralement l'extension .o), et l'éditeur de liens combine les différents fichiers objet dans l'exécutable final. Le processus de liaison est généralement beaucoup plus rapide que la compilation.

Avantages de cette séparation: Si vous recompilez l'un des fichiers .cpp de votre projet, vous n'avez pas à recompiler tous les autres. Vous créez simplement le nouveau fichier objet pour ce fichier .cpp particulier. Le compilateur n'a pas besoin de regarder les autres fichiers .cpp. Cependant, si vous souhaitez appeler des fonctions dans votre fichier .cpp actuel qui ont été implémentées dans les autres fichiers .cpp, vous devez indiquer au compilateur les arguments qu'elles prennent; c'est le but d'inclure les fichiers d'en-tête.

Inconvénients: lors de la compilation d'un fichier .cpp donné, le compilateur ne peut pas «voir» ce qu'il y a à l'intérieur des autres fichiers .cpp. Il ne sait donc pas comment les fonctions sont implémentées et ne peut donc pas être optimisé de manière aussi agressive. Mais je pense que vous n'avez pas besoin de vous en préoccuper pour le moment (:

int3
la source
5

L'idée de base selon laquelle les en-têtes sont uniquement inclus et les fichiers cpp ne sont que compilés. Cela deviendra plus utile une fois que vous aurez plusieurs fichiers cpp, et la recompilation de l'ensemble de l'application lorsque vous en modifiez un seul sera trop lente. Ou lorsque les fonctions des fichiers commenceront en fonction les unes des autres. Donc, vous devez séparer les déclarations de classe dans vos fichiers d'en-tête, laisser l'implémentation dans les fichiers cpp et écrire un Makefile (ou autre chose, selon les outils que vous utilisez) pour compiler les fichiers cpp et lier les fichiers objets résultants dans un programme.

Lukáš Lalinský
la source
3

Si vous #incluez un fichier cpp dans plusieurs autres fichiers de votre programme, le compilateur essaiera de compiler le fichier cpp plusieurs fois et générera une erreur car il y aura plusieurs implémentations des mêmes méthodes.

La compilation prendra plus de temps (ce qui devient un problème sur les grands projets), si vous apportez des modifications dans les fichiers # cpp inclus, ce qui forcera la recompilation de tous les fichiers # y compris.

Mettez simplement vos déclarations dans des fichiers d'en-tête et incluez-les (car ils ne génèrent pas de code en soi), et l'éditeur de liens reliera les déclarations avec le code cpp correspondant (qui n'est alors compilé qu'une seule fois).

NeilDurant
la source
Donc, en plus d'avoir des temps de compilation plus longs, je vais commencer à avoir des problèmes lorsque j'inclus mon fichier cpp dans de nombreux fichiers différents qui utilisent les fonctions des fichiers cpp inclus?
ialm
Oui, cela s'appelle une collision d'espace de noms. Il est intéressant ici de savoir si la liaison avec les bibliothèques introduit des problèmes d'espace de noms. En général, je trouve que les compilateurs produisent de meilleurs temps de compilation pour la portée de l'unité de traduction (le tout dans un seul fichier), ce qui introduit des problèmes d'espace de noms - ce qui conduit à une nouvelle séparation .... vous pouvez inclure le fichier d'inclusion dans chaque unité de traduction, (supposé) il y a même un pragma (#pragma une fois) qui est censé imposer cela, mais c'est une supposition suppositoire. Veillez à ne pas vous fier aveuglément aux bibliothèques (fichiers .O) de n'importe où, car les liens 32 bits ne sont pas appliqués.
Nicholas Jordan
2

Bien qu'il soit certainement possible de faire ce que vous avez fait, la pratique standard est de mettre les déclarations partagées dans des fichiers d'en-tête (.h) et les définitions de fonctions et de variables - implémentation - dans des fichiers source (.cpp).

En tant que convention, cela permet de préciser où tout se trouve et de faire une distinction claire entre l'interface et l'implémentation de vos modules. Cela signifie également que vous n'avez jamais à vérifier si un fichier .cpp est inclus dans un autre, avant d'y ajouter quelque chose qui pourrait casser s'il était défini dans plusieurs unités différentes.

Avi
la source
2

réutilisabilité, architecture et encapsulation des données

voici un exemple:

disons que vous créez un fichier cpp qui contient une forme simple de routines de chaîne le tout dans une classe mystring, vous placez la classe decl pour cela dans un mystring.h compilant mystring.cpp dans un fichier .obj

maintenant, dans votre programme principal (par exemple main.cpp), vous incluez un en-tête et un lien avec le mystring.obj. pour utiliser mystring dans votre programme, vous ne vous souciez pas des détails de l' implémentation de mystring puisque l'en-tête dit ce qu'il peut faire

maintenant, si un copain veut utiliser votre classe mystring, vous lui donnez mystring.h et mystring.obj, il n'a pas non plus nécessairement besoin de savoir comment cela fonctionne tant que cela fonctionne.

plus tard, si vous avez plus de fichiers .obj, vous pouvez les combiner dans un fichier .lib et créer un lien vers celui-ci à la place.

vous pouvez également décider de changer le fichier mystring.cpp et de l'implémenter plus efficacement, cela n'affectera pas votre main.cpp ou votre programme de copains.

AndersK
la source
2

Si cela fonctionne pour vous, il n'y a rien de mal à cela - sauf que cela ébouriffera les plumes des gens qui pensent qu'il n'y a qu'une seule façon de faire les choses.

Bon nombre des réponses données ici concernent les optimisations pour les projets logiciels à grande échelle. Ce sont de bonnes choses à savoir, mais il ne sert à rien d’optimiser un petit projet comme s’il s’agissait d’un grand projet - c’est ce que l’on appelle «optimisation prématurée». En fonction de votre environnement de développement, il peut y avoir une complexité supplémentaire significative impliquée dans la configuration d'une configuration de construction pour prendre en charge plusieurs fichiers source par programme.

Si, au fil du temps, votre projet évolue et vous trouvez que le processus de construction prend trop de temps, alors vous pouvez factoriser votre code pour utiliser plusieurs fichiers source pour builds plus rapide incrémentale.

Plusieurs des réponses discutent de la séparation de l'interface de l'implémentation. Cependant, ce n'est pas une fonctionnalité inhérente aux fichiers d'inclusion, et il est assez courant d'inclure des fichiers «d'en-tête» qui incorporent directement leur implémentation (même la bibliothèque standard C ++ le fait dans une large mesure).

La seule chose vraiment "non conventionnelle" à propos de ce que vous avez fait était de nommer vos fichiers inclus ".cpp" au lieu de ".h" ou ".hpp".

Brent Bradburn
la source
1

Lorsque vous compilez et liez un programme, le compilateur compile d'abord les fichiers cpp individuels, puis ils les lient (connectent). Les en-têtes ne seront jamais compilés, à moins d'être d'abord inclus dans un fichier cpp.

Les en-têtes sont généralement des déclarations et cpp sont des fichiers d'implémentation. Dans les en-têtes, vous définissez une interface pour une classe ou une fonction, mais vous oubliez comment vous implémentez réellement les détails. De cette façon, vous n'avez pas à recompiler chaque fichier cpp si vous apportez une modification dans un.

Jonas
la source
si vous laissez l'implémentation hors du fichier d'en-tête, excusez-moi, mais cela ressemble à une interface Java pour moi, n'est-ce pas?
gansub
1

Je vais vous suggérer de passer par conception de logiciels C ++ grande échelle par John Lakos . Au collège, nous écrivons généralement de petits projets où nous ne rencontrons pas de tels problèmes. Le livre souligne l'importance de séparer les interfaces et les implémentations.

Les fichiers d'en-tête ont généralement des interfaces censées ne pas être modifiées aussi fréquemment. De même, un examen des modèles tels que l'idiome Virtual Constructor vous aidera à comprendre le concept plus loin.

J'apprends toujours comme toi :)

pankajt
la source
Merci pour la suggestion de livre. Je ne sais pas si j'arriverai un jour au stade de la création de programmes C ++ à grande échelle ...
ialm
c'est amusant de coder des programmes à grande échelle et pour de nombreux défis. Je commence à aimer :)
pankajt
1

C'est comme écrire un livre, vous ne voulez imprimer les chapitres finis qu'une seule fois

Disons que vous écrivez un livre. Si vous placez les chapitres dans des fichiers séparés, il vous suffit d'imprimer un chapitre si vous l'avez modifié. Travailler sur un chapitre ne change aucun des autres.

Mais inclure les fichiers cpp est, du point de vue du compilateur, comme éditer tous les chapitres du livre dans un seul fichier. Ensuite, si vous le modifiez, vous devez imprimer toutes les pages du livre entier afin d'obtenir votre chapitre révisé imprimé. Il n'y a pas d'option "imprimer les pages sélectionnées" dans la génération de code objet.

Retour au logiciel: j'ai Linux et Ruby src qui traînent. Une mesure approximative des lignes de code ...

     Linux       Ruby
   100,000    100,000   core functionality (just kernel/*, ruby top level dir)
10,000,000    200,000   everything 

Chacune de ces quatre catégories a beaucoup de code, d'où le besoin de modularité. Ce type de base de code est étonnamment typique des systèmes du monde réel.

DigitalRoss
la source