Quelqu’un peut-il aider à clarifier le fonctionnement des fichiers d’en-tête?

Cela fait déjà quelques semaines que je travaille avec C ++, mais le mécanisme derrière les fichiers d’en-tête (ou l’éditeur de liens, je suppose?) Me rend confus. J’ai pris l’habitude de créer un fichier “main.h” pour regrouper mes autres fichiers d’en-tête et garder le fichier main.cpp rangé, mais ces fichiers d’en-tête se plaignent parfois de ne pas pouvoir trouver un fichier d’en-tête différent (même s’il est déclaré dans le “main.h”). Je ne l’explique probablement pas très bien alors voici une version abrégée de ce que j’essaie de faire:

//main.cpp #include "main.h" int main() { return 0; } 

 //main.h #include "player.h" #include "health.h" #include "custvector.h" 

 //player.h #include "main.h" class Player { private: Vector playerPos; public: Health playerHealth; }; 

 //custvector.h struct Vector { int X; int Y; int Z; }; 

 //health.h class Health { private: int curHealth; int maxHealth; public: int getHealth() const; void setHealth(int inH); void modHealth(int inHM); }; 

Je n’inclurai pas health.cpp car c’est un peu long (mais ça marche), il contient #include "health.h" .

Quoi qu’il en soit, le compilateur (Code :: Blocks) se plaint que “player.h” ne trouve pas les types ‘Health’ ou ‘Vector’. Je pensais que si j’utilisais #include "main.h" dans “player.h”, il serait en mesure de trouver les définitions de Health et de Vector , ils sont inclus dans “main.h”. Je pensais qu’ils allaient s’efforcer de creuser un tunnel à leur façon (player.h -> main.h -> santé.h). Mais cela n’a pas très bien fonctionné. Existe-t-il une sorte de diagramme ou de vidéo qui pourrait clarifier la manière dont cela devrait être mis en place? Google n’a pas beaucoup aidé (ni mon livre).

Les autres réponses ici ont bien expliqué le mode de fonctionnement des fichiers d’en-tête et du préprocesseur. Le plus gros problème que vous avez est la dépendance circulaire, qui, par expérience, peut être une douleur royale. En outre, lorsque cela commence à se produire, le compilateur commence à se comporter de manière très étrange et envoie des messages d’erreur qui ne sont pas très utiles. La méthode qui m’a été enseignée par un gourou C ++ au collège consistait à démarrer chaque fichier (un fichier d’en-tête par exemple) avec:

 //very beginning of the file #ifndef HEADER_FILE_H //use a name that is unique though!! #define HEADER_FILE_H ... //code goes here ... #endif //very end of the file 

Ceci utilise les directives du préprocesseur pour empêcher automatiquement les dépendances circulaires. Fondamentalement, j’utilise toujours une version entièrement en majuscule du nom de fichier. custom-vector.h devient

 #ifndef CUSTOM_VECTOR_H #define CUSTOM_VECTOR_H 

Cela vous permet d’inclure des fichiers willie-nillie sans créer de dépendances circulaires, car si un fichier est inclus plusieurs fois, sa variable de préprocesseur est déjà définie. Le préprocesseur ignore le fichier. Cela facilite également l’utilisation ultérieure du code, car vous n’avez pas à parcourir vos anciens fichiers d’en-tête pour vous assurer que vous n’avez pas déjà inclus quelque chose. Je répète cependant, assurez-vous que les noms de variable que vous utilisez dans vos instructions #define sont uniques, sinon vous pourriez rencontrer des problèmes où quelque chose n’est pas inclus correctement ;-).

Bonne chance!

La meilleure façon de penser à vos fichiers d’en-tête est d’utiliser un “copier-coller automatisé”.

Un bon moyen d’y réfléchir (bien que ce ne soit pas le cas) est que, lorsque vous comstackz un fichier C ou C ++, le pré-processeur s’exécute en premier. Chaque fois qu’il rencontre une instruction #include, il collera en réalité le contenu de ce fichier au lieu de l’instruction #include. Ceci est fait jusqu’à ce qu’il n’y ait plus d’inclusions. Le tampon final est transmis au compilateur.

Cela introduit plusieurs complexités:

Premièrement, si AH comprend BH et BH comprend Ah, vous avez un problème. Parce que chaque fois que vous voulez coller A, vous aurez besoin de B et il aura en interne A! C’est une récursion. Pour cette raison, les fichiers d’en-tête utilisent #ifndef, pour s’assurer que la même partie n’est pas lue plusieurs fois. Cela se produit probablement dans votre code.

Deuxièmement, votre compilateur C lit le fichier une fois que tous les fichiers d’en-tête ont été “aplatis”. Vous devez donc en tenir compte lorsque vous raisonnez sur ce qui est déclaré avant ce qui est déclaré.

Vous avez une dépendance circulaire. Player comprend main.h, mais main.h inclut player.h. Résolvez ce problème en supprimant l’une ou l’autre dépendance. \

Player.h devrait inclure health.h et custvector.h, et pour le moment, je ne pense pas que main.h en ait besoin. Finalement, il peut avoir besoin de player.h.

comprend un travail très simple, il suffit de commander préprocesseur pour append le contenu du fichier à où include est défini. L’idée de base est d’inclure les en-têtes dont vous dépendez. Dans player.h vous devez inclure custvector.h et Health.h . Dans le player.h principal uniquement player.h , car tous les éléments nécessaires seront transportés avec le lecteur. et vous n’avez pas du tout besoin d’inclure main.h dans player.h .

il convient également de s’assurer que cet en-tête n’est inclus qu’une fois. dans cette question une solution générale est donnée Comment empêcher plusieurs définitions en C? Dans le cas de Visual Studio, vous pouvez utiliser #pragma once . Si Borland c ++ existe également une astuce mais je l’ai oublié.

Vous souhaitez organiser vos #includes (et vos bibliothèques) dans un DAG (graphe acyclique dirigé). C’est la façon compliquée de dire “éviter les cycles entre les fichiers d’en-tête”:

Si B comprend A, A ne doit pas inclure B.

Donc, utiliser “one big master main.h ” n’est pas la bonne approche, car il est difficile d’inclure uniquement les dépendances directes.

Chaque fichier .cpp doit inclure son propre fichier .h. Ce fichier .h ne doit inclure que les éléments qu’il a lui-même besoin de comstackr.

Il n’y a généralement pas de main.h , car main.cpp personne n’a besoin de la définition de main.

En outre, vous voudrez inclure des gardes pour vous protéger contre les inclus multiples.

Par exemple

 //player.h #ifndef PLAYER_H_ #define PLAYER_H_ #include "vector.h" // Because we use Vector #include "health.h" // Because we use Health class Player { private: Vector playerPos; public: Health playerHealth; }; #endif 

 //vector.h #ifndef VECTOR_H_ #define VECTOR_H_ struct Vector { int X; int Y; int Z; }; #endif 

 //health.h #ifndef HEALTH_H_ #define HEALTH_H_ class Health { private: int curHealth; int maxHealth; public: int getHealth() const; void setHealth(int inH); void modHealth(int inHM); }; #endif 

Le seul moment où vous souhaitez regrouper un groupe de #include s dans un en-tête unique est lorsque vous le fournissez à titre de commodité pour une très grande bibliothèque.

Dans votre exemple actuel, vous allez un peu trop loin – chaque classe n’a pas besoin de son propre fichier d’en-tête. Tout peut aller dans main.cpp.

Le préprocesseur c insère littéralement le fichier d’un #include dans le fichier qui l’inclut (sauf s’il a déjà été inséré, raison pour laquelle vous avez besoin des gardes d’inclusion). Il vous permet d’utiliser les classes définies dans ces fichiers car vous avez maintenant access à leur définition.