Guten Tag an alle!
Wir präsentieren Ihnen die Übersetzung eines interessanten Artikels, der im Rahmen des Kurses
"C ++ Developer" für Sie vorbereitet wurde. Wir hoffen, dass es sowohl für Sie als auch für unsere Zuhörer nützlich und interessant sein wird.
Lass uns gehen.
Haben Sie jemals die Begriffe interne und externe Kommunikation kennengelernt? Möchten Sie wissen, wofür das Schlüsselwort extern verwendet wird oder wie sich die Deklaration von etwas Statischem auf den globalen Bereich auswirkt? Dann ist dieser Artikel für Sie.
KurzgesagtDie Übersetzungseinheit (.c / .cpp) und alle ihre Header-Dateien (.h / .hpp) sind in der Übersetzungseinheit enthalten. Wenn ein Objekt oder eine Funktion innerhalb einer Übersetzungseinheit eine interne Bindung aufweist, ist dieses Symbol für den Linker nur innerhalb dieser Übersetzungseinheit sichtbar. Wenn das Objekt oder die Funktion einen externen Link hat, kann der Linker diesen sehen, wenn er andere Übersetzungseinheiten verarbeitet. Die Verwendung des statischen Schlüsselworts im globalen Namespace gibt dem Zeichen eine interne Bindung. Das Schlüsselwort extern gibt eine externe Bindung an.
Der Standard-Compiler gibt Zeichen die folgenden Bindungen:
- Nicht konstante globale Variablen - externe Bindung;
- Const globale Variablen - interne Bindung;
- Funktionen - Externe Verknüpfung.
Die GrundlagenLassen Sie uns zunächst über zwei einfache Konzepte sprechen, die zur Erörterung der Bindung erforderlich sind.
- Der Unterschied zwischen einer Erklärung und einer Definition;
- Broadcast-Einheiten.
Achten Sie auch auf die Namen: Wir werden das Konzept des "Symbols" verwenden, wenn es um eine "Code-Entität" geht, mit der der Linker arbeitet, beispielsweise mit einer Variablen oder Funktion (oder mit Klassen / Strukturen, aber wir werden uns nicht auf sie konzentrieren).
Ankündigung VS. DefinitionWir diskutieren kurz den Unterschied zwischen einer Deklaration und einer Symboldefinition: Eine Ansage (oder Deklaration) informiert den Compiler über die Existenz eines bestimmten Symbols und ermöglicht den Zugriff auf dieses Symbol in Fällen, in denen keine genaue Speicheradresse oder Symbolspeicherung erforderlich ist. Die Definition teilt dem Compiler mit, was im Hauptteil der Funktion enthalten ist oder wie viel Speicher die Variable zuweisen muss.
In einigen Situationen reicht eine Deklaration für den Compiler nicht aus, z. B. wenn ein Klassendatenelement eine Referenz oder einen Werttyp hat (dh keine Referenz und keinen Zeiger). Gleichzeitig ist ein Zeiger auf einen deklarierten (aber nicht definierten) Typ zulässig, da er unabhängig vom Typ, auf den er verweist, eine feste Speichermenge benötigt (z. B. 8 Byte in 64-Bit-Systemen). Um den Wert mit diesem Zeiger zu erhalten, ist eine Definition erforderlich. Um eine Funktion zu deklarieren, müssen Sie außerdem alle Parameter (unabhängig davon, ob sie nach Wert, Referenz oder Zeiger verwendet werden) und den Rückgabetyp deklarieren (aber nicht definieren). Das Bestimmen der Art des Rückgabewerts und der Parameter ist nur zum Definieren einer Funktion erforderlich.
FunktionenDer Unterschied zwischen dem Definieren und Deklarieren einer Funktion ist sehr offensichtlich.
int f();
VariablenBei Variablen ist das etwas anders. Erklärung und Definition werden normalerweise nicht geteilt. Die Hauptsache ist:
int x;
Deklariert nicht nur
x
, sondern definiert es auch. Dies ist auf den Aufruf des Standardkonstruktors int zurückzuführen. (In C ++ initialisiert der Konstruktor einfacher Typen (z. B. int) im Gegensatz zu Java den Wert standardmäßig nicht auf 0. Im obigen Beispiel entspricht x dem Müll, der in der vom Compiler zugewiesenen Speicheradresse liegt.)
Sie können die Variablendeklaration und ihre Definition jedoch explizit mit dem Schlüsselwort
extern
trennen.
extern int x;
Beim Initialisieren und Hinzufügen von
extern
zur Deklaration wird der Ausdruck jedoch zu einer Definition, und das Schlüsselwort
extern
wird unbrauchbar.
extern int x = 5;
AnzeigenvorschauIn C ++ gibt es das Konzept, ein Zeichen vorab zu deklarieren. Dies bedeutet, dass wir den Typ und den Namen des Symbols für Situationen deklarieren, für die keine Definition erforderlich ist. Daher müssen wir nicht die vollständige Definition eines Zeichens (normalerweise eine Header-Datei) einschließen, ohne dass dies offensichtlich erforderlich ist. Somit reduzieren wir die Abhängigkeit von der Datei, die die Definition enthält. Der Hauptvorteil besteht darin, dass beim Ändern einer Datei mit einer Definition die Datei, in der wir dieses Symbol vorläufig deklarieren, nicht neu kompiliert werden muss (was bedeutet, dass alle anderen Dateien, einschließlich dieser).
BeispielAngenommen, wir haben eine Funktionsdeklaration (als Prototyp bezeichnet) für f, die ein Objekt vom Typ
Class
nach Wert annimmt:
Fügen Sie sofort die Definition von
Class
- naiv hinzu. Da wir gerade
f
deklariert haben, reicht es aus, dem Compiler eine Klassendeklaration zu geben. Auf diese Weise kann der Compiler die Funktion an seinem Prototyp erkennen und die Abhängigkeit von file.hpp von der Datei, die die Definition von
Class
, wie beispielsweise class.hpp, beseitigen:
Angenommen, file.hpp ist in 100 anderen Dateien enthalten. Nehmen wir an, wir ändern die Definition von Class in class.hpp. Wenn Sie class.hpp zu file.hpp hinzufügen, müssen file.hpp und alle 100 Dateien, die es enthalten, neu kompiliert werden. Dank der vorläufigen Deklaration von Class müssen nur class.hpp und file.hpp neu kompiliert werden (vorausgesetzt, dort ist f definiert).
VerwendungshäufigkeitEin wichtiger Unterschied zwischen einer Deklaration und einer Definition besteht darin, dass ein Symbol mehrmals deklariert, aber nur einmal definiert werden kann. Sie können eine Funktion oder Klasse also beliebig oft vordeklarieren, es kann jedoch nur eine Definition geben. Dies wird als
Regel einer Definition bezeichnet . In C ++ funktioniert Folgendes:
int f(); int f(); int f(); int f(); int f(); int f(); int f() { return 5; }
Und das funktioniert nicht:
int f() { return 6; } int f() { return 9; }
Broadcast-EinheitenProgrammierer arbeiten normalerweise mit Header- und Implementierungsdateien. Aber keine Compiler - sie arbeiten mit Übersetzungseinheiten (kurz Übersetzungseinheiten - TU), die manchmal als Kompilierungseinheiten bezeichnet werden. Die Definition einer solchen Einheit ist recht einfach: Jede Datei, die nach ihrer vorläufigen Verarbeitung an den Compiler übertragen wird. Um genau zu sein, ist dies eine Datei, die aus der Arbeit eines Erweiterungsmakro-Präprozessors resultiert, der Quellcode enthält, der von den Ausdrücken
#ifdef
und
#ifndef
abhängt, und das Kopieren und Einfügen aller
#include
Dateien.
Folgende Dateien stehen zur Verfügung:
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
program.cpp:
#include "header.hpp" int strlen(const char* string) { int length = 0; while(string[length]) ++length; return length + VALUE; }
Der Präprozessor erzeugt die folgende Übersetzungseinheit, die dann an den Compiler übergeben wird:
int strlen(const char* string); int strlen(const char* string) { int length = 0; while(string[length]) ++length; return length + 5; }
KommunikationNachdem Sie die Grundlagen besprochen haben, können Sie die Beziehung beginnen. Im Allgemeinen ist Kommunikation die Sichtbarkeit von Zeichen für den Linker bei der Verarbeitung von Dateien. Die Kommunikation kann entweder extern oder intern erfolgen.
Externe KommunikationWenn ein Symbol (Variable oder Funktion) eine externe Verbindung hat, wird es für Linker aus anderen Dateien sichtbar, dh "global" sichtbar und für alle Übersetzungseinheiten zugänglich. Dies bedeutet, dass Sie ein solches Symbol an einer bestimmten Stelle einer Übersetzungseinheit definieren müssen, normalerweise in der Implementierungsdatei (.c / .cpp), damit es nur eine sichtbare Definition hat. Wenn Sie versuchen, das Symbol gleichzeitig mit der Deklaration des Symbols zu definieren, oder wenn Sie die Definition in eine Datei für die Deklaration einfügen, besteht die Gefahr, dass Sie den Linker verärgern. Der Versuch, eine Datei zu mehr als einer Implementierungsdatei hinzuzufügen, führt dazu, dass mehr als einer Übersetzungseinheit eine Definition hinzugefügt wird - Ihr Linker wird weinen.
Das Schlüsselwort extern in C und C ++ deklariert (explizit), dass ein Zeichen eine externe Verbindung hat.
extern int x; extern void f(const std::string& argument);
Beide Zeichen haben eine externe Verbindung. Es wurde oben angemerkt, dass konstante globale Variablen standardmäßig eine interne Bindung haben, nicht konstante globale Variablen eine externe Bindung. Dies bedeutet, dass int x; - wie extern int x ;, richtig? Nicht wirklich. int x; eigentlich analog zu extern int x {}; (Verwenden der Universal / Bracket-Initialisierungssyntax, um das unangenehmste Parsen (das ärgerlichste Parsen) zu vermeiden), da int x; deklariert nicht nur, sondern definiert auch x. Fügen Sie daher int nicht extern zu int x hinzu. global ist so schlecht wie das Definieren einer Variablen, wenn sie extern deklariert wird:
int x;
Schlechtes BeispielDeklarieren wir eine Funktion
f
mit externem Link in file.hpp und definieren sie dort:
Bitte beachten Sie, dass Sie hier kein externes hinzufügen müssen, da alle Funktionen explizit extern sind. Eine Trennung von Erklärung und Definition ist ebenfalls nicht erforderlich. Schreiben wir es also einfach so um:
Ein solcher Code könnte vor dem Lesen dieses Artikels oder nach dem Lesen unter dem Einfluss von Alkohol oder schweren Substanzen (z. B. Zimtschnecken) geschrieben werden.
Mal sehen, warum sich das nicht lohnt. Jetzt haben wir zwei Implementierungsdateien: a.cpp und b.cpp, beide in file.hpp enthalten:
Lassen Sie nun den Compiler arbeiten und generieren Sie zwei Übersetzungseinheiten für die beiden obigen Implementierungsdateien (denken Sie daran, dass
#include
wörtlich Kopieren / Einfügen bedeutet):
// TU A, from a.cpp int f(int) { return x + 1; } /* ... */
// TU B, from b.cpp int f(int) { return x + 1; } /* ... */
Zu diesem Zeitpunkt greift der Linker ein (die Bindung erfolgt nach der Kompilierung). Der Linker nimmt das Zeichen
f
und sucht nach einer Definition. Heute hat er Glück, er findet bis zu zwei! Einer in Übersetzungseinheit A, der andere in B. Der Linker friert vor Glück ein und sagt Ihnen so etwas:
duplicate symbol __Z1fv in: /path/to/ao /path/to/bo
Der Linker findet zwei Definitionen für ein
f
Zeichen. Da
f
eine externe Bindung hat, ist sie für den Linker sichtbar, wenn sowohl A als auch B verarbeitet werden. Dies verstößt offensichtlich gegen die Regel einer Definition und verursacht einen Fehler. Genauer gesagt führt dies zu einem doppelten Symbolfehler, den Sie nicht weniger als einen undefinierten Symbolfehler erhalten, der auftritt, wenn Sie ein Symbol deklarieren, aber vergessen haben, es zu definieren.
Verwenden SieEin Standardbeispiel für die Deklaration externer Variablen sind globale Variablen. Angenommen, Sie arbeiten an einem selbstbackenden Kuchen. Sicherlich gibt es globale Variablen, die mit dem Kuchen verbunden sind und in verschiedenen Teilen Ihres Programms verfügbar sein sollten. Angenommen, die Taktfrequenz eines essbaren Schaltkreises in Ihrem Kuchen. Dieser Wert wird natürlich in verschiedenen Teilen für den Synchronbetrieb der gesamten Schokoladenelektronik benötigt. Der (böse) C-Weg, eine solche globale Variable zu deklarieren, ist ein Makro:
#define CLK 1000000
Ein C ++ - Programmierer, der von Makros angewidert ist, schreibt echten Code besser. Zum Beispiel:
(Ein moderner C ++ - Programmierer möchte Trennungsliterale verwenden: unsigned int clock_rate = 1'000'000;)
GegensprechanlageWenn das Symbol eine interne Verbindung hat, ist es nur innerhalb der aktuellen Übersetzungseinheit sichtbar. Verwechseln Sie Sichtbarkeit nicht mit Zugriffsrechten wie privat. Sichtbarkeit bedeutet, dass der Linker dieses Symbol nur bei der Verarbeitung der Übersetzungseinheit verwenden kann, in der das Symbol deklariert wurde, und nicht später (wie bei Symbolen mit externer Kommunikation). In der Praxis bedeutet dies, dass beim Deklarieren eines Symbols mit einem internen Link in der Header-Datei jede Broadcast-Einheit, die diese Datei enthält, eine eindeutige Kopie dieses Symbols erhält. Als hätten Sie jedes dieser Symbole in jeder Übersetzungseinheit vorgegeben. Für Objekte bedeutet dies, dass der Compiler jeder Übersetzungseinheit buchstäblich eine völlig neue, eindeutige Kopie zuweist, was natürlich zu hohen Speicherkosten führen kann.
Um ein miteinander verbundenes Symbol zu deklarieren, ist das statische Schlüsselwort in C und C ++ vorhanden. Diese Verwendung unterscheidet sich von der Verwendung von static in Klassen und Funktionen (oder im Allgemeinen in beliebigen Blöcken).
BeispielHier ist ein Beispiel:
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; }
Jede Übersetzungseinheit, einschließlich header.hpp, erhält aufgrund ihrer internen Verbindung eine eindeutige Kopie der Variablen. Es gibt drei Übersetzungseinheiten:
- file1.cpp
- file2.cpp
- main.cpp
Wenn function1 aufgerufen wird, erhält eine Kopie der Variablen file1.cpp den Wert 10. Wenn function2 aufgerufen wird, erhält eine Kopie der Variablen file2.cpp den Wert 123. Der in main.cpp zurückgegebene Wert ändert sich jedoch nicht und bleibt gleich 42.
Anonyme NamespacesIn C ++ gibt es eine andere Möglichkeit, ein oder mehrere intern verknüpfte Zeichen zu deklarieren: anonyme Namespaces. Ein solches Leerzeichen stellt sicher, dass die darin deklarierten Zeichen nur in der aktuellen Übersetzungseinheit sichtbar sind. Im Wesentlichen ist dies nur eine Möglichkeit, mehrere statische Zeichen zu deklarieren. Für eine Weile wurde die Verwendung des statischen Schlüsselworts zum Deklarieren eines intern verknüpften Zeichens zugunsten anonymer Namespaces aufgegeben. Sie begannen jedoch erneut damit, eine Variable oder Funktion mit interner Kommunikation zu deklarieren. Es gibt noch ein paar kleinere Unterschiede, auf die ich nicht näher eingehen werde.
In jedem Fall ist dies:
namespace { int variable = 0; }
Tut (fast) dasselbe wie:
static int variable = 0;
Verwenden SieIn welchen Fällen sollten interne Verbindungen verwendet werden? Es ist eine schlechte Idee, sie für Objekte zu verwenden. Der Speicherverbrauch großer Objekte kann aufgrund des Kopierens für jede Übersetzungseinheit sehr hoch sein. Aber im Grunde verursacht es nur seltsames, unvorhersehbares Verhalten. Stellen Sie sich vor, Sie haben einen Singleton (eine Klasse, in der Sie nur eine Instanz einer Instanz erstellen) und plötzlich erscheinen mehrere Instanzen Ihres „Singletons“ (eine für jede Übersetzungseinheit).
Interne Kommunikation kann jedoch verwendet werden, um die Übersetzungseinheit vor dem globalen Bereich lokaler Hilfsfunktionen zu verbergen. Angenommen, in file1.hpp gibt es eine foo-Hilfsfunktion, die Sie in file1.cpp verwenden. Gleichzeitig haben Sie die Funktion foo in file2.hpp, die in file2.cpp verwendet wird. Das erste und das zweite foo unterscheiden sich voneinander, aber Sie können sich keine anderen Namen einfallen lassen. Daher können Sie sie als statisch deklarieren. Wenn Sie nicht sowohl file1.hpp als auch file2.hpp zur selben Übersetzungseinheit hinzufügen, wird foo voneinander ausgeblendet. Wenn dies nicht getan wird, haben sie implizit eine externe Verbindung und die Definition des ersten foo trifft auf die Definition des zweiten, was einen Linkerfehler über die Verletzung der Regel einer Definition verursacht.
DAS ENDE
Sie können hier jederzeit Ihre Kommentare und / oder Fragen hinterlassen oder uns an einem
Tag der offenen Tür besuchen
.