J'ai donc terminé ma première mission de programmation C++ et 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 sais pas trop ce que cela signifie.
En jetant un coup d'œil à 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 bien fonctionner sans les fichiers d'en-tête ...). Je suppose que la niveleuse signifie que j'ai écrit '#include "mycppfile.cpp";' dans certains de mes fichiers.
Mon raisonnement pour #include
'les fichiers cpp étaient: - Tout ce qui était censé aller dans le fichier d'en-tête était dans mon fichier cpp, donc j'ai fait comme si c'était un fichier d'en-tête - De façon singe-voir-singe, j'ai vu cet autre en-tête les fichiers étaient #include
'd dans 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?
À ma connaissance, la norme C++ ne fait 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 de séparer vos fichiers source en premier lieu.
Essentiellement, ce que #include
est de dire au préprocesseur de prendre le fichier entier que vous avez spécifié et de le copier dans votre fichier actif avant le le compilateur met la main dessus. Ainsi, lorsque vous incluez tous les fichiers source dans votre projet ensemble, il n'y a fondamentalement aucune différence entre ce que vous avez fait et simplement créer un énorme fichier source sans aucune séparation.
"Oh, ce n'est pas grave. Si ça marche, ça va," Je vous entends pleurer. Et dans un sens, vous auriez raison. Mais en ce moment, vous avez affaire à un tout petit programme minuscule et à un processeur agréable et relativement peu encombrant pour le compiler pour vous. Vous n'aurez pas toujours autant de chance.
Si jamais vous plongez dans les domaines de la programmation informatique sérieuse, vous verrez des projets dont le nombre de lignes peut atteindre des millions plutôt que des dizaines. Ça 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! Cependant, puis-je empêcher ce destin terrible?!" 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 recompiler.
A moins que vous ne changiez quelque chose.
Maintenant, si vous aviez fusionné deux millions de lignes de code en un seul géant géant, et que vous deviez corriger un bogue simple comme, par exemple, x = y + 1
, cela signifie que vous devez à nouveau compiler les deux millions de lignes afin de tester cela. Et si vous découvrez que vous vouliez faire un x = y - 1
au lieu de cela, là encore, deux millions de lignes de compilation vous attendent. C'est beaucoup d'heures de temps perdu qui pourraient être mieux dépensées pour faire 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 en quelque sorte reliez-les ensemble 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, sauf si vous souhaitez exécuter un tas de minuscules fichiers .exe minuscules à la place.
"Mais ça doit être possible! La programmation sonne comme une pure torture sinon! Et si je trouvais un moyen de séparer l'interface de l'implémentation ? Dis en prenant juste assez d'informations de ces segments de code distincts pour les identifier dans le reste du programme et en les plaçant dans une sorte de fichier d'en-tête à la place? de cette façon, je peux utiliser le #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.
C'est probablement une réponse plus détaillée que vous ne le vouliez, 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 fonctions 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/struct sont compilées et assemblées une fois, puis d'autres fonctions peuvent appeler ce code à partir d'un seul 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 fonction mais pas la corps de la fonction.
Alors, comment tout est-il mélangé à la fin? C'est le travail du lieur. Un éditeur de liens lit tous les fichiers objets qui sont générés par l'étape d'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 réputés 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 vraiment qu'une archive avec du code réutilisable.
Il y a deux exceptions notables. Tout d'abord, si vous avez une petite fonction, vous pouvez la rendre inline. Cela signifie que le code machine généré ne génère pas d'appel de fonction externe, mais qu'il est littéralement concaténé sur place. Comme ils sont généralement de petite taille, 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 fonctions à l'intérieur d'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. Étant donné que le compilateur doit voir la définition de type de modèle entière lors de leur instanciation, il n'est pas possible de dissocier 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 de compilateur généralisé pour le mot-clé "export" a pris beaucoup de temps. Ainsi, sans prise en charge de l '"exportation", les unités de traduction obtiennent leurs propres copies locales des types et fonctions de modèles instanciés, comme le fonctionnement des fonctions en ligne. 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 placer les implémentations de fonctions en ligne, de fonctions modèles et de types modèles dans des fichiers .cpp, puis #include le fichier .cpp. Qu'il s'agisse d'un en-tête ou d'un fichier source n'a pas vraiment d'importance; le préprocesseur s'en fiche et n'est qu'une convention.
Un résumé rapide de l'ensemble du processus à partir du code C++ (plusieurs fichiers) et d'un exécutable final:
Encore une fois, c'était certainement plus que ce que vous aviez demandé, mais j'espère que les détails les plus fins vous aideront à voir la situation dans son ensemble.
La solution typique consiste à utiliser les fichiers .h
Pour les déclarations uniquement et les fichiers .cpp
Pour l'implémentation. Si vous devez réutiliser l'implémentation, vous incluez le fichier .h
Correspondant dans le fichier .cpp
Où la classe/fonction/ce qui est nécessaire est utilisé et vous liez à un fichier .cpp
Déjà compilé (soit un fichier .obj
- généralement utilisé dans un projet - ou un fichier .lib - généralement utilisé pour la réutilisation à partir de plusieurs projets). De cette façon, vous n'avez pas besoin de tout recompiler si seule l'implémentation change.
Considérez les fichiers cpp comme une boîte noire et les fichiers .h comme des guides sur l'utilisation de ces boîtes noires.
Les fichiers cpp peuvent être compilés à l'avance. Cela ne fonctionne pas en 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: Les modèles d'inclusion du fichier d'en-tête
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 venez de créer le nouveau fichier objet pour ce fichier .cpp particulier. Le compilateur n'a pas à 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 quels arguments ils 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 qui se trouve à l'intérieur des autres fichiers .cpp. Il ne sait donc pas comment les fonctions y sont implémentées et, par conséquent, ne peut pas optimiser de manière aussi agressive. Mais je pense que vous n'avez pas besoin de vous en préoccuper pour l'instant (:
L'idée de base que les en-têtes sont uniquement inclus et que les fichiers cpp sont uniquement compilés. Cela deviendra plus utile une fois que vous disposerez de nombreux fichiers cpp, et la recompilation de l’application entière lorsque vous en modifiez un seul sera trop lente. Ou quand les fonctions dans les 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.
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 #included, ce qui force alors la recompilation de tous les fichiers #including them.
Placez simplement vos déclarations dans des fichiers d'en-tête et incluez-les (car elles ne génèrent pas réellement de code en soi), et l'éditeur de liens connectera les déclarations avec le code cpp correspondant (qui n'est alors compilé qu'une seule fois).
réutilisation, 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 dans une classe mystring, vous placez la classe décl pour cela dans un mystring.h compilant mystring.cpp dans un fichier .obj
maintenant dans votre programme principal (par exemple main.cpp), vous incluez l'en-tête et le lien avec le mystring.obj. pour utiliser mystring dans votre programme, vous ne vous souciez pas des détails comment mystring est mis en œuvre depuis l'en-tête dit quoi ça peut faire
maintenant, si un copain veut utiliser votre classe de mystring, vous lui donnez mystring.h et le mystring.obj, il n'a pas non plus nécessairement besoin de savoir comment cela fonctionne tant qu'il fonctionne.
plus tard, si vous avez plus de tels fichiers .obj, vous pouvez les combiner dans un fichier .lib et les lier à la place.
vous pouvez également décider de modifier le fichier mystring.cpp et de l'implémenter plus efficacement, cela n'affectera pas votre programme main.cpp ou vos copains.
Si cela fonctionne pour vous, il n'y a rien de mal à cela - sauf qu'il ébouriffera les plumes des gens qui pensent qu'il n'y a qu'une seule façon de faire les choses.
La plupart des réponses données ici concernent des optimisations pour des 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 "l’optimisation prématurée". Selon votre environnement de développement, il peut y avoir une complexité supplémentaire importante impliquée dans la mise en place d'une configuration de build pour prendre en charge plusieurs fichiers source par programme.
Si, au fil du temps, votre projet évolue et que vous trouvez que le processus de construction prend trop de temps, alors vous pouvez refactoriser votre code pour utiliser plusieurs fichiers source pour des constructions incrémentielles plus rapides.
Plusieurs des réponses discutent de la séparation de l'interface de l'implémentation. Cependant, ce n'est pas une caractéristique inhérente des fichiers d'inclusion, et il est assez fréquent de #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".
Bien qu'il soit certainement possible de faire comme vous l'avez fait, la pratique standard consiste à placer des déclarations partagées dans des fichiers d'en-tête (.h) et des définitions de fonctions et de variables - implémentation - dans des fichiers source (.cpp).
En tant que convention, cela permet de clarifier où tout se trouve et de faire une distinction claire entre l'interface et la mise en œuvre 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 se casser s'il était défini dans plusieurs unités différentes.
Dites que vous écrivez un livre. Si vous placez les chapitres dans des fichiers séparés, vous n'avez besoin d'imprimer un chapitre que 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 de l'ouvrage dans son intégralité afin d'imprimer votre chapitre révisé. 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îne. 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.
Je vais vous suggérer de passer par Large Scale C++ Software Design by 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 si fréquemment. De même, un examen des modèles comme l'idiome Virtual Constructor vous aidera à mieux comprendre le concept.
J'apprends toujours comme toi :)
Lorsque vous compilez et liez un programme, le compilateur compile d'abord les fichiers cpp individuels puis ils les lient (se connectent). Les en-têtes ne seront jamais compilés, sauf s'ils sont 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 ignorez comment vous implémentez réellement les détails. De cette façon, vous n'avez pas à recompiler chaque fichier cpp si vous effectuez une modification en un.