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 ajoute des fichiers inutiles au projet. Et puis il y a le problème d'avoir à inclure des fichiers d'en-tête, mais de vérifier explicitement s'il a déjà été inclus.
Le 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é?
Comment le compilateur trouve-t-il le fichier .cpp avec le code qu'il contient, 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?
Vous semblez demander comment séparer les définitions des déclarations, bien qu'il existe d'autres utilisations pour les fichiers d'en-tête.
La réponse est que C++ n'en a pas "besoin". 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), la séparation n'est pas nécessaire. Vous pouvez simplement tout définir dans les fichiers d'en-tête.
Les raisons pour lesquelles vous pouvez vouloir séparer sont:
Si votre question plus générale est "pourquoi le C++ n'est-il pas identique à Java?", Alors je dois demander "pourquoi écrivez-vous le C++ au lieu de Java?" ;-p
Plus sérieusement, cependant, la raison en est que le compilateur C++ ne peut pas simplement atteindre une autre unité de traduction et comprendre comment utiliser ses symboles, de la même manière que javac le peut et le 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 du lien.
Alors #include
est 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 de 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 basé si étroitement 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 a utilisé le fichier d'en-tête. Les fonctions contre lesquelles vous liez n'ont même pas encore besoin d'être écrites, sans parler du compilateur sachant ce que .cpp
fichier dans lequel ils seront. Tout ce que le code appelant doit savoir au moment de la compilation est exprimé dans la déclaration de fonction. Au moment du lien, vous fournirez une liste de .o
fichiers, ou bibliothèques statiques ou dynamiques, et l'en-tête en vigueur est une promesse que les définitions des fonctions y seront quelque part.
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? Wikipedia en parle un peu.
Les langages compilés plus récents (tels que Java, C #) n'utilisent pas de déclarations avancées; les identifiants sont reconnus automatiquement à partir des fichiers source et lus directement à partir des symboles de bibliothèque dynamique. Cela signifie que les fichiers d'en-tête ne sont pas nécessaires.
Certaines personnes considèrent les fichiers d'en-tête comme un avantage:
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 a dû commencer à lire le fichier en haut, puis procéder linéairement à travers le code source. Le mécanisme d'en-tête le permet. Le compilateur n'a pas à prendre en compte d'autres unités de traduction, il suffit de 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 que était le but.
Cependant, l'une des propositions pour C++ 0x était d'ajouter un système de modules approprié, permettant de compiler du code similaire à .NET ou Java, dans des modules plus grands, le tout en une seule fois et sans en-têtes. Cette proposition n'a pas fait la coupe en C++ 0x, mais je crois qu'elle est toujours dans la catégorie "nous aimerions faire cela plus tard". Peut-être dans un TR2 ou similaire.
Pour ma compréhension (limitée - je ne suis pas un développeur C normalement), cela est enraciné en C. N'oubliez pas 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 devrait donner une erreur de compilation:
void SomeFunction() {
SomeOtherFunction();
}
void SomeOtherFunction() {
printf("What?");
}
L'erreur doit être que "SomeOtherFunction n'est pas déclaré" car 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 existe une fonction appelée SomeOtherFunction qui retourne void et ne prend aucun paramètre. Donc, si vous encouragez du code qui essaie 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à qu'interviennent les fichiers .h: 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. En outre, 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 des classes et des modificateurs privés/publics, donc bien que vous puissiez toujours demander s'ils sont nécessaires, C++ AFAIK nécessite toujours une déclaration de fonctions avant de les utiliser. En outre, de nombreux développeurs C++ sont ou étaient également des développeurs C et ont repris leurs concepts et habitudes en C++ - pourquoi changer ce qui n'est pas cassé?
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 une nouvelle compilation des fichiers inclus lorsque le fichier inclus change.
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.)
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.
Des langages comme Java ou C # d'autre part 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 des déclarations autonomes pour être inclus 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 gaspiller de l'espace. N'oubliez pas que C/C++ vient d'une époque où la taille d'un exécutable ou d'une bibliothèque importait beaucoup.
C++ a été conçu pour ajouter des fonctionnalités de langage de programmation modernes à l'infrastructure C, sans changer inutilement quoi que ce soit sur C qui ne concernait pas spécifiquement le langage lui-même.
Oui, à ce stade (10 ans après le premier standard C++ et 20 ans après qu'il a commencé à se développer sérieusement), il est facile de se demander pourquoi il n'a pas un système de modules approprié. De toute évidence, tout nouveau langage en cours de conception 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 en douceur de la pratique existante, en ajoutant seulement 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 commençant un nouveau projet), et certaines choses plus faciles (en particulier pour ceux qui maintiennent le code existant) que les 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 réelle valeur à le réécrire tout. Ils s'intègrent très bien de toute façon, donc c'est largement indolore.
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 à l'aide de plusieurs fichiers de code et si vous utilisez une fonction qui n'a pas été déclarée précédemment.
C'est vraiment un problème de portée.
Eh bien, le C++ a été ratifié en 1998, mais il était en usage depuis bien plus longtemps que cela, et la ratification fixait principalement l'usage actuel plutôt que d'imposer une structure. Et puisque C++ était basé sur C, et C a des fichiers d'en-tête, C++ les a aussi.
La raison principale des fichiers d'en-tête est d'activer la compilation séparée des fichiers et de minimiser les dépendances.
Disons que j'ai foo.cpp et que je veux 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 promet au compilateur que les classes/fonctions de bar.h existeront au moment de l'exécution et qu'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'essaie de lier mon programme, il ne se 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 changez pas du tout l'en-tête, il vous suffit de 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.
Si vous voulez que le compilateur découvre automatiquement les symboles définis dans d'autres fichiers, vous devez forcer le programmeur à placer ces fichiers dans des emplacements prédéfinis (comme Java détermine la structure des dossiers du projet). I préférez les fichiers d'en-tête. Vous aurez également besoin de sources de bibliothèques que vous utilisez ou d'une manière uniforme de mettre les informations nécessaires au compilateur dans les binaires.
Le 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, la vérification 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.
Je pense que la vraie raison (historique) derrière les fichiers d'en-tête était plus facile pour les développeurs de compilateurs ... mais alors, les fichiers d'en-tête offrent des avantages.
Vérifiez cet article précédent pour plus de discussions ...
Eh bien, vous pouvez parfaitement développer C++ sans fichiers d'en-tête. En fait, certaines bibliothèques qui utilisent intensivement les 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é. Une façon pratique de résoudre ce problème consiste à utiliser des fichiers d'en-tête. De plus, vous obtenez l'avantage de partager une interface sans partager de code/implémentation. Et je pense que cela n'a pas été envisagé par les créateurs 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 langue mais un moyen pratique de gérer l'inclusion multiple.
Donc, je pense que lorsque C a été créé, les problèmes avec la déclaration directe étaient 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++ ...