web-dev-qa-db-fra.com

En-tête C / C ++ et fichiers d'implémentation: comment fonctionnent-ils?

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
  • myfunction.cpp
  • myfunction.hpp

//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

40
nickelpro

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

63
Ed Heal

Vous devez comprendre que la compilation est une opération en 2 étapes, du point de vue de l'utilisateur.


1ère étape: compilation d'objets

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.


2e étape: liaison

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 * .

15
Coren

1. Le principe

Lorsque vous écrivez:

int A = myfunction(12);

Cela se traduit par:

int A = @call(myfunction, 12);

@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:

  • ceux qui sont dans la bibliothèque, et peuvent être référencés via un offset (l'adresse finale est encore inconnue)
  • ceux qui sont en dehors de la bibliothèque, et dont l'adresse est complètement inconnue jusqu'à l'exécution.

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 le symbole est mappé en mémoire, et écrasera le <undefined-address> avec l'adresse réelle (pour cette exécution).

5
Matthieu M.

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:

  1. Le compilateur est appelé pour chaque fichier cpp et le traduit en un fichier objet (code binaire) avec un table des symboles qui associe le nom de la fonction (les noms sont modifiés en c ++) à leur emplacement dans le fichier objet.
  2. L'éditeur de liens est invoqué une seule fois: avec chaque fichier objet en paramètre. Il résoudra l'emplacement des appels de fonction d'un fichier objet à un autre grâce à tables de symboles. Une fonction main () DOIT exister quelque part. Finalement, un fichier exécutable binaire est produit lorsque l'éditeur de liens a trouvé tout ce dont il a besoin.
4
yves Baumes

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.

4
Ram

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 .

2
LihO

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.

1
atoMerz