Pourquoi ne devrais-je pas inclure les fichiers cpp et utiliser plutôt un en-tête?

J’ai donc terminé ma première affectation de programmation C ++ et reçu ma note. Mais selon l’évaluation, j’ai perdu des marques pour avoir including cpp files instead of compiling and linking them . Je ne sais pas trop 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 les fichiers d’en-tête …). Je suppose que la niveleuse signifiait que j’avais écrit “#include” mycppfile.cpp “;” dans certains de mes fichiers.

Mon raisonnement pour #include les fichiers cpp était le suivant: – Tout ce qui était supposé aller dans le fichier d’en-tête était dans mon fichier cpp, donc j’ai prétendu que c’était comme un fichier d’en-tête – Dans monkey-see-monkey les autres fichiers d’en-tête étaient #include ‘d dans les fichiers, alors j’ai fait la même chose pour mon fichier cpp.

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

    À ma connaissance, le standard 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 un code légal est le même que tout autre. Cependant, bien que cela ne soit pas illégal, inclure des fichiers sources dans votre programme éliminera à peu près tous les avantages que vous auriez obtenus en séparant vos fichiers sources en premier lieu.

    Essentiellement, ce que #include fait, c’est demander au préprocesseur de prendre l’intégralité du fichier que vous avez spécifié, et de le copier dans votre fichier actif avant que le compilateur ne le trouve. Ainsi, lorsque vous incluez tous les fichiers source dans votre projet, il n’y a pas de différence fondamentale entre ce que vous avez fait et la création d’un fichier source énorme sans aucune séparation.

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

    Si vous vous penchez sur la programmation informatique sérieuse, vous verrez des projets dont le nombre de lignes peut atteindre des millions plutôt que des dizaines. C’est beaucoup de lignes. Et si vous essayez de comstackr l’un d’entre eux sur un ordinateur de bureau moderne, cela peut prendre quelques heures au lieu de quelques secondes.

    “Oh non! Ca a l’air horrible! Cependant, est-ce que je peux empêcher ce terrible destin?” Malheureusement, vous ne pouvez pas faire grand chose à ce sujet. S’il faut des heures pour comstackr, cela prend des heures pour comstackr. Mais ce n’est vraiment important que la première fois – une fois que vous l’avez compilé une fois, il n’y a aucune raison de le comstackr à nouveau.

    A moins de changer quelque chose.

    Maintenant, si vous aviez deux millions de lignes de code fusionnées en un géant géant, et que vous deviez faire un simple correctif tel que, par exemple, x = y + 1 , cela signifie que vous devrez comstackr à nouveau les deux millions de lignes testez ceci. Et si vous découvrez que vous vouliez faire un x = y - 1 place, deux millions de lignes de compilation vous attendent. C’est beaucoup de temps perdu qui pourrait être mieux passé à faire autre chose.

    “Mais je déteste être improductif! Si seulement il y avait un moyen de comstackr des parties distinctes de mon code base individuellement, et de les relier d’une manière ou d’une autre après!” Une excellente idée, en théorie. Mais que se passe-t-il si votre programme a besoin de savoir ce qui se passe dans un fichier différent? 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.

    “Mais cela doit sûrement être possible! La programmation sonne comme une pure torture autrement! Et si je trouvais un moyen de séparer l’ interface de la mise en œuvre ? Dites juste assez d’informations à ces différents segments de code pour les identifier au rest du programme et eux dans une sorte de fichier d’en- tête à la place? Et de cette façon, je peux utiliser la directive de préprocesseur #include pour apporter uniquement les informations nécessaires pour comstackr! “

    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 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 les déclarations de fonction, les définitions de type et les définitions de classe. Les implémentations de fonctions réelles résident dans des unités de traduction, à savoir des fichiers .cpp.

    L’idée sous-jacente est que les fonctions et les fonctions des membres de classe / struct sont compilées et assemblées une seule fois, puis les autres fonctions peuvent appeler ce code à partir d’un endroit sans faire de doublons. Vos prototypes de fonctions sont déclarés comme “extern” implicitement.

     /* Function prototype, usually found in headers. */ /* Implicitly 'extern', ie the symbols 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 la même implémentation plusieurs fois. Donc, vous voulez que toutes vos unités de traduction voient le prototype de fonction mais pas le corps de la fonction .

    Alors, comment tout se mélange-t-il à la fin? C’est le travail du lieur. Un éditeur de liens lit tous les fichiers objects générés par l’étape d’assemblage 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 des 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. Le lieur résout le symbole non résolu en connectant l’unité de traduction qui contient le symbole indéfini avec celui 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 y a deux exceptions notables. Premièrement, si vous avez une petite fonction, vous pouvez la mettre 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. Comme ils sont généralement petits, la taille ne compte pas. Vous pouvez les imaginer statiques dans leur fonctionnement. Il est donc prudent 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 découpler l’implémentation de la définition comme avec des fonctions autonomes ou des classes normales. Eh bien, c’est peut-être possible maintenant, mais la prise en charge étendue du compilateur pour le mot clé “export” a pris beaucoup de temps. Donc, sans la prise en charge de «l’exportation», les unités de traduction obtiennent leurs propres copies locales des types et fonctions des modèles instanciés, de la même manière que fonctionnent les fonctions en ligne. Avec le soutien de «l’exportation», ce n’est pas le cas.

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

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

    • Le préprocesseur est exécuté, ce qui parsing toutes les directives qui commencent par un “#”. La directive #include concatène, par exemple, le fichier inclus avec une version inférieure. Il effectue également le macro-remplacement et le collage de jetons.
    • Le compilateur réel s’exécute sur le fichier texte intermédiaire après la phase de préprocesseur et émet du code assembleur.
    • L’ assembleur s’exécute sur le fichier d’assemblage et émet du code machine. On l’appelle généralement un fichier object et suit le format exécutable binary du système opérationnel en question. Par exemple, Windows utilise le format PE (Portable Executable Format), tandis que Linux utilise le format Unix System V ELF, avec les extensions GNU. A 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 dans chaque unité de traduction dans l’ordre. Cependant, l’étape de l’éditeur de liens fonctionne sur tous les fichiers objects générés 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 binary. Les programmeurs ne sont pas obligés de le savoir en général, mais cela aide sûrement dans certains cas.

    Encore une fois, c’était vraiment plus que ce que vous aviez demandé, mais j’espère que les détails les plus concrets vous aideront à voir la situation dans son ensemble.

    La solution typique consiste à utiliser des fichiers .h pour les déclarations uniquement et les fichiers .cpp pour leur implémentation. Si vous avez besoin de réutiliser l’implémentation, vous devez inclure le fichier .h correspondant dans le fichier .cpp où la classe / fonction / quoi que ce soit est utilisée et liée à un fichier .cpp déjà compilé (fichier .obj – généralement utilisé dans un projet) – ou fichier .lib – généralement utilisé pour la réutilisation de plusieurs projets). De cette façon, vous n’avez pas besoin de tout recomstackr si seules les implémentations changent.

    Considérez les fichiers cpp comme une boîte noire et les fichiers .h comme les guides d’utilisation de ces boîtes noires.

    Les fichiers cpp peuvent être compilés à l’avance. Cela ne fonctionne pas avec vous #include les, car il doit inclure le code dans votre programme chaque fois qu’il le comstack. Si vous incluez simplement l’en-tête, il suffit d’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 car les temps de compilation vont exploser.

    Lisez également ceci: Fichier d’en-tête Inclure les modèles

    Les fichiers d’en-tête contiennent généralement des déclarations de fonctions / classes, alors que les fichiers .cpp contiennent les implémentations réelles. Au moment de la compilation, chaque fichier .cpp est compilé dans un fichier object (généralement l’extension .o), et l’éditeur de liens combine les différents fichiers objects 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 recomstackz l’un des fichiers .cpp de votre projet, vous n’avez pas à recomstackr tous les autres. Vous créez simplement le nouveau fichier object pour ce fichier .cpp particulier. Le compilateur n’a pas à regarder les autres fichiers .cpp. Cependant, si vous voulez 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 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. Donc, il ne sait pas comment les fonctions 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 préoccuper de cela pour l’instant (:

    L’idée de base selon laquelle les en-têtes sont uniquement inclus et les fichiers cpp sont uniquement compilés. Cela deviendra plus utile une fois que vous aurez beaucoup de fichiers cpp et que la recompilation de l’ensemble de l’application lorsque vous en modifierez un seul sera trop lente. Ou lorsque les fonctions dans les fichiers commencent à dépendre les unes des autres. Vous devez donc 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 comstackr les fichiers cpp et lier les fichiers d’object résultants à un programme.

    Si vous #incluez un fichier cpp dans plusieurs autres fichiers de votre programme, le compilateur essaiera de comstackr 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 forcera ensuite la recompilation de tous les fichiers # en les incluant.

    Il suffit de placer vos déclarations dans des fichiers d’en-tête et d’inclure ceux-ci (car ils ne génèrent pas réellement de code en soi), et l’éditeur de liens connectera les déclarations au code cpp correspondant (qui ne sera compilé qu’une seule fois).

    Bien qu’il soit certainement possible de faire comme vous l’avez fait, la pratique courante consiste à placer les déclarations partagées dans les fichiers d’en-tête (.h) et les définitions des fonctions et des variables (implémentation) dans les fichiers source (.cpp).

    En guise de convention, cela permet d’indiquer clairement où tout se trouve et d’établir 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 append quelque chose qui pourrait se casser s’il était défini dans plusieurs unités différentes.

    réutilisation, architecture et encapsulation de données

    voici un exemple:

    disons que vous créez un fichier cpp qui contient une forme simple de routines de chaînes tout dans une classe myssortingng, vous placez la classe decl pour cela dans un fichier myssortingng.h compilant myssortingng.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 fichier myssortingng.obj. pour utiliser myssortingng dans votre programme, vous ne vous souciez pas des détails de la manière dont myssortingng est implémenté puisque l’en-tête indique ce qu’il peut faire

    maintenant, si un copain veut utiliser votre classe myssortingng, vous lui donnez myssortingng.h et le myssortingng.obj, il n’a pas non plus besoin de savoir comment cela fonctionne tant qu’il fonctionne.

    plus tard, si vous avez plus de fichiers .obj, vous pouvez les combiner dans un fichier .lib et y accéder par un lien.

    vous pouvez également décider de changer le fichier myssortingng.cpp et l’implémenter plus efficacement, cela n’affectera pas votre programme main.cpp ou vos amis.

    Si cela fonctionne pour vous, il n’y a rien de mal à cela – sauf que cela va ébranler 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 si c’était un grand projet, c’est ce que l’on appelle l’optimisation prématurée. Selon votre environnement de développement, la configuration d’une configuration permettant de prendre en charge plusieurs fichiers sources par programme peut s’avérer particulièrement complexe.

    Si, au fil du temps, votre projet évolue et que vous constatez que le processus de génération prend trop de temps, vous pouvez modifier votre code pour utiliser plusieurs fichiers sources afin de créer des versions incrémentielles plus rapides.

    Plusieurs des réponses traitent de la séparation de l’interface de la mise en œuvre. Cependant, ce n’est pas une caractéristique inhérente aux fichiers d’inclusion, et il est assez courant d’inclure des fichiers «en-tête» qui incorporent directement leur implémentation (même la bibliothèque standard C ++ le fait de manière significative).

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

    Lorsque vous comstackz et liez un programme, le compilateur comstack d’abord les fichiers cpp individuels, puis les relie (se connecte). Les en-têtes ne seront jamais compilés, sauf s’ils sont 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 omettez la manière dont vous implémentez réellement les détails. De cette façon, vous n’avez pas à recomstackr chaque fichier cpp si vous en modifiez un.

    Je vous suggère de passer par la 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 la séparation des interfaces et des implémentations.

    Les fichiers d’en-tête ont généralement des interfaces qui ne sont pas censées être modifiées si souvent. De même, un regard sur des modèles tels que Virtual Constructor Idiom vous aidera à mieux comprendre le concept.

    J’apprends encore comme toi 🙂

    C’est comme écrire un livre, vous voulez imprimer des chapitres finis 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 rien aux autres.

    Mais inclure les fichiers cpp est, du sharepoint vue du compilateur, comme l’édition de tous les chapitres du livre dans un seul fichier. Ensuite, si vous le modifiez, vous devez imprimer toutes les pages du livre entier pour que votre chapitre révisé soit imprimé. Il n’y a pas d’option “Imprimer les pages sélectionnées” dans la génération de code object.

    Retour au logiciel: Linux et Ruby src traînent. Une mesure approximative de lignes de code …

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

    N’importe laquelle de ces quatre catégories a beaucoup de code, d’où la nécessité de la modularité. Ce type de base de code est étonnamment typique des systèmes du monde réel.