C'est probablement une question stupide, mais j'ai cherché un bon moment maintenant ici et sur le web et je n'ai pas pu trouver de réponse claire (j'ai fait ma recherche sur Google).
Je suis donc nouveau dans la programmation ... Ma question est, comment la fonction principale connaît-elle les définitions de fonction (implémentations) dans un fichier différent?
ex. Dis que j'ai 3 fichiers
//main.cpp
#include "myfunction.hpp"
int main() {
int A = myfunction( 12 );
...
}
-
//myfunction.cpp
#include "myfunction.hpp"
int myfunction( int x ) {
return x * x;
}
-
//myfunction.hpp
int myfunction( int x );
-
J'obtiens comment le préprocesseur inclut le code d'en-tête, mais comment l'en-tête et la fonction principale savent-ils même que la définition de la fonction existe, et encore moins l'utiliser?
Je m'excuse si ce n'est pas clair ou si je me trompe énormément sur quelque chose de nouveau ici
Le fichier d'en-tête déclare fonctions/classes - c.-à-d. Indique au compilateur quand il compile un .cpp
fichier quelles fonctions/classes sont disponibles.
Le .cpp
le fichier définit ces fonctions - c'est-à-dire que le compilateur compile le code et produit donc le code machine réel pour effectuer les actions déclarées dans le .hpp
fichier.
Dans votre exemple, main.cpp
comprend un .hpp
fichier. Le préprocesseur remplace le #include
avec le contenu du .hpp
fichier. Ce fichier indique au compilateur que la fonction myfunction
est définie ailleurs et prend un paramètre (un int
) et renvoie un int
.
Ainsi, lorsque vous compilez main.cpp
dans le fichier objet (extension .o), il note dans ce fichier qu'il requiert la fonction myfunction
. Lorsque vous compilez myfunction.cpp
dans un fichier objet, le fichier objet contient une note indiquant qu'il a la définition de myfunction
.
Ensuite, lorsque vous arrivez à lier les deux fichiers objets ensemble dans un exécutable, l'éditeur de liens lie les extrémités - c'est-à-dire main.o
utilise myfunction
comme défini dans myfunction.o
.
J'espère que ça aide
Vous devez comprendre que la compilation est une opération en 2 étapes, du point de vue de l'utilisateur.
Au cours de cette étape, vos fichiers * .c sont individuellement compilés en séparés fichiers objets. Cela signifie que lorsque main.cpp est compilé, il ne sait rien de votre myfunction.cpp. La seule chose qu'il sait, c'est que vous déclarez qu'une fonction avec cette signature: int myfunction( int x )
existe dans un autre fichier objet.
Le compilateur gardera une référence de cet appel et l'inclura directement dans le fichier objet. Le fichier objet contiendra un "Je dois appeler mafonction avec un int et il me reviendra avec un int Il garde un index de tous les appels extern afin de pouvoir se connecter avec les autres par la suite.
Au cours de cette étape, le linker examinera tous ces index de vos fichiers objets et tentera de résoudre les dépendances au sein de ces fichiers. Si vous n'y êtes pas, vous obtiendrez le célèbre undefined symbol XXX
à partir de cela. Il traduira ensuite ces références en véritable adresse mémoire dans un fichier de résultat: soit un binaire soit une bibliothèque.
Et puis, vous pouvez commencer à demander comment est-ce possible de le faire avec un programme gigantesque comme une suite Office, qui a des tonnes de méthodes et d'objets? Eh bien, ils utilisent le mécanisme bibliothèque partagée . Vous les connaissez avec vos fichiers '.dll' et/ou '.so' que vous avez sur votre station de travail Unix/Windows. Il permet de reporter la résolution d'un symbole non défini jusqu'à l'exécution du programme.
Il permet même de résoudre des symboles non définis à la demande, avec les fonctions dl * .
1. Le principe
Lorsque vous écrivez:
int A = myfunction(12);
Cela se traduit par:
int A = @call(myfunction, 12);
où @call
peut être considéré comme une recherche de dictionnaire. Et si vous pensez à l'analogie du dictionnaire, vous pouvez certainement connaître un mot (smogashboard?) Avant de connaître sa définition. Tout ce dont vous avez besoin, c'est qu'à l'exécution, la définition soit dans le dictionnaire.
2. Un point sur ABI
Comment cela @ call fonctionne-t-il? À cause de l'ABI. L'ABI est un moyen qui décrit de nombreuses choses, et parmi celles-ci comment effectuer un appel à une fonction donnée (en fonction de ses paramètres). Le contrat d'appel est simple: il indique simplement où chacun des arguments de fonction peut être trouvé (certains seront dans les registres du processeur, d'autres sur la pile).
Par conséquent, @call:
@Push 12, reg0
@invoke myfunction
Et la définition de la fonction sait que son premier argument (x) est situé dans reg0
.
. Mais je pensais que les dictionnaires étaient pour les langues dynamiques?
Et vous avez raison, dans une certaine mesure. Les langages dynamiques sont généralement implémentés avec une table de hachage pour la recherche de symboles qui est remplie dynamiquement.
Pour C++, le compilateur transformera une unité de traduction (grosso modo, un fichier source prétraité) en objet (.o
ou .obj
en général). Chaque objet contient un tableau des symboles auxquels il fait référence mais dont la définition n'est pas connue:
.undefined
[0]: myfunction
Ensuite, l'éditeur de liens rapproche les objets et réconcilie les symboles. Il existe deux types de symboles à ce stade:
Les deux peuvent être traités de la même manière.
.dynamic
[0]: myfunction at <undefined-address>
Et puis le code fera référence à l'entrée de recherche:
@invoke .dynamic[0]
Lorsque la bibliothèque est chargée (DLL_Open
par exemple), le runtime saura enfin où le symbole est mappé en mémoire, et écrasera le <undefined-address>
avec l'adresse réelle (pour cette exécution).
Comme suggéré dans le commentaire de Matthieu M., c'est le job linker de trouver la bonne "fonction" au bon endroit. Les étapes de compilation sont, en gros:
Le préprocesseur inclut le contenu des fichiers d'en-tête dans les fichiers cpp (les fichiers cpp sont appelés unité de traduction). Lorsque vous compilez le code, chaque unité de traduction est vérifiée séparément pour les erreurs sémantiques et syntaxiques. La présence de définitions de fonctions dans les unités de traduction n'est pas prise en compte. Les fichiers .obj sont générés après la compilation.
À l'étape suivante, lorsque les fichiers obj sont liés. la définition des fonctions (fonctions membres pour les classes) utilisées est recherchée et la liaison se produit. Si la fonction n'est pas trouvée, une erreur de l'éditeur de liens est levée.
Dans votre exemple, si la fonction n'était pas définie dans myfunction.cpp, la compilation se poursuivrait sans problème. Une erreur serait signalée lors de l'étape de liaison.
int myfunction(int);
est le prototype de la fonction. Vous déclarez la fonction avec lui afin que le compilateur sache que vous appelez cette fonction lorsque vous écrivez myfunction(0);
.
Et comment l'en-tête et la fonction principale savent-ils que la définition de la fonction existe?
Eh bien, c'est le travail de Linker .
Lorsque vous compilez un programme, le préprocesseur ajoute le code source de chaque fichier d'en-tête au fichier qui l'a inclus. Le compilateur compile TOUS LES .cpp
fichier. Le résultat est un certain nombre de .obj
des dossiers.
Après cela vient l'éditeur de liens. Linker prend tout .obj
fichiers, à partir de votre fichier principal, chaque fois qu'il trouve une référence qui n'a pas de définition (par exemple une variable, une fonction ou une classe), il essaie de localiser la définition respective dans un autre .obj
fichiers créés à l'étape de compilation ou fournis à l'éditeur de liens au début de l'étape de liaison.
Maintenant, pour répondre à votre question: chaque .cpp
le fichier est compilé dans un .obj
fichier contenant des instructions dans le code machine. Lorsque vous incluez un .hpp
fichier et utiliser une fonction définie dans un autre .cpp
fichier, au stade de la liaison, l'éditeur de liens recherche cette définition de fonction dans le .obj
fichier. Voilà comment il le trouve.