Pourquoi C ++ a-t-il besoin d'un fichier d'en-tête séparé?

138

Je n'ai jamais vraiment compris pourquoi C ++ a besoin d'un fichier d'en-tête séparé avec les mêmes fonctions que dans le fichier .cpp. Cela rend la création de classes et leur refactorisation très difficiles, et cela ajoute des fichiers inutiles au projet. Et puis il y a le problème de devoir inclure des fichiers d'en-tête, mais de vérifier explicitement s'ils ont déjà été inclus.

C ++ a été ratifié en 1998, alors pourquoi est-il conçu de cette façon? Quels sont les avantages d'avoir un fichier d'en-tête séparé?


Question complémentaire:

Comment le compilateur trouve-t-il le fichier .cpp avec le code, alors que tout ce que j'inclus est le fichier .h? Suppose-t-il que le fichier .cpp porte le même nom que le fichier .h ou examine-t-il réellement tous les fichiers de l'arborescence de répertoires?

Marius
la source
2
Si vous souhaitez modifier un seul fichier, vérifiez uniquement lzz (www.lazycplusplus.com).
Richard Corden
3
En double exact: stackoverflow.com/questions/333889 . Proche du doublon: stackoverflow.com/questions/752793
Peter Mortensen

Réponses:

105

Vous semblez demander comment séparer les définitions des déclarations, bien qu'il existe d'autres utilisations des fichiers d'en-tête.

La réponse est que C ++ n'a pas "besoin" de cela. Si vous marquez tout en ligne (ce qui est de toute façon automatique pour les fonctions membres définies dans une définition de classe), alors il n'y a pas besoin de séparation. Vous pouvez simplement tout définir dans les fichiers d'en-tête.

Les raisons pour lesquelles vous pourriez vouloir vous séparer sont:

  1. Pour améliorer les temps de construction.
  2. Pour créer un lien avec du code sans avoir la source des définitions.
  3. Pour éviter de tout marquer "en ligne".

Si votre question plus générale est «pourquoi le C ++ n'est-il pas identique à Java?», Alors je dois vous demander: «pourquoi écrivez-vous C ++ au lieu de Java? ;-p

Plus sérieusement, cependant, la raison est que le compilateur C ++ ne peut pas simplement accéder à une autre unité de traduction et comprendre comment utiliser ses symboles, de la manière que javac peut et fait. Le fichier d'en-tête est nécessaire pour déclarer au compilateur ce qu'il peut espérer être disponible au moment de la liaison.

Il en #includeva de même pour une substitution textuelle directe. Si vous définissez tout dans les fichiers d'en-tête, le préprocesseur finit par créer un énorme copier-coller de chaque fichier source de votre projet, et l'introduire dans le compilateur. Le fait que la norme C ++ ait été ratifiée en 1998 n'a rien à voir avec cela, c'est le fait que l'environnement de compilation pour C ++ est si étroitement basé sur celui de C.

Conversion de mes commentaires pour répondre à votre question de suivi:

Comment le compilateur trouve-t-il le fichier .cpp avec le code qu'il contient

Ce n'est pas le cas, du moins pas au moment où il compile le code qui utilisait le fichier d'en-tête. Les fonctions que vous liez n'ont même pas besoin d'avoir été écrites encore, peu importe que le compilateur sache dans quel .cppfichier elles se trouveront. Tout ce que le code appelant doit savoir au moment de la compilation est exprimé dans la déclaration de fonction. Au moment de la liaison, vous fournissez une liste de .ofichiers, ou de bibliothèques statiques ou dynamiques, et l'en-tête en fait est une promesse que les définitions des fonctions seront là quelque part.

Steve Jessop
la source
3
Pour ajouter à "Les raisons pour lesquelles vous pourriez vouloir séparer sont:" & Je pense que la fonction la plus importante des fichiers d'en-tête est: Pour séparer la conception de la structure du code de l'implémentation, Parce que: A. Lorsque vous entrez dans des structures vraiment compliquées qui impliquent de nombreux objets, cela Il est beaucoup plus facile de passer au crible les fichiers d'en-tête et de se rappeler comment ils fonctionnent ensemble, complétés par vos commentaires d'en-tête. B. Une personne s'est occupée de définir toute la structure de l'objet et une autre s'est occupée de la mise en œuvre, elle maintient les choses organisées. Dans l'ensemble, je pense que cela rend le code complexe plus lisible.
Andres Canella
De la manière la plus simple, je peux penser à l'utilité de la séparation des fichiers d'en-tête et des fichiers cpp est de séparer l'interface et les implémentations, ce qui aide vraiment pour les projets de taille moyenne / grande.
Krishna Oza
J'avais l'intention que ce soit inclus dans (2), "lien contre code sans avoir les définitions". OK, vous pouvez donc toujours avoir accès au repo git avec les définitions dans, mais le fait est que vous pouvez compiler votre code séparément des implémentations de ses dépendances, puis lier. Si littéralement tout ce que vous vouliez était de séparer l'interface / l'implémentation en différents fichiers, sans vous soucier de la construction séparément, alors vous pourriez le faire en ayant simplement foo_interface.h inclure foo_implementation.h.
Steve Jessop
4
@AndresCanella Non, ce n'est pas le cas. Cela fait de la lecture et de la maintenance d'un code qui n'est pas le vôtre un cauchemar. Pour bien comprendre ce que fait quelque chose dans le code, vous devez parcourir 2n fichiers au lieu de n fichiers. Ce n'est tout simplement pas une notation Big-Oh, 2n fait une grande différence par rapport à juste n.
Błażej Michalik
1
Je seconde que c'est un mensonge que les en-têtes aident. vérifier la source minix par exemple, il est si difficile de suivre où il commence, où le contrôle est passé, où les choses sont déclarées / définies .. s'il était construit via des modules dynamiques séparés, il serait digeste en donnant un sens à une chose puis en sautant à un module de dépendance. au lieu de cela, vous devez suivre les en-têtes et cela rend la lecture de tout code écrit de cette façon un enfer. en revanche, nodejs indique clairement d'où vient ce qui vient sans ifdefs, et vous pouvez facilement identifier d'où vient ce qui vient.
Dmitry
91

C ++ le fait de cette façon parce que C l'a fait de cette façon, donc la vraie question est de savoir pourquoi C l'a fait de cette façon? Wikipédia en parle un peu.

Les nouveaux langages compilés (tels que Java, C #) n'utilisent pas de déclarations directes; les identificateurs sont reconnus automatiquement à partir des fichiers source et lus directement à partir des symboles de bibliothèque dynamiques. Cela signifie que les fichiers d'en-tête ne sont pas nécessaires.

Donald Byrd
la source
13
+1 Frappe le clou sur la tête. Cela ne nécessite vraiment pas d'explication détaillée.
MSalters
6
Cela n'a pas frappé mon ongle sur la tête: (Je dois encore chercher pourquoi C ++ doit utiliser des déclarations directes et pourquoi il ne peut pas reconnaître les identificateurs des fichiers source et lire directement à partir des symboles de bibliothèque dynamiques, et pourquoi C ++ l'a fait façon juste parce que C l'a fait de cette façon: p
Alexander Taylor
3
Et vous êtes un meilleur programmeur pour l'avoir fait @AlexanderTaylor :)
Donald Byrd
66

Certaines personnes considèrent les fichiers d'en-tête comme un avantage:

  • On prétend qu'il permet / applique / permet la séparation de l'interface et de l'implémentation - mais ce n'est généralement pas le cas. Les fichiers d'en-tête sont pleins de détails d'implémentation (par exemple, les variables membres d'une classe doivent être spécifiées dans l'en-tête, même si elles ne font pas partie de l'interface publique), et les fonctions peuvent, et sont souvent, définies en ligne dans la déclaration de classe dans l'en-tête, détruisant à nouveau cette séparation.
  • On dit parfois qu'il améliore le temps de compilation car chaque unité de traduction peut être traitée indépendamment. Et pourtant, C ++ est probablement le langage le plus lent en matière de compilation. Une partie de la raison est les nombreuses nombreuses inclusions répétées du même en-tête. Un grand nombre d'en-têtes sont inclus par plusieurs unités de traduction, ce qui nécessite leur analyse plusieurs fois.

En fin de compte, le système d'en-tête est un artefact des années 70 lorsque C a été conçu. À l'époque, les ordinateurs avaient très peu de mémoire et garder le module entier en mémoire n'était tout simplement pas une option. Un compilateur devait commencer à lire le fichier en haut, puis procéder linéairement à travers le code source. Le mécanisme d'en-tête permet cela. Le compilateur n'a pas besoin de prendre en compte d'autres unités de traduction, il doit simplement lire le code de haut en bas.

Et C ++ a conservé ce système pour une compatibilité ascendante.

Aujourd'hui, cela n'a aucun sens. Il est inefficace, sujet aux erreurs et trop compliqué. Il existe de bien meilleurs moyens de séparer l'interface et l'implémentation, si tel était l'objectif.

Cependant, l'une des propositions pour C ++ 0x était d'ajouter un système de modules approprié, permettant au code d'être compilé comme .NET ou Java, en modules plus grands, le tout en une seule fois et sans en-têtes. Cette proposition n'a pas fait la coupe dans C ++ 0x, mais je crois qu'elle est toujours dans la catégorie «nous aimerions faire ça plus tard». Peut-être dans un TR2 ou similaire.

jalf
la source
CECI est la meilleure réponse sur la page. Je vous remercie!
Chuck Le Butt
29

À ma compréhension (limitée - je ne suis pas un développeur C normalement), ceci est enraciné dans C. Rappelez-vous que C ne sait pas ce que sont les classes ou les espaces de noms, c'est juste un long programme. De plus, les fonctions doivent être déclarées avant de les utiliser.

Par exemple, ce qui suit doit donner une erreur du compilateur:

void SomeFunction() {
    SomeOtherFunction();
}

void SomeOtherFunction() {
    printf("What?");
}

L'erreur devrait être que "SomeOtherFunction n'est pas déclaré" parce que vous l'appelez avant sa déclaration. Une façon de résoudre ce problème consiste à déplacer SomeOtherFunction au-dessus de SomeFunction. Une autre approche consiste à déclarer d'abord la signature des fonctions:

void SomeOtherFunction();

void SomeFunction() {
    SomeOtherFunction();
}

void SomeOtherFunction() {
    printf("What?");
}

Cela permet au compilateur de savoir: Regardez quelque part dans le code, il y a une fonction appelée SomeOtherFunction qui renvoie void et ne prend aucun paramètre. Donc, si vous rencontrez du code qui tente d'appeler SomeOtherFunction, ne paniquez pas et allez plutôt le chercher.

Maintenant, imaginez que vous avez SomeFunction et SomeOtherFunction dans deux fichiers .c différents. Vous devez ensuite #inclure "SomeOther.c" dans Some.c. Maintenant, ajoutez quelques fonctions "privées" à SomeOther.c. Comme C ne connaît pas les fonctions privées, cette fonction serait également disponible dans Some.c.

C'est là que les fichiers .h entrent en jeu: ils spécifient toutes les fonctions (et variables) que vous souhaitez «exporter» à partir d'un fichier .c accessible dans d'autres fichiers .c. De cette façon, vous gagnez quelque chose comme une portée publique / privée. De plus, vous pouvez donner ce fichier .h à d'autres personnes sans avoir à partager votre code source - les fichiers .h fonctionnent également avec les fichiers .lib compilés.

Donc, la raison principale est vraiment pour la commodité, pour la protection du code source et pour avoir un peu de découplage entre les parties de votre application.

C'était C cependant. C ++ a introduit les classes et les modificateurs privés / publics, donc même si vous pouvez toujours demander s'ils sont nécessaires, C ++ AFAIK nécessite toujours la déclaration des fonctions avant de les utiliser. De plus, de nombreux développeurs C ++ sont ou étaient également des développeurs C et ont repris leurs concepts et leurs habitudes en C ++ - pourquoi changer ce qui n'est pas cassé?

Michael Stum
la source
5
Pourquoi le compilateur ne peut-il pas parcourir le code et trouver toutes les définitions de fonction? Cela semble être quelque chose qui serait assez facile à programmer dans le compilateur.
Marius
3
Si vous avez la source, ce que vous n'avez souvent pas. Le C ++ compilé est en fait un code machine avec juste assez d'informations supplémentaires pour charger et lier le code. Ensuite, vous pointez le processeur sur le point d'entrée et laissez-le fonctionner. Ceci est fondamentalement différent de Java ou C #, où le code est compilé dans un bytecode intermédiaire contenant des métadonnées sur son contenu.
DevSolar
J'imagine qu'en 1972, cela aurait pu être une opération assez coûteuse pour le compilateur.
Michael Stum
Exactement ce que Michael Stum a dit. Lorsque ce comportement a été défini, un balayage linéaire à travers une seule unité de traduction était la seule chose qui pouvait être pratiquement implémentée.
jalf
3
Ouais - compiler sur un 16 amer avec un torage de masse de bande n'est pas trivial.
MSalters
11

Premier avantage: si vous n'avez pas de fichiers d'en-tête, vous devrez inclure des fichiers source dans d'autres fichiers source. Cela entraînerait la recompilation des fichiers inclus lorsque le fichier inclus changerait.

Deuxième avantage: il permet de partager les interfaces sans partager le code entre différentes unités (différents développeurs, équipes, entreprises etc.)

élève
la source
1
Est-ce que vous sous-entendez que, par exemple en C #, vous devrez inclure des fichiers source dans d'autres fichiers source? Parce que évidemment vous ne le faites pas. Pour le deuxième avantage, je pense que cela dépend trop de la langue: vous n'utiliserez pas de fichiers .h dans par exemple Delphi
Vlagged
De toute façon, vous devez recompiler l'ensemble du projet, alors le premier avantage compte-t-il vraiment?
Marius
ok, mais je ne pense pas qu'une fonctionnalité de langue. Il est plus pratique de traiter la déclaration C avant la définition de «problème». C'est comme si quelqu'un de célèbre disait "ce n'est pas un bug, c'est une fonctionnalité" :)
neuro
@Marius: Oui, ça compte vraiment. Lier l'ensemble du projet est différent de la compilation et de la liaison de l'ensemble du projet. Et à mesure que le nombre de fichiers dans le projet augmente, la compilation de tous devient vraiment ennuyeuse. @Vlagged: Vous avez raison, mais je n'ai pas comparé C ++ avec un autre langage. J'ai comparé en utilisant uniquement des fichiers source et en utilisant des fichiers source et en-tête.
erelender
C # n'inclut pas les fichiers source dans les autres, mais vous devez toujours référencer les modules - et cela oblige le compilateur à extraire les fichiers source (ou à les refléter dans le binaire) pour analyser les symboles que votre code utilise.
gbjbaanb
5

Le besoin de fichiers d'en-tête résulte des limitations que le compilateur a pour connaître les informations de type pour les fonctions et / ou les variables dans d'autres modules. Le programme ou la bibliothèque compilé n'inclut pas les informations de type requises par le compilateur pour se lier à des objets définis dans d'autres unités de compilation.

Afin de compenser cette limitation, C et C ++ autorisent les déclarations et ces déclarations peuvent être incluses dans des modules qui les utilisent à l'aide de la directive #include du préprocesseur.

Les langages comme Java ou C #, quant à eux, incluent les informations nécessaires pour la liaison dans la sortie du compilateur (fichier de classe ou assembly). Par conséquent, il n'est plus nécessaire de maintenir les déclarations autonomes à inclure par les clients d'un module.

La raison pour laquelle les informations de liaison ne sont pas incluses dans la sortie du compilateur est simple: elles ne sont pas nécessaires au moment de l'exécution (toute vérification de type se produit au moment de la compilation). Cela ne ferait que perdre de l'espace. N'oubliez pas que le C / C ++ vient d'une époque où la taille d'un exécutable ou d'une bibliothèque importait un peu.

VoidPointer
la source
Je suis d'accord avec toi. J'ai eu une idée similaire ici: stackoverflow.com/questions/3702132/…
smwikipedia
4

C ++ a été conçu pour ajouter des fonctionnalités de langage de programmation moderne à l'infrastructure C, sans rien changer inutilement à C qui ne concerne pas spécifiquement le langage lui-même.

Oui, à ce stade (10 ans après le premier standard C ++ et 20 ans après le début de son utilisation), il est facile de se demander pourquoi il n'a pas un système de modules approprié. De toute évidence, tout nouveau langage conçu aujourd'hui ne fonctionnerait pas comme C ++. Mais ce n'est pas le but du C ++.

Le but du C ++ est d'être évolutif, une continuation fluide de la pratique existante, en ajoutant uniquement de nouvelles capacités sans (trop souvent) casser des choses qui fonctionnent correctement pour sa communauté d'utilisateurs.

Cela signifie que cela rend certaines choses plus difficiles (en particulier pour les personnes qui démarrent un nouveau projet), et certaines choses plus faciles (en particulier pour ceux qui maintiennent le code existant) que d'autres langages.

Donc, plutôt que de s'attendre à ce que C ++ se transforme en C # (ce qui serait inutile car nous avons déjà C #), pourquoi ne pas simplement choisir le bon outil pour le travail? Moi-même, je m'efforce d'écrire des morceaux importants de nouvelles fonctionnalités dans un langage moderne (il se trouve que j'utilise C #), et j'ai une grande quantité de C ++ existant que je garde en C ++ car il n'y aurait aucune valeur réelle à le réécrire tout. Ils s'intègrent très bien de toute façon, donc c'est largement indolore.

Daniel Earwicker
la source
Comment intégrez-vous C # et C ++? Par COM?
Peter Mortensen
1
Il existe trois méthodes principales, la «meilleure» dépend de votre code existant. J'ai utilisé les trois. Celui que j'utilise le plus est COM parce que mon code existant a déjà été conçu autour de lui, donc il est pratiquement transparent, fonctionne très bien pour moi. Dans certains endroits étranges, j'utilise C ++ / CLI, ce qui donne une intégration incroyablement fluide pour toute situation où vous ne disposez pas déjà d'interfaces COM (et vous pouvez préférer utiliser les interfaces COM existantes même si vous les avez). Enfin, il existe p / invoke qui vous permet d'appeler n'importe quelle fonction de type C exposée à partir d'une DLL, ce qui vous permet d'appeler directement n'importe quelle API Win32 à partir de C #.
Daniel Earwicker
4

Eh bien, C ++ a été ratifié en 1998, mais il était utilisé depuis bien plus longtemps que cela, et la ratification fixait principalement l'utilisation actuelle plutôt qu'une structure imposante. Et comme C ++ était basé sur C et que C a des fichiers d'en-tête, C ++ les a aussi.

La principale raison des fichiers d'en-tête est de permettre la compilation séparée des fichiers et de minimiser les dépendances.

Disons que j'ai foo.cpp et que je souhaite utiliser le code des fichiers bar.h / bar.cpp.

Je peux #inclure "bar.h" dans foo.cpp, puis programmer et compiler foo.cpp même si bar.cpp n'existe pas. Le fichier d'en-tête agit comme une promesse au compilateur que les classes / fonctions de bar.h existeront au moment de l'exécution, et il a déjà tout ce qu'il doit savoir.

Bien sûr, si les fonctions de bar.h n'ont pas de corps lorsque j'essaye de lier mon programme, alors il ne liera pas et j'obtiendrai une erreur.

Un effet secondaire est que vous pouvez donner aux utilisateurs un fichier d'en-tête sans révéler votre code source.

Un autre est que si vous modifiez l'implémentation de votre code dans le fichier * .cpp, mais ne modifiez pas du tout l'en-tête, vous n'avez qu'à compiler le fichier * .cpp au lieu de tout ce qui l'utilise. Bien sûr, si vous mettez beaucoup d'implémentation dans le fichier d'en-tête, cela devient moins utile.

Mark Krenitsky
la source
3

Il n'a pas besoin d'un fichier d'en-tête séparé avec les mêmes fonctions que dans main. Il n'en a besoin que si vous développez une application utilisant plusieurs fichiers de code et si vous utilisez une fonction qui n'a pas été déclarée auparavant.

C'est vraiment un problème de portée.

Alex Rodrigues
la source
1

C ++ a été ratifié en 1998, alors pourquoi est-il conçu de cette façon? Quels sont les avantages d'avoir un fichier d'en-tête séparé?

En fait, les fichiers d'en-tête deviennent très utiles lors de l'examen des programmes pour la première fois, l'extraction des fichiers d'en-tête (en utilisant uniquement un éditeur de texte) vous donne un aperçu de l'architecture du programme, contrairement à d'autres langages où vous devez utiliser des outils sophistiqués pour afficher les classes et leurs fonctions de membre.

Diaa Sami
la source
1

Je pense que la vraie raison (historique) derrière les fichiers d' en- tête a été fait comme facile pour les développeurs de compilateur ... mais, les fichiers en- tête ne donne des avantages.
Consultez ce post précédent pour plus de discussions ...

Paolo Tedesco
la source
1

Eh bien, vous pouvez parfaitement développer C ++ sans fichiers d'en-tête. En fait, certaines bibliothèques qui utilisent de manière intensive des modèles n'utilisent pas le paradigme des fichiers d'en-tête / code (voir boost). Mais en C / C ++, vous ne pouvez pas utiliser quelque chose qui n'est pas déclaré. Un moyen pratique de résoudre ce problème consiste à utiliser des fichiers d'en-tête. De plus, vous bénéficiez de l'avantage de partager l'interface sans partager le code / l'implémentation. Et je pense que cela n'a pas été envisagé par les créateurs du C: lorsque vous utilisez des fichiers d'en-tête partagés, vous devez utiliser le fameux:

#ifndef MY_HEADER_SWEET_GUARDIAN
#define MY_HEADER_SWEET_GUARDIAN

// [...]
// my header
// [...]

#endif // MY_HEADER_SWEET_GUARDIAN

ce n'est pas vraiment une fonctionnalité de langage, mais un moyen pratique de gérer l'inclusion multiple.

Donc, je pense que lorsque C a été créé, les problèmes de déclaration forward ont été sous-estimés et maintenant, lorsque nous utilisons un langage de haut niveau comme C ++, nous devons faire face à ce genre de choses.

Un autre fardeau pour nous, pauvres utilisateurs de C ++ ...

neuro
la source
1

Si vous voulez que le compilateur trouve automatiquement les symboles définis dans d'autres fichiers, vous devez forcer le programmeur à placer ces fichiers dans des emplacements prédéfinis (comme la structure des packages Java détermine la structure des dossiers du projet). Je préfère les fichiers d'en-tête. Vous aurez également besoin des sources des bibliothèques que vous utilisez ou d'un moyen uniforme de mettre les informations nécessaires au compilateur dans des binaires.

Tadeusz Kopec
la source