Pourquoi avoir des fichiers d'en-tête et des fichiers .cpp? [fermé]

484

Pourquoi C ++ a-t-il des fichiers d'en-tête et des fichiers .cpp?

Peter Mortensen
la source
3
Question connexe: stackoverflow.com/questions/1945846/…
Spoike
c'est un paradigme OOP commun, .h est une déclaration de classe et cpp en est la définition.
Manish Kakati
C'est la meilleure partie de l'interface de séparation c ++ de l'implémentation. C'est toujours bien plutôt que de garder tout le code dans un seul fichier, nous avons une interface séparée. Une certaine quantité de code est toujours là comme la fonction en ligne qui fait partie des fichiers d'en-tête. A l'air bien quand un fichier d'en-tête est vu affiche la liste des fonctions déclarées et des variables de classe.
Miank
Il y a des moments où les fichiers d'en-tête sont essentiels pour la compilation - pas seulement une préférence d'organisation ou un moyen de distribuer des bibliothèques précompilées. Supposons que vous ayez une structure où game.c dépend à la fois de physics.c et de math.c; physics.c dépend également de math.c. Si vous incluez des fichiers .c et oubliez les fichiers .h pour toujours, vous aurez des déclarations en double de math.c et aucun espoir de compilation. C'est ce qui est le plus logique pour moi pourquoi les fichiers d'en-tête sont importants. J'espère que cela aide quelqu'un d'autre.
Samy Bencherif du
Je pense que cela a à voir avec le fait que seuls les caractères alphanumériques sont autorisés dans les extensions. Je ne sais même pas si c'est vrai, juste deviner
user12211554

Réponses:

202

Eh bien, la principale raison serait de séparer l'interface de l'implémentation. L'en-tête déclare "ce que" fera une classe (ou quoi que ce soit en cours d'implémentation), tandis que le fichier cpp définit "comment" il exécutera ces fonctionnalités.

Cela réduit les dépendances de sorte que le code qui utilise l'en-tête n'a pas nécessairement besoin de connaître tous les détails de l'implémentation et toutes les autres classes / en-têtes nécessaires uniquement pour cela. Cela réduira les temps de compilation et également la quantité de recompilation nécessaire lorsqu'un élément de l'implémentation change.

Ce n'est pas parfait, et vous auriez généralement recours à des techniques comme l' idiome Pimpl pour séparer correctement l'interface et la mise en œuvre, mais c'est un bon début.

MadKeithV
la source
178
Pas vraiment vrai. L'en-tête contient toujours une partie importante de l'implémentation. Depuis quand les variables d'instance privées font-elles partie de l'interface d'une classe? Fonctions de membre privé? Alors que diable font-ils dans l'en-tête publiquement visible? Et il se désagrège davantage avec les modèles.
jalf
13
C'est pourquoi j'ai dit que ce n'était pas parfait, et l'idiome Pimpl est nécessaire pour plus de séparation. Les modèles sont une toute autre boîte de vers - même si le mot-clé "exports" était entièrement pris en charge par la plupart des compilateurs, il me resterait du sucre syntaxique plutôt qu'une véritable séparation.
Joris Timmermans
4
Comment les autres langues gèrent-elles cela? par exemple - Java? Il n'y a pas de concept de fichier d'en-tête en Java.
Lazer
8
@Lazer: Java est plus simple à analyser. Le compilateur Java peut analyser un fichier sans connaître toutes les classes des autres fichiers et vérifier les types ultérieurement. En C ++, beaucoup de constructions sont ambiguës sans informations de type, donc le compilateur C ++ a besoin d'informations sur les types référencés pour analyser un fichier. C'est pourquoi il a besoin d'en-têtes.
Niki
15
@nikie: Qu'est-ce que la "facilité" d'analyse a à voir avec cela? Si Java avait une grammaire au moins aussi complexe que C ++, il pourrait toujours utiliser des fichiers java. Dans les deux cas, qu'en est-il de C? C est facile à analyser, mais utilise à la fois des en-têtes et des fichiers c.
Thomas Eding
609

Compilation C ++

Une compilation en C ++ se fait en 2 phases majeures:

  1. Le premier est la compilation de fichiers texte "source" en fichiers "objets" binaires: le fichier CPP est le fichier compilé et est compilé sans aucune connaissance des autres fichiers CPP (ou même des bibliothèques), à moins qu'il ne soit alimenté par une déclaration brute ou inclusion d'en-tête. Le fichier CPP est généralement compilé dans un fichier .OBJ ou .O "objet".

  2. La seconde est la liaison entre tous les fichiers "objet", et donc la création du fichier binaire final (soit une bibliothèque soit un exécutable).

Où se situe le HPP dans tout ce processus?

Un pauvre fichier CPP solitaire ...

La compilation de chaque fichier CPP est indépendante de tous les autres fichiers CPP, ce qui signifie que si A.CPP a besoin d'un symbole défini dans B.CPP, comme:

// A.CPP
void doSomething()
{
   doSomethingElse(); // Defined in B.CPP
}

// B.CPP
void doSomethingElse()
{
   // Etc.
}

Il ne sera pas compilé car A.CPP n'a aucun moyen de savoir que "doSomethingElse" existe ... Sauf s'il y a une déclaration dans A.CPP, comme:

// A.CPP
void doSomethingElse() ; // From B.CPP

void doSomething()
{
   doSomethingElse() ; // Defined in B.CPP
}

Ensuite, si vous avez C.CPP qui utilise le même symbole, vous copiez / collez ensuite la déclaration ...

ALERTE COPIE / COLLAGE!

Oui, il y a un problème. Les copies / pâtes sont dangereuses et difficiles à entretenir. Ce qui signifie que ce serait cool si nous avions un moyen de NE PAS copier / coller, et de toujours déclarer le symbole ... Comment pouvons-nous le faire? Par l'inclusion d'un fichier texte, qui est généralement suffixé par .h, .hxx, .h ++ ou, mon préféré pour les fichiers C ++, .hpp:

// B.HPP (here, we decided to declare every symbol defined in B.CPP)
void doSomethingElse() ;

// A.CPP
#include "B.HPP"

void doSomething()
{
   doSomethingElse() ; // Defined in B.CPP
}

// B.CPP
#include "B.HPP"

void doSomethingElse()
{
   // Etc.
}

// C.CPP
#include "B.HPP"

void doSomethingAgain()
{
   doSomethingElse() ; // Defined in B.CPP
}

Comment ça includemarche?

L'inclusion d'un fichier analysera, puis copiera-collera son contenu dans le fichier CPP.

Par exemple, dans le code suivant, avec l'en-tête A.HPP:

// A.HPP
void someFunction();
void someOtherFunction();

... la source B.CPP:

// B.CPP
#include "A.HPP"

void doSomething()
{
   // Etc.
}

... deviendra après inclusion:

// B.CPP
void someFunction();
void someOtherFunction();

void doSomething()
{
   // Etc.
}

Une petite chose - pourquoi inclure B.HPP dans B.CPP?

Dans le cas actuel, cela n'est pas nécessaire, et B.HPP a la doSomethingElsedéclaration de fonction, et B.CPP a la doSomethingElsedéfinition de fonction (qui est, en soi, une déclaration). Mais dans un cas plus général, où B.HPP est utilisé pour les déclarations (et le code en ligne), il pourrait ne pas y avoir de définition correspondante (par exemple, des énumérations, des structures simples, etc.), donc l'inclusion pourrait être nécessaire si B.CPP utilise ces déclarations de B.HPP. Dans l'ensemble, il est "de bon goût" qu'une source inclue par défaut son en-tête.

Conclusion

Le fichier d'en-tête est donc nécessaire, car le compilateur C ++ ne peut pas rechercher seul les déclarations de symboles, et donc, vous devez l'aider en incluant ces déclarations.

Un dernier mot: vous devez placer des protections d'en-tête autour du contenu de vos fichiers HPP, pour être sûr que plusieurs inclusions ne cassent rien, mais dans l'ensemble, je pense que la principale raison de l'existence des fichiers HPP est expliquée ci-dessus.

#ifndef B_HPP_
#define B_HPP_

// The declarations in the B.hpp file

#endif // B_HPP_

ou encore plus simple

#pragma once

// The declarations in the B.hpp file
paercebal
la source
2
@nimcap:: You still have to copy paste the signature from header file to cpp file, don't you?Pas besoin. Tant que le CPP "inclut" le HPP, le précompilateur fera automatiquement le copier-coller du contenu du fichier HPP dans le fichier CPP. J'ai mis à jour la réponse pour clarifier cela.
paercebal
7
@Bob: While compiling A.cpp, compiler knows the types of arguments and return value of doSomethingElse from the call itself. Non, non. Il ne connaît que les types fournis par l'utilisateur, qui, la moitié du temps, ne prendront même pas la peine de lire la valeur de retour. Ensuite, des conversions implicites se produisent. Et puis, quand vous avez le code:, foo(bar)vous ne pouvez même pas être sûr qu'il foos'agit d'une fonction. Le compilateur doit donc avoir accès aux informations contenues dans les en-têtes pour décider si la source se compile correctement ou non ... Ensuite, une fois le code compilé, l'éditeur de liens reliera simplement les appels de fonctions.
paercebal
3
@Bob: [continuer] ... Maintenant, l'éditeur de liens pourrait faire le travail effectué par le compilateur, je suppose, ce qui rendrait alors votre option possible. (Je suppose que c'est le sujet de la proposition "modules" pour la prochaine norme). Seems, they're just a pretty ugly arbitrary design.: Si C ++ avait été créé en 2012, en effet. Mais rappelez-vous que C ++ a été construit sur C dans les années 1980, et à cette époque, les contraintes étaient assez différentes à cette époque (IIRC, il a été décidé à des fins d'adoption de conserver les mêmes linkers que C).
paercebal
1
@paercebal Merci pour l'explication et les notes, paercebal! Pourquoi ne puis-je pas être sûr, c'est foo(bar)une fonction - si elle est obtenue comme un pointeur? En fait, en parlant de mauvaise conception, je blâme C, pas C ++. Je n'aime vraiment pas certaines contraintes de C pur, comme avoir des fichiers d'en-tête ou avoir des fonctions renvoyant une et une seule valeur, tout en prenant plusieurs arguments en entrée (ne semble-t-il pas naturel que l'entrée et la sortie se comportent de la même manière ; pourquoi plusieurs arguments, mais une seule sortie?) :)
Boris Burkov
1
@Bobo:: Why can't I be sure, that foo(bar) is a functionfoo pourrait être un type, donc vous auriez un constructeur de classe appelé. In fact, speaking of bad design, I blame C, not C++: Je peux blâmer C pour beaucoup de choses, mais avoir été conçu dans les années 70 n'en fera pas partie. Encore une fois, les contraintes de cette époque ... such as having header files or having functions return one and only one value: Les tuples peuvent aider à atténuer cela, ainsi qu'à transmettre des arguments par référence. Maintenant, quelle serait la syntaxe pour récupérer plusieurs valeurs retournées, et cela vaudrait-il la peine de changer la langue?
paercebal
93

Parce que C, à l'origine du concept, a 30 ans, et à l'époque, c'était le seul moyen viable de lier le code de plusieurs fichiers.

Aujourd'hui, c'est un hack horrible qui détruit totalement le temps de compilation en C ++, provoque d'innombrables dépendances inutiles (car les définitions de classe dans un fichier d'en-tête exposent trop d'informations sur l'implémentation), etc.

jalf
la source
3
Je me demande pourquoi les fichiers d'en-tête (ou tout ce qui était réellement nécessaire pour la compilation / liaison) n'étaient pas simplement "générés automatiquement"?
Mateen Ulhaq
54

Parce qu'en C ++, le code exécutable final ne porte aucune information de symbole, c'est du code machine plus ou moins pur.

Ainsi, vous avez besoin d'un moyen de décrire l'interface d'un morceau de code, qui est distinct du code lui-même. Cette description se trouve dans le fichier d'en-tête.

se détendre
la source
16

Parce que C ++ les a hérités de C. Malheureusement.

andref
la source
4
Pourquoi l'héritage de C ++ de C est regrettable?
Lokesh
3
@Lokesh À cause de ses bagages :(
陳 力
1
Comment cela peut-il être une réponse?
Shuvo Sarker
14
@ShuvoSarker car, comme l'ont démontré des milliers de langages, il n'y a pas d'explication technique pour que C ++ oblige les programmeurs à écrire deux fois les signatures de fonction. La réponse à "pourquoi?" c'est "l'histoire".
Boris
15

Parce que les personnes qui ont conçu le format de bibliothèque ne voulaient pas "gaspiller" de l'espace pour des informations rarement utilisées comme les macros de préprocesseur C et les déclarations de fonctions.

Puisque vous avez besoin de ces informations pour dire à votre compilateur "cette fonction est disponible plus tard lorsque l'éditeur de liens fait son travail", ils ont dû créer un deuxième fichier où ces informations partagées pourraient être stockées.

La plupart des langages après C / C ++ stockent ces informations dans la sortie (Java bytecode, par exemple) ou n'utilisent pas du tout un format précompilé, sont toujours distribués sous forme source et compilent à la volée (Python, Perl).

Aaron Digulla
la source
Ne fonctionnerait pas, références cycliques. Ieyou ne peut pas construire a.lib à partir de a.cpp avant de construire b.lib à partir de b.cpp, mais vous ne pouvez pas non plus construire b.lib avant a.lib.
MSalters
20
Java a résolu cela, Python peut le faire, n'importe quel langage moderne peut le faire. Mais au moment où C a été inventé, la RAM était si chère et si rare qu'elle n'était tout simplement pas une option.
Aaron Digulla
6

C'est la manière de préprocesseur de déclarer des interfaces. Vous mettez l'interface (déclarations de méthode) dans le fichier d'en-tête et l'implémentation dans le cpp. Les applications utilisant votre bibliothèque doivent seulement connaître l'interface, à laquelle elles peuvent accéder via #include.

Martin c. Löwis
la source
4

Souvent, vous voudrez avoir une définition d'une interface sans avoir à expédier le code entier. Par exemple, si vous avez une bibliothèque partagée, vous expédieriez un fichier d'en-tête qui définit toutes les fonctions et symboles utilisés dans la bibliothèque partagée. Sans fichiers d'en-tête, vous devez envoyer la source.

Dans un même projet, les fichiers d'en-tête sont utilisés, à mon humble avis, à au moins deux fins:

  • Clarté, c'est-à-dire qu'en gardant les interfaces séparées de l'implémentation, il est plus facile de lire le code
  • Compiler le temps. En utilisant uniquement l'interface lorsque cela est possible, au lieu de l'implémentation complète, le temps de compilation peut être réduit car le compilateur peut simplement faire référence à l'interface au lieu d'avoir à analyser le code réel (ce qui, théoriquement, ne devrait être fait que une seule fois).
utilisateur21037
la source
3
Pourquoi les fournisseurs de bibliothèques ne pouvaient-ils pas simplement envoyer un fichier "en-tête" généré? Un fichier "en-tête" libre de pré-processeur devrait donner de bien meilleures performances (à moins que l'implémentation ne soit vraiment cassée).
Tom Hawtin - tackline
Je pense que ce n'est pas pertinent si le fichier d'en-tête est généré ou écrit à la main, la question n'était pas "pourquoi les gens écrivent-ils eux-mêmes les fichiers?", C'était "pourquoi avons-nous des fichiers d'en-tête". Il en va de même pour les en-têtes sans préprocesseur. Bien sûr, ce serait plus rapide.
-5

En réponse à la réponse de MadKeithV ,

Cela réduit les dépendances de sorte que le code qui utilise l'en-tête n'a pas nécessairement besoin de connaître tous les détails de l'implémentation et toutes les autres classes / en-têtes nécessaires uniquement pour cela. Cela réduira les temps de compilation, ainsi que la quantité de recompilation nécessaire lorsque quelque chose dans l'implémentation change.

Une autre raison est qu'un en-tête donne un identifiant unique à chaque classe.

Donc, si nous avons quelque chose comme

class A {..};
class B : public A {...};

class C {
    include A.cpp;
    include B.cpp;
    .....
};

Nous aurons des erreurs, lorsque nous essaierons de construire le projet, puisque A fait partie de B, avec des en-têtes nous éviterions ce genre de maux de tête ...

Alex v
la source