Liaison interne et externe en C ++

Bonne journée à tous!

Nous vous présentons la traduction d'un article intéressant qui vous a été préparé dans le cadre du cours "Développeur C ++" . Nous espérons que ce sera utile et intéressant pour vous, ainsi que pour nos auditeurs.

Allons-y.

Avez-vous déjà rencontré les termes communication interne et externe? Vous voulez savoir à quoi sert le mot-clé extern ou comment la déclaration de quelque chose de statique affecte la portée globale? Alors cet article est fait pour vous.

En bref

L'unité de traduction (.c / .cpp) et tous ses fichiers d'en-tête (.h / .hpp) sont inclus dans l'unité de traduction. Si un objet ou une fonction possède une liaison interne à l'intérieur d'une unité de traduction, ce symbole est visible par l'éditeur de liens uniquement à l'intérieur de cette unité de traduction. Si l'objet ou la fonction possède un lien externe, l'éditeur de liens pourra le voir lors du traitement d'autres unités de traduction. L'utilisation du mot-clé statique dans l'espace de noms global donne la liaison interne du caractère. Le mot clé extern donne une liaison externe.
Le compilateur par défaut donne aux caractères les liaisons suivantes:

  • Variables globales non const - liaison externe;
  • Const variables globales - liaison interne;
  • Fonctions - Liens externes.



Les bases

Tout d'abord, parlons de deux concepts simples nécessaires pour discuter de la liaison.

  • La diffĂ©rence entre une dĂ©claration et une dĂ©finition;
  • UnitĂ©s de diffusion.

Faites également attention aux noms: nous utiliserons le concept de «symbole» pour toute «entité de code» avec laquelle l'éditeur de liens fonctionne, par exemple avec une variable ou une fonction (ou avec des classes / structures, mais nous ne nous concentrerons pas sur elles).

Annonce VS. DĂ©finition

Nous discutons brièvement de la différence entre une déclaration et une définition de symbole: une annonce (ou déclaration) informe le compilateur de l'existence d'un symbole spécifique et autorise l'accès à ce symbole dans les cas qui ne nécessitent pas d'adresse mémoire exacte ou de stockage de symboles. La définition indique au compilateur ce qui est contenu dans le corps de la fonction ou la quantité de mémoire que la variable doit allouer.

Dans certaines situations, la déclaration n'est pas suffisante pour le compilateur, par exemple, lorsque l'élément de données de la classe a un type de lien ou de valeur (c'est-à-dire pas un lien et pas un pointeur). Dans le même temps, un pointeur vers un type déclaré (mais non défini) est autorisé, car il a besoin d'une quantité fixe de mémoire (par exemple, 8 octets dans les systèmes 64 bits), quel que soit le type vers lequel il pointe. Pour obtenir la valeur par ce pointeur, une définition est requise. De plus, pour déclarer une fonction, vous devez déclarer (mais pas définir) tous les paramètres (qu'elle soit prise par valeur, référence ou pointeur) et le type de retour. La détermination du type de valeur de retour et des paramètres n'est nécessaire que pour définir une fonction.

Les fonctions

La différence entre définir et déclarer une fonction est très évidente.

int f(); //  int f() { return 42; } //  

Variables

Avec les variables, c'est un peu différent. La déclaration et la définition ne sont généralement pas partagées. L'essentiel est:

 int x; 

Non seulement déclare x , mais le définit également. Cela est dû à l'appel au constructeur par défaut int. (En C ++, contrairement à Java, le constructeur de types simples (tels que int) n'initialise pas la valeur à 0 par défaut. Dans l'exemple ci-dessus, x sera égal à toute ordure située dans l'adresse mémoire allouée par le compilateur).

Mais vous pouvez séparer explicitement la déclaration de variable et sa définition à l'aide du mot clé extern .

 extern int x; //  int x = 42; //  

Cependant, lors de l'initialisation et de l'ajout d' extern à la déclaration, l'expression se transforme en définition et le mot clé extern devient inutile.

 extern int x = 5; //   ,   int x = 5; 

Aperçu de l'annonce

En C ++, il y a le concept de pré-déclaration d'un caractère. Cela signifie que nous déclarons le type et le nom du symbole pour une utilisation dans des situations qui ne nécessitent pas sa définition. Nous n'avons donc pas besoin d'inclure la définition complète d'un caractère (généralement un fichier d'en-tête) sans un besoin évident. Ainsi, nous réduisons la dépendance au fichier contenant la définition. Le principal avantage est que lors de la modification d'un fichier avec une définition, le fichier dans lequel nous déclarons au préalable ce symbole ne nécessite pas de recompilation (ce qui signifie que tous les autres fichiers y compris).

Exemple

Supposons que nous ayons une déclaration de fonction (appelée prototype) pour f qui prend un objet de type Class par valeur:

 // file.hpp void f(Class object); 

Inclure immédiatement la définition de Class - naïf. Mais puisque nous venons de déclarer f , il suffit de donner au compilateur une déclaration de Class . Ainsi, le compilateur pourra reconnaître la fonction par son prototype, et nous pourrons nous débarrasser de la dépendance de file.hpp sur le fichier contenant la définition de Class , disons class.hpp:

 // file.hpp class Class; void f(Class object); 

Disons que file.hpp est contenu dans 100 autres fichiers. Et disons que nous changeons la définition de Class dans class.hpp. Si vous ajoutez class.hpp à file.hpp, file.hpp et les 100 fichiers qui le contiennent devront être recompilés. Grâce à la déclaration préalable de Class, les seuls fichiers nécessitant une recompilation seront class.hpp et file.hpp (en supposant que f y soit défini).

Fréquence d'utilisation

Une différence importante entre une déclaration et une définition est qu'un symbole peut être déclaré plusieurs fois, mais défini une seule fois. Vous pouvez donc pré-déclarer une fonction ou une classe autant de fois que vous le souhaitez, mais il ne peut y avoir qu'une seule définition. C'est ce qu'on appelle la règle d'une définition . En C ++, les travaux suivants:

 int f(); int f(); int f(); int f(); int f(); int f(); int f() { return 5; } 

Et cela ne fonctionne pas:

 int f() { return 6; } int f() { return 9; } 

Unités de diffusion

Les programmeurs travaillent généralement avec des fichiers d'en-tête et des fichiers d'implémentation. Mais pas les compilateurs - ils travaillent avec des unités de traduction (unités de traduction, pour faire court - TU), qui sont parfois appelées unités de compilation. La définition d'une telle unité est assez simple - tout fichier transféré au compilateur après son traitement préliminaire. Pour être précis, il s'agit du fichier obtenu à la suite du travail du préprocesseur de macro d'extension, y compris le code source, qui dépend des expressions #ifdef et #ifndef , et du copier-coller de tous les fichiers #include .

Les fichiers suivants sont disponibles:

header.hpp:

 #ifndef HEADER_HPP #define HEADER_HPP #define VALUE 5 #ifndef VALUE struct Foo { private: int ryan; }; #endif int strlen(const char* string); #endif /* HEADER_HPP */ 

program.cpp:

 #include "header.hpp" int strlen(const char* string) { int length = 0; while(string[length]) ++length; return length + VALUE; } 

Le préprocesseur produira l'unité de traduction suivante, qui sera ensuite transmise au compilateur:

 int strlen(const char* string); int strlen(const char* string) { int length = 0; while(string[length]) ++length; return length + 5; } 

Les communications

Après avoir discuté des bases, vous pouvez commencer la relation. En général, la communication est la visibilité des caractères pour l'éditeur de liens lors du traitement des fichiers. La communication peut être externe ou interne.

Communication externe

Lorsqu'un symbole (variable ou fonction) a une connexion externe, il devient visible pour les éditeurs de liens à partir d'autres fichiers, c'est-à-dire visible «globalement», accessible à toutes les unités de traduction. Cela signifie que vous devez définir un tel symbole à un emplacement spécifique d'une unité de traduction, généralement dans le fichier d'implémentation (.c / .cpp), afin qu'il n'ait qu'une seule définition visible. Si vous essayez de définir simultanément le symbole en même temps que le symbole est déclaré, ou si vous placez la définition dans un fichier pour la déclaration, vous risquez de mettre en colère l'éditeur de liens. Tenter d'ajouter un fichier à plusieurs fichiers d'implémentation conduit à ajouter une définition à plusieurs unités de traduction - votre éditeur de liens va pleurer.

Le mot clé extern en C et C ++ (explicitement) déclare qu'un caractère a une connexion externe.

 extern int x; extern void f(const std::string& argument); 

Les deux personnages ont une connexion externe. Il a été noté ci-dessus que les variables globales const ont une liaison interne par défaut, les variables globales non const ont une liaison externe. Cela signifie que int x; - identique à extern int x;, non? Pas vraiment. int x; en fait analogue à extern int x {}; (en utilisant la syntaxe d'initialisation universal / bracket pour éviter l'analyse la plus désagréable (l'analyse la plus contrariante)), puisque int x; non seulement déclare, mais définit également x. Par conséquent, n'ajoutez pas extern à int x; globalement est aussi mauvais que de définir une variable lors de sa déclaration externe:

 int x; //   ,   extern int x{}; //      . extern int x; //      ,   

Mauvais exemple

Déclarons une fonction f avec un lien externe dans file.hpp et définissons-la ici:

 // file.hpp #ifndef FILE_HPP #define FILE_HPP extern int f(int x); /* ... */ int f(int) { return x + 1; } /* ... */ #endif /* FILE_HPP */ 

Veuillez noter que vous n'avez pas besoin d'ajouter extern ici, car toutes les fonctions sont explicitement externes. La séparation de la déclaration et de la définition n'est pas non plus requise. Alors réécrivons-le comme ceci:

 // file.hpp #ifndef FILE_HPP #define FILE_HPP int f(int) { return x + 1; } #endif /* FILE_HPP */ 

Un tel code pourrait être rédigé avant de lire cet article, ou après l'avoir lu sous l'influence d'alcool ou de substances lourdes (par exemple, des rouleaux de cannelle).

Voyons pourquoi cela n'en vaut pas la peine. Nous avons maintenant deux fichiers d'implémentation: a.cpp et b.cpp, tous deux inclus dans file.hpp:

 // a.cpp #include "file.hpp" /* ... */ 


 // b.cpp #include "file.hpp" /* ... */ 

Laissez maintenant le compilateur fonctionner et générer deux unités de traduction pour les deux fichiers d'implémentation ci-dessus (rappelez-vous que #include signifie littéralement copier / coller):

 // TU A, from a.cpp int f(int) { return x + 1; } /* ... */ 

 // TU B, from b.cpp int f(int) { return x + 1; } /* ... */ 

À ce stade, l'éditeur de liens intervient (la liaison se produit après la compilation). L'éditeur de liens prend le caractère f et recherche une définition. Aujourd'hui, il a de la chance, il en trouve jusqu'à deux! L'un dans l'unité de traduction A, l'autre dans B. L'éditeur de liens se fige de bonheur et vous dit quelque chose comme ceci:

 duplicate symbol __Z1fv in: /path/to/ao /path/to/bo 

L'éditeur de liens trouve deux définitions pour un caractère f . Étant donné que f a une liaison externe, elle est visible par l'éditeur de liens lors du traitement de A et de B. Évidemment, cela viole la règle de la définition unique et provoque une erreur. Plus précisément, cela provoque une erreur de symbole en double, que vous recevrez pas moins qu'une erreur de symbole non définie qui se produit lorsque vous déclarez un symbole, mais que vous avez oublié de le définir.

Utiliser

Un exemple standard de déclaration de variables externes est les variables globales. Supposons que vous travaillez sur un gâteau auto-cuit. Il y a sûrement des variables globales associées au gâteau qui devraient être disponibles dans différentes parties de votre programme. Disons la fréquence d'horloge d'un circuit comestible à l'intérieur de votre gâteau. Cette valeur est naturellement requise dans différentes parties pour le fonctionnement synchrone de toute l'électronique chocolatée. La (mauvaise) façon C de déclarer une telle variable globale est une macro:

 #define CLK 1000000 

Un programmeur C ++ dégoûté des macros écrira mieux le vrai code. Par exemple, ceci:

 // global.hpp namespace Global { extern unsigned int clock_rate; } // global.cpp namespace Global { unsigned int clock_rate = 1000000; } 

(Un programmeur C ++ moderne voudra utiliser des littéraux de séparation: unsigned int clock_rate = 1'000'000;)

Interphone

Si le symbole a une connexion interne, il ne sera visible qu'à l'intérieur de l'unité de traduction actuelle. Ne confondez pas la visibilité avec les droits d'accès, tels que privés. La visibilité signifie que l'éditeur de liens ne pourra utiliser ce symbole que lors du traitement de l'unité de traduction dans laquelle le symbole a été déclaré, et pas plus tard (comme dans le cas des symboles avec communication externe). En pratique, cela signifie que lors de la déclaration d'un symbole avec un lien interne dans le fichier d'en-tête, chaque unité de diffusion qui inclut ce fichier recevra une copie unique de ce symbole. Comme si vous aviez prédéterminé chacun de ces symboles dans chaque unité de traduction. Pour les objets, cela signifie que le compilateur allouera littéralement une copie entièrement nouvelle et unique pour chaque unité de traduction, ce qui, évidemment, peut entraîner des coûts de mémoire élevés.

Pour déclarer un symbole interconnecté, le mot clé statique existe en C et C ++. Cette utilisation est différente de l'utilisation de statique dans les classes et les fonctions (ou, en général, dans tous les blocs).

Exemple

Voici un exemple:

header.hpp:

 static int variable = 42; 

file1.hpp:

 void function1(); 

file2.hpp:

 void function2(); 

file1.cpp:

 #include "header.hpp" void function1() { variable = 10; } 


file2.cpp:

 #include "header.hpp" void function2() { variable = 123; } 

main.cpp:

 #include "header.hpp" #include "file1.hpp" #include "file2.hpp" #include <iostream> auto main() -> int { function1(); function2(); std::cout << variable << std::endl; } 

Chaque unité de traduction, dont header.hpp, obtient une copie unique de la variable, en raison de sa connexion interne. Il existe trois unités de traduction:

  1. file1.cpp
  2. file2.cpp
  3. main.cpp

Lorsque function1 est appelée, une copie de la variable file1.cpp obtient la valeur 10. Lorsque function2 est appelée, une copie de la variable file2.cpp obtient la valeur 123. Cependant, la valeur renvoyée dans main.cpp ne change pas et reste égale à 42.

Espaces de noms anonymes

En C ++, il existe une autre façon de déclarer un ou plusieurs caractères liés en interne: les espaces de noms anonymes. Un tel espace garantit que les caractères déclarés à l'intérieur ne sont visibles que dans l'unité de traduction actuelle. Essentiellement, ce n'est qu'un moyen de déclarer plusieurs caractères statiques. Pendant un certain temps, l'utilisation du mot-clé statique pour déclarer un caractère lié en interne a été abandonnée au profit d'espaces de noms anonymes. Cependant, ils ont de nouveau commencé à l'utiliser en raison de la commodité de déclarer une variable ou une fonction avec une communication interne. Il y a quelques autres différences mineures sur lesquelles je ne m'attarderai pas.

En tout cas, c'est:

 namespace { int variable = 0; } 

Fait (presque) la mĂŞme chose que:

 static int variable = 0; 

Utiliser

Alors, dans quels cas utiliser des connexions internes? Les utiliser pour des objets est une mauvaise idée. La consommation de mémoire des gros objets peut être très élevée en raison de la copie pour chaque unité de traduction. Mais fondamentalement, cela provoque juste un comportement étrange et imprévisible. Imaginez que vous ayez un singleton (une classe dans laquelle vous créez une instance d'une seule instance) et soudainement plusieurs instances de votre «singleton» apparaissent (une pour chaque unité de traduction).

Cependant, la communication interne peut être utilisée pour cacher l'unité de traduction de la zone globale des fonctions d'assistance locales. Supposons qu'il existe une fonction d'assistance foo dans file1.hpp que vous utilisez dans file1.cpp. En même temps, vous avez la fonction foo dans file2.hpp utilisée dans file2.cpp. Le premier et le deuxième foo sont différents l'un de l'autre, mais vous ne pouvez pas trouver d'autres noms. Par conséquent, vous pouvez les déclarer statiques. Si vous n'ajoutez pas à la fois file1.hpp et file2.hpp à la même unité de traduction, cela se cachera les uns les autres. Si cela n'est pas fait, ils auront implicitement une connexion externe et la définition du premier foo rencontrera la définition du second, provoquant une erreur de l'éditeur de liens sur la violation de la règle d'une définition.

LA FIN

Vous pouvez toujours laisser vos commentaires et / ou questions ici ou nous rendre visite lors d'une journée portes ouvertes.

Source: https://habr.com/ru/post/fr432834/


All Articles