1. Modèle de compilation C++
Habituellement, un programme C++ ne contient que deux types de fichiers — les fichiers .cpp et les fichiers .h. Parmi eux, les fichiers .cpp sont appelés fichiers source C++, qui contiennent du code source C++ ; les fichiers .h sont appelés fichiers d'en-tête C++, qui contiennent également du code source C++.
Le langage C++ prend en charge la « compilation séparée ». C'est-à-dire que tout le contenu d'un programme peut être divisé en différentes parties et placé dans différents fichiers .cpp. Le contenu d'un fichier .cpp est relativement indépendant ; lors de la compilation, il n'a pas besoin de communiquer avec d'autres fichiers. Il suffit de les lier avec d'autres fichiers objets après la compilation en fichiers objets. Par exemple, une fonction globale "void a(){}" est définie dans le fichier a.cpp, et le fichier b.cpp doit appeler cette fonction. Malgré cela, les fichiers a.cpp et b.cpp n'ont pas besoin de connaître l'existence l'un de l'autre. Ils peuvent être compilés séparément, et après compilation en fichiers objets, ils sont liés, et le programme entier peut s'exécuter.
Comment cela est-il réalisé ? Du point de vue de l'écriture d'un programme, c'est très simple. Dans le fichier b.cpp, avant d'appeler la fonction "void a()", il suffit de déclarer la fonction "void a();". En effet, lorsque le compilateur compile b.cpp, il génère une table des symboles. Les symboles sans définition, tels que "void a()", sont stockés dans cette table. Lors de l'édition des liens, le compilateur recherche la définition de ce symbole dans d'autres fichiers objets. Une fois trouvée, le programme peut être généré avec succès.
Notez que deux concepts sont mentionnés ici : « définition » et « déclaration ». En termes simples, une « définition » décrit complètement un symbole : s'il s'agit d'une variable ou d'une fonction, quel type de retour il a, quels paramètres il nécessite, etc. Une « déclaration » déclare seulement l'existence du symbole, c'est-à-dire qu'elle dit au compilateur : ce symbole est défini dans un autre fichier ; je vais l'utiliser ici, et lors de l'édition de liens, allez ailleurs pour découvrir ce qu'il est réellement. Lors de la définition, vous devez définir complètement un symbole (variable ou fonction) conformément à la syntaxe C++, mais lors de la déclaration, vous devez seulement écrire le prototype du symbole. Il convient de noter qu'un symbole peut être déclaré plusieurs fois dans un programme entier, mais il doit être défini exactement une fois. Imaginez, si un symbole a deux définitions différentes, laquelle le compilateur doit-il écouter ? Ce mécanisme apporte de nombreux avantages aux programmeurs C++, et conduit aussi à une méthode d'écriture de programmes. Considérez ceci : s'il existe une fonction couramment utilisée « void f() {} », qui est appelée dans de nombreux fichiers .cpp à travers le programme, alors nous avons seulement besoin de définir la fonction dans un fichier et de la déclarer dans les autres fichiers. Une fonction est facile à gérer ; la déclarer ne prend qu'une phrase. Mais que se passe-t-il s'il y a beaucoup de fonctions, comme un grand ensemble de fonctions mathématiques, plusieurs centaines ? Pouvons-nous garantir que chaque programmeur peut se souvenir avec précision et écrire les formes de toutes les fonctions ?2. Qu'est-ce qu'un fichier d'en-tête ?
Évidemment, la réponse est impossible. Mais il existe un moyen très simple d'aider les programmeurs à s'épargner la peine de se souvenir d'autant de prototypes de fonctions : nous pouvons écrire à l'avance toutes les instructions de déclaration de ces centaines de fonctions, les mettre dans un seul fichier, et lorsque les programmeurs en ont besoin, ils peuvent les copier toutes dans leur code source.
Cette méthode est certes réalisable, mais elle reste trop pénible et plutôt maladroite. Ainsi, les fichiers d'en-tête peuvent jouer leur rôle. Le fichier d'en-tête, pour ainsi dire, a en fait le même contenu que les fichiers .cpp ; tous deux sont du code source C++. Cependant, les fichiers d'en-tête n'ont pas besoin d'être compilés. Nous plaçons toutes les déclarations de fonctions dans un fichier d'en-tête. Lorsqu'un fichier source .cpp en a besoin, elles peuvent être incluses dans le fichier .cpp via une commande macro "#include", fusionnant ainsi leur contenu dans le fichier .cpp. Lorsque le fichier .cpp est compilé, les fichiers .h inclus prennent effet.
Par exemple, supposons qu'il n'y ait que deux fonctions mathématiques : f1 et f2. Ensuite, nous plaçons leurs définitions dans math.cpp :
Et placez les déclarations de "ces" fonctions dans un fichier d'en-tête math.h :
Dans un autre fichier, main.cpp, je veux appeler ces deux fonctions, donc je n'ai qu'à inclure le fichier d'en-tête :
De cette manière, il s'agit d'un programme complet. Il convient de noter que le fichier .h n'a pas besoin d'être écrit après la commande du compilateur, mais il doit se trouver dans un endroit où le compilateur peut le trouver (ou au même endroit que main). main.cpp et math.cpp peuvent tous deux être compilés séparément, générant main.o et math.o. Ensuite, en liant ces deux fichiers objets, le programme peut être exécuté.
3. #include
#include est une commande macro du langage C. Elle prend effet avant que le compilateur ne compile, c'est-à-dire pendant le prétraitement. La fonction de #include est d'inclure le contenu du fichier écrit après lui dans le fichier courant complètement et sans changer un mot. Il convient de mentionner qu'elle n'a pas d'autres fonctions ni d'effets de bord. Sa fonction est de remplacer chaque endroit où elle apparaît par le contenu du fichier écrit après elle. Un simple remplacement de texte, rien de plus. Par conséquent, la première ligne dans main.cpp (#include "math.h") sera remplacée par le contenu de math.h avant la compilation. C'est-à-dire que, lorsque le processus de compilation est sur le point de commencer, le contenu de main.cpp a déjà changé :
Ni plus, ni moins, juste ce qu'il faut. Par le même raisonnement, si en plus de main.cpp, il y a beaucoup d'autres fichiers .cpp qui utilisent aussi les fonctions f1 et f2, alors ils n'ont tous qu'à écrire #include "math.h" avant d'utiliser ces deux fonctions.
4. Que faut-il écrire dans un fichier d'en-tête ?
D'après la discussion ci-dessus, nous pouvons comprendre que le rôle des fichiers d'en-tête est d'être inclus par d'autres fichiers .cpp. Ils ne participent pas eux-mêmes à la compilation, mais en fait, leur contenu est compilé dans plusieurs fichiers .cpp. Grâce à la règle selon laquelle « une définition ne peut apparaître qu'une seule fois », nous pouvons facilement conclure que les fichiers d'en-tête ne doivent contenir que des déclarations de variables et de fonctions, et non leurs définitions. Parce que le contenu d'un fichier d'en-tête est en fait introduit dans plusieurs fichiers .cpp différents, et qu'ils sont tous compilés. Mettre des déclarations convient. Si des définitions y sont placées, cela équivaut à avoir une définition d'un symbole (variable ou fonction) qui apparaît dans plusieurs fichiers. Même si ces définitions sont toutes identiques, c'est illégal pour le compilateur.
Ainsi, un point à retenir est que dans les fichiers d'en-tête .h, seules des déclarations de variables ou de fonctions doivent exister, pas des définitions. Autrement dit, vous ne pouvez écrire que des lignes comme : extern int a; et void f(); dans les fichiers d'en-tête. Ce sont des déclarations. Si vous écrivez des lignes comme int a; ou void f() {}, dès que ce fichier d'en-tête est inclus par deux fichiers .cpp ou plus, le compilateur signalera immédiatement une erreur. (Concernant extern, nous en avons discuté plus tôt ; ici, nous ne reviendrons pas sur la différence entre définitions et déclarations.)
Cependant, il y a trois exceptions à cette règle :
- Premièrement, les définitions d'objets const peuvent être écrites dans des fichiers d'en-tête. Parce que les objets const globaux n'ont pas de déclarations extern par défaut, ils ne sont valides que dans le fichier courant. Si l'on écrit de tels objets dans un fichier d'en-tête, même s'il est inclus dans plusieurs autres fichiers .cpp, l'objet n'est valide que dans le fichier qui l'inclut et est invisible pour les autres fichiers, donc cela ne provoquera pas de définitions multiples. En même temps, comme l'objet de ces fichiers .cpp est inclus depuis un seul fichier d'en-tête, cela garantit que la valeur de l'objet const dans ces fichiers .cpp est la même. On peut dire que cela fait d'une pierre deux coups. De même, les définitions d'objets static peuvent aussi être placées dans des fichiers d'en-tête.
- Deuxièmement, les définitions de fonctions inline peuvent être écrites dans des fichiers d'en-tête. Parce qu'une fonction inline a besoin que le compilateur l'étende inline à l'endroit où elle est rencontrée selon sa définition, et contrairement aux fonctions ordinaires, elle ne peut pas être déclarée d'abord puis liée (les fonctions inline ne sont pas liées), le compilateur doit voir la définition complète de la fonction inline au moment de la compilation. Si une fonction inline ne pouvait être définie qu'une seule fois comme une fonction ordinaire, cela serait difficile. Car dans un fichier, cela va ; je peux placer la définition de la fonction inline tout au début, afin que la définition soit visible partout où elle est utilisée ensuite. Mais que se passe-t-il si j'utilise aussi cette fonction dans d'autres fichiers ? Il n'y a presque pas de bonne solution. Par conséquent, C++ stipule qu'une fonction inline peut être définie plusieurs fois dans un programme, tant que la fonction inline n'apparaît qu'une seule fois dans un fichier .cpp, et que la définition de la fonction inline est la même dans tous les fichiers .cpp, elle peut passer la compilation. De toute évidence, placer les définitions de fonctions inline dans un fichier d'en-tête est une pratique très judicieuse.
- Troisièmement, les définitions de classes peuvent être écrites dans des fichiers d’en-tête. En effet, lorsqu’un programme crée un objet d’une classe, le compilateur ne peut savoir comment l’objet de cette classe doit être disposé que lorsque la définition de la classe est entièrement visible. Par conséquent, les exigences relatives aux définitions de classes sont fondamentalement les mêmes que celles relatives aux fonctions inline. Donc, placer une définition de classe dans un fichier d’en-tête et inclure ce fichier d’en-tête dans les fichiers .cpp qui utilisent la classe est une bonne pratique. Ici, il convient de mentionner qu’une définition de classe contient des membres de données et des membres de fonction. Les membres de données sont définis (de l’espace leur est alloué) uniquement lorsqu’un objet spécifique est créé, mais les membres de fonction doivent être définis dès le départ, ce que nous appelons habituellement l’implémentation de la classe. Généralement, notre approche consiste à placer la définition de la classe dans le fichier d’en-tête et à placer le code d’implémentation des membres de fonction dans un fichier .cpp. C’est acceptable et c’est une très bonne approche. Cependant, il existe une autre approche : écrire également directement le code d’implémentation des membres de fonction à l’intérieur de la définition de la classe. Dans les classes C++, si un membre de fonction est défini dans le corps de la définition de la classe, le compilateur traitera cette fonction comme inline. Par conséquent, définir des membres de fonction à l’intérieur du corps de la définition de la classe et les regrouper dans le fichier d’en-tête est légal. Notez que si la définition d’un membre de fonction est écrite dans le fichier d’en-tête de la définition de la classe mais pas à l’intérieur de la définition de la classe, c’est illégal, car le membre de fonction n’est plus inline à ce stade. Une fois que le fichier d’en-tête est inclus par deux ou plusieurs fichiers .cpp, le membre de fonction sera redéfini.
5. Mesures de protection dans les fichiers d’en-tête
Considérez ceci : si le fichier d'en-tête ne contient que des déclarations, alors quel que soit le nombre de fois qu'il est inclus par le même fichier .cpp, il n'y a aucun problème—car l'apparition des déclarations n'est pas restreinte. Cependant, les trois exceptions dans les fichiers d'en-tête abordées ci-dessus sont aussi un usage très courant des fichiers d'en-tête. Ainsi, dès qu'une des trois exceptions ci-dessus apparaît dans un fichier d'en-tête, s'il est inclus plusieurs fois par un fichier .cpp, il y aura un gros problème. En effet, les éléments de syntaxe dans ces trois exceptions, bien qu'ils « puissent être définis dans plusieurs fichiers source », « ne peuvent apparaître qu'une seule fois dans un seul fichier source ». Imaginez que a.h contienne la définition de la classe A, et b.h la définition de la classe B. Comme la définition de la classe B dépend de la classe A, b.h #include aussi a.h. Maintenant, il y a un fichier source qui utilise à la fois la classe A et la classe B, donc le programmeur inclut à la fois a.h et b.h dans ce fichier source. À ce moment, le problème surgit : la définition de la classe A apparaît deux fois dans ce fichier source ! En conséquence, le programme entier ne peut pas être compilé. Vous pourriez penser que c'est une erreur du programmeur—il aurait dû savoir que b.h inclut a.h—mais en fait il ne devrait pas avoir à le savoir.
Utiliser « #define » combiné à la compilation conditionnelle peut bien résoudre ce problème. Dans un fichier d'en-tête, définissez un nom via #define, et utilisez la compilation conditionnelle #ifndef...#endif afin que le compilateur puisse décider de continuer ou non à compiler le reste du fichier d'en-tête selon que ce nom est défini. Cette méthode est simple, mais vous devez vous rappeler de l'inclure lorsque vous écrivez des fichiers d'en-tête.
La différence entre les fichiers d'en-tête C++ et les fichiers source
1. Comment les fichiers source s'associent aux fichiers d'en-tête en fonction de #include
- 1. Les fichiers d'en-tête fournis par le système sont placés entre chevrons, donc le compilateur les recherchera dans le répertoire des fichiers système.
- 2. Les fichiers définis par l'utilisateur sont encadrés par des guillemets doubles. Le compilateur effectue d'abord une recherche dans le répertoire utilisateur, puis dans le répertoire d'installation de C++ (par exemple, dans VC vous pouvez spécifier et modifier le chemin de recherche des fichiers de bibliothèque, et sous Unix et Linux vous pouvez le définir via des variables d'environnement), et enfin dans les fichiers système.
#include "xxx.h" (j'ai toujours pensé que "" et <> ne faisaient aucune différence, mais tinyxml.h est un fichier non système, donc il faut utiliser "")
2. Comment les fichiers d'en-tête s'associent aux fichiers source
Cette question signifie en fait : étant donné que le fichier d'en-tête "a.h" déclare une série de fonctions, et que "b.cpp" implémente ces fonctions, si je veux utiliser dans "c.cpp" les fonctions déclarées dans "a.h" et implémentées dans "b.cpp", j'utilise normalement #include "a.h" dans "c.cpp". Alors, comment c.cpp trouve-t-il les implémentations dans b.cpp ?
En fait, les noms de fichiers .cpp et .h n'ont pas de relation directe. De nombreux compilateurs peuvent accepter d'autres extensions. Par exemple, je vois maintenant dans le code source de notre entreprise que les fichiers .cpp ont été remplacés par des fichiers .cc.
Dans Turbo C, la compilation se fait en ligne de commande, et le paramètre de ligne de commande est le nom de fichier, dont les extensions par défaut sont .cpp et .h, mais elles peuvent aussi être personnalisées en .xxx, etc.
Dans le livre "Programmation C" du professeur Tan Haoqiang, il est mentionné que, lors du prétraitement du compilateur, la commande #include est traitée comme un "traitement d'inclusion de fichier" : tout le contenu de file2.c est copié à l'emplacement de #include "file2.c". Cela explique aussi pourquoi de nombreux compilateurs ne se soucient pas de l'extension du fichier — parce que le prétraitement de #include ne fait qu'un travail de "copie et insertion de code".
Lors de la compilation, il ne recherche pas les implémentations de fonctions dans b.cpp ; cela n'est fait qu'à l'édition de liens. L'utilisation de #include "a.h" dans b.cpp ou c.cpp introduit en fait des déclarations associées afin que la compilation puisse passer. Le programme ne se soucie pas de l'endroit où se trouve l'implémentation ni de la manière dont elle est implémentée. Une fois les fichiers sources compilés, des fichiers objets (fichiers .o ou .obj) sont générés. Dans ces fichiers objets, ces fonctions et variables sont traitées comme des symboles. À l'édition de liens, le makefile doit spécifier quel fichier .o ou .obj lier (ici, le fichier .o ou .obj généré par b.cpp). À ce stade, l'éditeur de liens va dans ce fichier .o ou .obj pour trouver les fonctions implémentées dans b.cpp, puis les intègre dans le fichier exécutable spécifié dans le makefile.
Sous Unix, vous pouvez même éviter d'inclure les fichiers d'en-tête dans les fichiers sources ; il vous suffit de les spécifier dans le makefile (mais cela réduit considérablement la lisibilité du programme, ce qui est une mauvaise habitude, ^_^). Sous VC, en général, vous n'avez pas besoin d'écrire vous-même un makefile ; il vous suffit d'inclure tous les fichiers requis dans le projet, et VC écrira automatiquement le makefile pour vous.
Habituellement, le compilateur C++ recherche les symboles requis dans chaque fichier .o ou .obj, au lieu de ne chercher que dans un fichier spécifique, ou de s'arrêter après en avoir trouvé un. Par conséquent, si la même fonction est implémentée dans plusieurs fichiers différents, ou si la même variable globale est définie, l'éditeur de liens signalera "redefined" lors de l'édition de liens.
En résumé
Les fichiers .h peuvent contenir :
- Des déclarations de données membres de classe, mais sans affectation de valeurs
- Des définitions et affectations de membres de données statiques de classe, mais ce n'est pas recommandé ; une simple déclaration suffit.
- Des déclarations de fonctions membres de classe
- Des déclarations de fonctions non membres de classe
- Définitions de constantes : par ex., constint a=5;
- Définitions de fonctions statiques
- Définitions de fonctions inline de classe
Ne peut pas contenir :
- 1. Déclarations de toutes les variables non statiques (pas les membres de données de classe)
- 2. Ne placez pas les déclarations d'espace de noms par défaut dans les fichiers d'en-tête ; using namespace std; etc. doivent être placés dans les fichiers .cpp. Utilisez std::string dans les fichiers .h.
URL d'origine : https://blog.csdn.net/qq_35038153/article/details/71293265