2.8 – Namenskollisionen und eine Einführung in Namespaces

Angenommen, Sie fahren zum ersten Mal zum Haus eines Freundes und die Ihnen angegebene Adresse lautet 245 Front Street in Mill City. Wenn Sie Mill City erreichen, ziehen Sie Ihre Karte hoch, nur um festzustellen, dass Mill City tatsächlich zwei verschiedene Vorderstraßen in der Stadt voneinander hat! Zu welchem würdest du gehen? Es sei denn, es gibt einen zusätzlichen Hinweis, der Ihnen bei der Entscheidung hilft (z. sie erinnern sich, dass sein Haus in der Nähe des Flusses liegt) Sie müssten Ihren Freund anrufen und nach weiteren Informationen fragen. Da dies verwirrend und ineffizient wäre (insbesondere für Ihren Postboten), müssen in den meisten Ländern alle Straßennamen und Hausadressen innerhalb einer Stadt eindeutig sein.

In ähnlicher Weise erfordert C ++, dass alle Bezeichner nicht mehrdeutig sind. Wenn zwei identische Bezeichner so in dasselbe Programm eingeführt werden, dass der Compiler oder Linker sie nicht unterscheiden kann, erzeugt der Compiler oder Linker einen Fehler. Dieser Fehler wird im Allgemeinen als Namenskollision (oder Namenskonflikt) bezeichnet.

Ein Beispiel für eine Namenskollision

a.cpp:

1
2
3
4
5
6

# include < iostream>
leere myFcn(int x)
{
standard::cout << x;
}

haupt.cpp:

1
2
3
4
5
6
7
8
9
10
11

# include < iostream>
leere myFcn(int x)
{
Geschlechtskrankheit::cout << 2 * x;
}
int main()
{
rückkehr 0;
}

Wenn der Compiler dieses Programm kompiliert, wird es kompiliert a.cpp und main.cpp unabhängig, und jede Datei wird ohne Probleme kompiliert.

Wenn der Linker jedoch ausgeführt wird, werden alle Definitionen in a.cpp und main.cpp zusammen, und entdecken Sie widersprüchliche Definitionen für die Funktion myFcn. Der Linker bricht dann mit einem Fehler ab. Beachten Sie, dass dieser Fehler auftritt, obwohl myFcn nie aufgerufen wird!

Die meisten Namenskollisionen treten in zwei Fällen auf:
1) Zwei (oder mehr) Definitionen für eine Funktion (oder globale Variable) werden in separate Dateien eingefügt, die in dasselbe Programm kompiliert werden. Dies führt zu einem Linkerfehler, wie oben gezeigt.
2) Zwei (oder mehr) Definitionen für eine Funktion (oder globale Variable) werden in dieselbe Datei eingefügt (häufig über ein #include). Dies führt zu einem Compilerfehler.

Wenn Programme größer werden und mehr Bezeichner verwenden, steigt die Wahrscheinlichkeit, dass eine Namenskollision eingeführt wird, erheblich. Die gute Nachricht ist, dass C ++ viele Mechanismen zur Vermeidung von Namenskollisionen bietet. Der lokale Bereich, der verhindert, dass lokale Variablen, die in Funktionen definiert sind, miteinander in Konflikt stehen, ist ein solcher Mechanismus. Der lokale Bereich funktioniert jedoch nicht für Funktionsnamen. Wie verhindern wir also, dass Funktionsnamen miteinander in Konflikt geraten?

Was ist ein Namespace?

Zurück zu unserer Adresse: Für einen Moment war es nur problematisch, zwei vordere Straßen zu haben, weil diese Straßen in derselben Stadt existierten. Wenn Sie andererseits Post an zwei Adressen zustellen müssten, eine an der 209 Front Street in Mill City und eine andere an der 417 Front Street in Jonesville, gäbe es keine Verwirrung darüber, wohin Sie gehen sollen. Anders ausgedrückt, Städte bieten Gruppierungen, die es uns ermöglichen, Adressen zu disambiguieren, die sonst miteinander in Konflikt geraten könnten. Namespaces verhalten sich in dieser Analogie wie die Städte.

Ein Namespace ist eine Region, in der Sie Namen zum Zwecke der Begriffsklärung deklarieren können. Der Namespace stellt einen Bereich (namens Namespace scope) für die darin deklarierten Namen bereit – was einfach bedeutet, dass jeder im Namespace deklarierte Name nicht mit identischen Namen in anderen Bereichen verwechselt wird.

Key insight

Ein in einem Namespace deklarierter Name wird nicht mit einem identischen Namen verwechselt, der in einem anderen Bereich deklariert ist.

Innerhalb eines Namespaces müssen alle Namen eindeutig sein, da es sonst zu einer Namenskollision kommt.

Namespaces werden häufig verwendet, um verwandte Bezeichner in einem großen Projekt zu gruppieren, um sicherzustellen, dass sie nicht versehentlich mit anderen Bezeichnern kollidieren. Wenn Sie beispielsweise alle Ihre mathematischen Funktionen in einen Namensraum namens math einfügen, kollidieren Ihre mathematischen Funktionen nicht mit Funktionen mit identischem Namen außerhalb des Math-Namensraums.

Wir werden in einer zukünftigen Lektion darüber sprechen, wie Sie Ihre eigenen Namespaces erstellen.

Der globale Namespace

In C ++ wird jeder Name, der nicht in einer Klasse, Funktion oder einem Namespace definiert ist, als Teil eines implizit definierten Namespace betrachtet, der als globaler Namespace (manchmal auch als globaler Bereich bezeichnet) bezeichnet wird.

Im Beispiel am Anfang der Lektion sind die Funktionen main() und beide Versionen von myFcn() im globalen Namespace definiert. Die im Beispiel aufgetretene Namenskollision tritt auf, weil beide Versionen von myFcn () im globalen Namespace landen, was gegen die Regel verstößt, dass alle Namen im Namespace eindeutig sein müssen.

Der STD-Namespace

Als C ++ ursprünglich entwickelt wurde, konnten alle Bezeichner in der C ++ – Standardbibliothek (einschließlich std::cin und std::cout) ohne das Präfix std:: verwendet werden (sie waren Teil des globalen Namespace). Dies bedeutete jedoch, dass jeder Bezeichner in der Standardbibliothek möglicherweise mit einem Namen in Konflikt geraten könnte, den Sie für Ihre eigenen Bezeichner ausgewählt haben (auch im globalen Namespace definiert). Code, der funktionierte, konnte plötzlich einen Namenskonflikt haben, wenn Sie eine neue Datei aus der Standardbibliothek #included . Oder schlimmer noch, Programme, die unter einer Version von C ++ kompiliert würden, könnten nicht unter einer zukünftigen Version von C ++ kompiliert werden, da neue Bezeichner, die in die Standardbibliothek eingeführt werden, einen Namenskonflikt mit bereits geschriebenem Code haben könnten. Also hat C ++ alle Funktionen in der Standardbibliothek in einen Namespace namens „std“ (kurz für Standard) verschoben.

Es stellt sich heraus, dass der Name std::cout nicht wirklich std::cout . Es ist eigentlich nur cout , und std ist der Name des Namespaces, zu dem identifier cout gehört. Da cout im std-Namespace definiert ist, steht der Name cout nicht in Konflikt mit Objekten oder Funktionen mit dem Namen cout , die wir im globalen Namespace erstellen.

Wenn Sie auf einen Bezeichner zugreifen, der in einem Namespace definiert ist (z. B. std::cout ), müssen Sie dem Compiler mitteilen, dass wir nach einem Bezeichner suchen, der im Namespace definiert ist (std).

Key Insight

Wenn Sie einen Bezeichner verwenden, der in einem Namespace definiert ist (z. B. den std-Namespace), müssen Sie dem Compiler mitteilen, dass sich der Bezeichner im Namespace befindet.

Es gibt verschiedene Möglichkeiten, dies zu tun.

Explizites Namespace-Qualifikationsmerkmal std::

Der einfachste Weg, dem Compiler mitzuteilen, dass wir cout aus dem std-Namespace verwenden möchten, besteht darin, das Präfix std:: explizit zu verwenden. Zum Beispiel:

1
2
3
4
5
6
7

# einschließen <iostream>
int main()
{
std::cout << „Hallo Welt!“; // wenn wir cout sagen, meinen wir das im std Namespace definierte cout
return 0;
}

Das :: Symbol ist ein Operator, der als Bereichsauflösungsoperator bezeichnet wird. Der Bezeichner links vom :: -Symbol gibt den Namensraum an, in dem der Name rechts vom :: -Symbol enthalten ist. Wenn kein Bezeichner links vom :: -Symbol angegeben wird, wird der globale Namespace angenommen.

Wenn wir also std::cout sagen, sagen wir „der cout, der im Namespace std lebt“.

Dies ist der sicherste Weg, cout zu verwenden, da es keine Unklarheit darüber gibt, auf welches cout wir verweisen (das im std Namespace).

Best Practice

Verwenden Sie explizite Namespace-Präfixe, um auf in einem Namespace definierte Bezeichner zuzugreifen.

Verwenden von Namespace std (und warum es zu vermeiden ist)

Eine andere Möglichkeit, auf Bezeichner in einem Namespace zuzugreifen, besteht darin, eine using directive Anweisung zu verwenden. Hier ist unser ursprüngliches „Hello World“ -Programm mit einer using Direktive:

1
2
3
4
5
6
7
8
9

# include <iostream>
using namespace std; // Dies ist eine using Direktive, die den Compiler anweist, den std Namespace zu überprüfen, wenn Bezeichner ohne Präfix aufgelöst werden
int main()
{
cout << „Hallo Welt!“; // cout hat kein Präfix, daher überprüft der Compiler, ob cout lokal oder im Namespace std
definiert ist. 0;
}

Eine using-Direktive weist den Compiler an, einen angegebenen Namespace zu überprüfen, wenn versucht wird, einen Bezeichner aufzulösen, der kein Namespace-Präfix enthält. Wenn der Compiler im obigen Beispiel ermittelt, welcher Bezeichner cout ist, überprüft er sowohl lokal (wo er undefiniert ist) als auch im std Namespace (wo er mit std::cout übereinstimmt).

Viele Texte, Tutorials und sogar einige Compiler empfehlen oder verwenden eine using Direktive am Anfang des Programms. Auf diese Weise verwendet, ist dies jedoch eine schlechte Praxis und wird dringend abgeraten.

Betrachten Sie das folgende Programm:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# include <iostream> // importiert die Deklaration von std::cout
mit Namespace std; // macht std::cout zugänglich als „cout“
int cout() // deklariert unsere eigene „cout“ -Funktion
{
rückkehr 5;
}
int main()
{
cout << „Hallo, Welt!“; // Kompilierungsfehler! Welchen Cout wollen wir hier? Der im std-Namespace oder der, den wir oben definiert haben?
zurück 0;
}

Das obige Programm wird nicht kompiliert, da der Compiler jetzt nicht erkennen kann, ob wir die von uns definierte cout-Funktion oder die im std-Namespace definierte cout möchten.

Wenn eine using-Direktive auf diese Weise verwendet wird, kann jeder von uns definierte Bezeichner mit einem identischen Bezeichner im std-Namespace in Konflikt geraten. Schlimmer noch, obwohl ein Bezeichnername heute möglicherweise nicht in Konflikt steht, kann er in zukünftigen Sprachrevisionen mit neuen Bezeichnern in Konflikt stehen, die dem std-Namespace hinzugefügt wurden. Dies war der springende Punkt, um alle Bezeichner in der Standardbibliothek überhaupt in den std Namespace zu verschieben!

Warnung

Vermeiden Sie die Verwendung von Direktiven (z. B. namespace std;) oben in Ihrem Programm. Sie verletzen den Grund, warum Namespaces überhaupt hinzugefügt wurden.

Wir werden mehr über die Verwendung von Anweisungen (und wie man sie verantwortungsvoll verwendet) in Lektion 6.12 — Using statements sprechen.

2.9 — Einführung in den Präprozessor

Inhaltsverzeichnis

2.7 — Programme mit mehreren Codedateien

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.