[Zurück] [Index] [Weiter]     Schader/Kuhlins: Programmieren in C++


19  Namensbereiche

Alle Namen, die außerhalb sämtlicher Blöcke oder Klassen deklariert werden, sind globale Namen (vgl. Abschnitt 10.1); sie liegen im globalen Namensbereich. Hier können sich Probleme ergeben, wenn man gleichzeitig mehrere Klassenbibliotheken einsetzt und diese dieselben Namen zur Bezeichnung ihrer Klassen, Funktionen oder Konstanten verwenden. Die auftretenden Namenskonflikte können von Benutzern, denen die Bibliotheken typischerweise nur in übersetzter Form vorliegen, dann nicht gelöst werden.

19.1  Einleitung

In einem Programm soll aus einer Klassenbibliothek A eine Folge und aus der Bibliothek B ein Baum wiederverwendet werden. Beide Bibliotheken definieren für ihre Zwecke eine "allgemeine" Basisklasse Objekt, von der alle Klassen abzuleiten sind, die in der Folge verwaltet werden sollen bzw. als Knoten in einem Baum enthalten sein können. In der Header-Datei a.h der Bibliothek A befinden sich u.a. die beiden Definitionen

class Objekt { /* ... */ };

struct Folge {
    void fuegeEin(Objekt*);
    struct Element {
        // ...
        Objekt* obj;
    };
    Element* erstes;
    // ...
};

und in b.h, der Header-Datei der Bibliothek B, findet man die Definitionen

class Objekt { /* ... */ };

class Baum {
    // ...
    struct Knoten {
        // ...
        Objekt* zgrO;
    };
    Knoten* wurzel;
public:
    void balanciere();
};

Der Versuch, beide Bibliotheken gemeinsam, z.B. wie folgt, in einem Programm zu benutzen, schlägt jedoch fehl:

#include "a.h"
#include "b.h"

Folge f;
Baum b;

Da beide Bibliotheken denselben Namen für die allgemeine Basisklasse verwenden, gibt es für die Klasse Objekt zwei Definitionen, und das Programm kann nicht übersetzt werden. Wenn nicht für mindestens eine der Bibliotheken die C++-Quellen verfügbar sind, besteht keine Möglichkeit, das aufgetretene Problem durch Umbenennen einer der Klassen zu lösen. Der herkömmliche Versuch, solchen Fehlern von seiten der Bibliothekshersteller vorzubeugen, ist das Versehen der Klassennamen mit einem Präfix, z.B. AObjekt, AFolge, BObjekt, BBaum usw. Bei kurzen Vorsilben wie A oder B treten die genannten Probleme u.U. früher oder später wieder auf. Und sehr lange Klassennamen, z.B. OOTechGraphenUndNetzeBibBaum, sind unpraktisch und verschleiern die Klassennamen. Die beste Problemlösung liefern Namensbereiche, in denen Deklarationen zusammengefaßt werden können, die dann nicht mehr zum globalen Namensbereich gehören, sondern einen eigenständigen Geltungsbereich bilden.

19.2  Die Definition von Namensbereichen

Die Definition eines Namensbereichs wird mit dem Schlüsselwort namespace eingeleitet und muß global (oder in einem anderen Namensbereich) erfolgen. Soll der Namensbereich benannt werden, folgt vor der Aufzählung seiner Elemente ein Bezeichner.

Namespace-Definition:
    namespace Bezeichneropt { Deklarationsfolgeopt }

Zwischen den Klammern { und } werden die Elemente des Namensbereichs deklariert. Zum Beispiel wird mit

namespace A {
    class Objekt { /* ... */ };
    class Folge { /* ... */ };
}

ein Namensbereich A mit den beiden Elementen Objekt und Folge definiert. Im Unterschied zu anderen Geltungsbereichen kann ein Namensbereich innerhalb einer Programmdatei in mehrere zusammengehörende Teile zerlegt werden. Die einzelnen Teile müssen sich dann alle im selben (z.B. globalen) Geltungsbereich befinden. Zum Beispiel

namespace B {
    class Objekt { /* ... */ };  // Teil 1
}

// ...

namespace B {
    class Baum { /* ... */ };    // Teil 2
}

Hier wird der Namensbereich B mit den beiden Elementen Objekt und Baum in zwei Teilen definiert. Ein Namensbereich kann somit auch auf mehrere Header-Dateien verteilt werden. Auf die Namen der Elemente eines Namensbereichs kann vollständig qualifiziert zugegriffen werden:

A::Folge f;
B::Baum b;

Gleiche Namen in unterschiedlichen Namensbereichen führen nicht zu Mehrdeutigkeiten, da sie durch ihre Qualifizierung unterscheidbar sind wie z.B. die Klassen A::Objekt und B::Objekt. Bemerkungen:

  1. Es gibt keinen Unterschied zwischen Deklarationen und Definitionen von Namensbereichen - jede Deklaration ist auch gleichzeitig Definition des Namensbereichs.
  2. Ein Namensbereich kann andere Namensbereiche enthalten. Zum Beispiel
    namespace X {
        namespace Y {
            void f();
        }
    }

    Der Zugriff erfolgt hier - analog zum Zugriff auf eingebettete Klassen - vollständig qualifiziert mittels X::Y::f().

  3. Ein Name, der außerhalb aller benannten Namensbereiche, aller Blöcke und Klassen deklariert wird, gehört zum globalen Namensbereich. Falls erforderlich - oder um auszuschließen, daß er verdeckt ist -, kann auf einen globalen Namen mit dem einstelligen globalen Geltungsbereichoperator :: zugegriffen werden (vgl. Abschnitt 10.1).
    int fehlerCode;
    
    class X {
        // ...
        void fehler();
    };
    
    void X::fehler() {
        if (err == 0) ::fehlerCode = 0;
        else if (err > 0) { /* ... */ }
        else { /* ... */ }
    }
  4. Die Namen der Elemente eines Namensbereichs haben grundsätzlich externe Bindung. Wie bei globalen Namen kann mittels static, inline oder const interne Bindung hergestellt werden (vgl. Kapitel 12).

19.3  Die Definition der Elemente eines Namensbereichs

Die Elemente eines (benannten oder unbenannten) Namensbereichs können innerhalb des Namensbereichs nicht nur deklariert sondern auch definiert werden:

namespace X {
    void f() { /* ... */ }
}

In diesem Fall darf sich die Definition des Namensbereichs nicht in einer Header-Datei befinden, die mehrfach in ein Programm eingefügt wird. Von dieser Einschränkung sind Definitionen von Klassen, inline-Funktionen, Aufzählungstypen und Konstanten mit interner Bindung ausgenommen, da sie in jede Programmdatei (höchstens) einmal aufgenommen werden können (vgl. beispielsweise S. 213). Die Elemente eines benannten Namensbereichs können auch außerhalb des Namensbereichs definiert werden, wenn ihr Name mit dem Namen des Namensbereichs qualifiziert wird.

namespace T {
    enum tag { Mo, Di, Mi, Do, Fr, Sa, So };
    tag naechsterTag(tag);
}

T::tag T::naechsterTag(tag t) {
    return (t == So) ? Mo : static_cast<tag>(t + 1);
}

Wie im Beispiel dargestellt, können Elemente des Namensbereichs in einer Definition ohne Qualifizierung angesprochen werden, nachdem klar ist, zu welchem Namensbereich die Definition gehört. Ähnlich wie bei der Qualifizierung der Namen von Elementfunktionen in Abschnitt 14.3 wird auch die Definition eines Namensbereichs-Elements durch die Verwendung des Geltungsbereichoperators in den Geltungsbereich ihres Namensbereichs aufgenommen. Im folgenden Beispiel ist der Geltungsbereich des Namensbereichs Y dunkler schattiert dargestellt als der von X.

namespace.gif (3866 bytes)

Wenn ein benannter Namensbereich ausschließlich Deklarationen, Klassendefinitionen, inline-Funktionen, Aufzählungstypen oder Konstanten enthält, kann er in einer Header-Datei stehen, die mehrfach in ein Programm eingebunden wird. Z.B.

// Header-Datei n.h

#ifndef _N_H
#define _N_H

#include <iostream.h>

namespace A {
    class X {
    public:
        X(int = 0);
        int xWert() const;
    private:
        int x;
    };
    extern int i;
    inline int f() { return i*(i + 1)/2; }
}

#endif

Die noch fehlenden Definitionen werden in diesem Fall - analog zur Definition von Elementfunktionen und static Datenelementen einer Klasse - am besten in eine eigene Datei (n.cpp) aufgenommen, die getrennt übersetzbar ist. Zum Beispiel

#include "n.h"

namespace A {
    X::X(int n) { x = n; }
    int X::xWert() const { return x; }
    int i = 10;
}

Hier haben wir von der Möglichkeit Gebrauch gemacht, den Namensbereich fortzusetzen. Mit vollständiger Qualifizierung kann man dieselbe Wirkung erzielen:

#include "n.h"

A::X::X(int n) { x = n; }
int A::X::xWert() const { return x; }
int A::i = 10;

In prog-23 wird der Namensbereich A genutzt, indem dort (wie üblich) die Header-Datei eingefügt wird und die Datei mit den bereits übersetzen Definitionen hinzugebunden wird.

// prog-23

#include "n.h"

int main() {
    int j = 10*A::f();
    A::X feld[3] = { A::X(j), A::X(A::i), A::X() };
    A::X* z = feld;
    for (int i = 0; i < 3; i++)
        cout << z++->xWert() << endl;
    return 0;
}
// f() und h() noch nicht deklariert
namespace X {
    class C {
        friend void f();  // X::f
    };
    void g() {
        extern void h();  // X::h
    }
}

19.4  Aliasnamen

Bei sehr kurzen Bezeichnern von Namensbereichen, wie z.B. A und B für das Problem aus Abschnitt 19.1, besteht die Gefahr, daß durch die Verwendung gleicher Namensbereich- und Elementnamen die alten Namenskonflikte erneut auftreten. Bei der Implementation von Klassenbibliotheken wird man daher i.d.R. sehr lange Bezeichner für Namensbereiche wählen. Zur Vereinfachung des Zugriffs auf die Elemente dieser Namensbereiche können von den Anwendern auch Aliasnamen für Namensbereiche definiert werden:

Namespace-Alias-Definition:
    namespace Bezeichner = Namespace-Name ;
Namespace-Name:
    ::opt Bezeichner
    Namespace-Name :: Bezeichner

Damit ist es möglich, lange Bezeichner oder mehrfach qualifizierte Namen durch ein kürzeres Synonym zu ersetzen. Zum Beispiel

namespace OOTechGraphenUndNetzeBib {
    class Baum { /* ... */ };
    class BinaerBaum { /* ... */ };
    class Wald { /* ... */ };
    // ...
}

namespace GUN = OOTechGraphenUndNetzeBib;
GUN::Wald w1;  // statt: OOTechGraphenUndNetzeBib::Wald w1;

namespace XYZ = Xnsp::Ynsp::Znsp;

Ebenso wie die Vergabe von Aliasnamen für Namensbereiche verfolgt die im folgenden Abschnitt erläuterte using-Deklaration das Ziel, den Zugriff auf die Elemente eines Namensbereichs zu vereinfachen.

19.5  using-Deklarationen

Mit einer using-Deklaration wird ein Name aus einem Namensbereich in den Geltungsbereich eingeführt, in dem die using-Deklaration erfolgt.

Using-Deklaration:
    using Namespace-Name :: Bezeichner ;

Das bezeichnete Element wird dadurch deklariert; dabei handelt es sich um eine "normale" Deklaration, mit der man eine anderorts definierte Größe in einen Geltungsbereich einbringt. Typ, Bindung oder andere Eigenschaften des Namensbereich-Elements werden nicht beeinflußt. Sein Name kann jedoch anschließend ohne Qualifizierung benutzt werden - dies ist vorteilhaft, wenn viele Zugriffe nötig sind. Die Funktion main() aus prog-23 kann man beispielsweise auch so implementieren:

int j = 10*A::f();
using A::X;
X feld[3] = { X(j), X(A::i), X() };
X* z = feld;
// ...

Auf Elemente, die mittels einer using-Deklaration innerhalb eines anderen Namensbereichs deklariert wurden, kann ebenso wie auf die übrigen Elemente dieses Namensbereichs vollständig qualifiziert zugegriffen werden. Zum Beispiel

namespace A {
    void g();
}

namespace B {
    void f();
    using A::g;  // g() aus A
}

void test() {
    B::f();  // ruft f() aus B auf
    B::g();  // ruft g() aus A auf
}

Bei solchen Konstruktionen ist nicht mehr auf Anhieb erkennbar, welches Element gemeint ist. Wenn innerhalb eines Geltungsbereichs eine Menge von lokalen Deklarationen und using-Deklarationen für denselben Namen angegeben ist, so müssen sich diese entweder auf ein und dasselbe Objekt oder auf überladene Funktionen beziehen:

namespace A {
    int i;
    int h(int);
    int h(double);
}

void g() {
    int i;
    using A::i;  // Fehler
    int h(char);
    using A::h;
    // ...
}

Hier ist die using-Deklaration von A::i ein Fehler, weil in g() bereits eine lokale Variable i deklariert ist. Nach using A::h sind A::h(int) und A::h(double) in g() ohne Qualifizierung aufrufbar.

19.6  using-Direktiven

Durch eine using-Direktive können sämtliche Namen des angegebenen Namensbereichs für den Geltungsbereich zugreifbar gemacht werden, in dem die using- Direktive enthalten ist.

Using-Direktive:
    using namespace Namespace-Name ;

Der Zugriff kann anschließend ohne Qualifizierung erfolgen. Die using-Direktive wirkt sich dabei so aus, als seien alle Elemente außerhalb ihres Namensbereichs deklariert, und zwar an der Stelle, an der die Namensbereich-Definition tatsächlich steht. Zum Beispiel haben die beiden folgenden Programmfragmente dieselbe Wirkung.

namespace N {
    class X { /* ... */ };
    void f();
}

void test() {
    using namespace N;
    X x;
    f();
}
class X { /* ... */ };
void f();


void test() {

    X x;
    f();
}

Eine using-Direktive führt keine neuen Namen in den Geltungsbereich ein, in dem sie sich befindet. Darin unterscheidet sie sich von der using-Deklaration (vgl. Übungsaufgabe 1). using-Direktiven sind insbesondere zur Sicherung der Kompatibilität in der Übergangsphase der C++-Programmierung zur Standardverwendung von Namensbereichen bei der Entwicklung von Klassenbibliotheken sinnvoll einsetzbar. Zum Beispiel ist ein "altes" Anwendungsprogramm

#include "liste.h"

int main() {
    Liste l;
    // ...
    return 0;
}

auch dann noch unverändert übersetzbar, wenn die Header- Datei mit der Klassendefinition der Liste und die Datei mit den Definitionen der Elementfunktionen bei Überarbeitungen in einen Namensbereich aufgenommen werden - sofern die Header-Datei mit einer entsprechenden using-Direktive abgeschlossen wird:

// Header-Datei: liste.h

namespace OOTechContainerBib {
    class Liste { /* ... */ };
}

using namespace OOTechContainerBib;

Auch die C++-Standardbibliothek macht von dieser Technik Gebrauch, indem die alten Header-Dateien (mit Endung .h) die neuen (ohne .h) einbinden und die using-Direktive using namespace std; ergänzen (vgl. Abschnitt 2.7).

19.7  Der Namensbereich std

Im zukünftigen Sprachstandard werden sämtliche Bezeichner der C++-Standardbibliothek zum Namensbereich std gehören. Die Deklarationen befinden sich in den neuen Header- Dateien. Das elementare "Hello, world!"-Programm wird damit zu:

#include <iostream>

int main() {
    std::cout << "Hello, world!" << std::endl;
    return 0;
}

Sofern Bezeichner der Standardbibliothek nicht anderweitig benutzt werden - was aus Gründen der Lesbarkeit generell zu empfehlen ist - ist der Einsatz einer using-Direktive auch in einem Anwendungsprogramm gerechtfertigt, weil durch die vollständige Qualifizierung kein Vorteil erzielt wird.

#include <iostream>

int main() {
    using namespace std;
    cout << "Hello, world!" << endl;
    return 0;
}

19.8  Unbenannte Namensbereiche

Der Bezeichner in einer Namensbereich-Definition ist optional, d.h. es ist möglich, einen unbenannten Namensbereich zu definieren, z.B.

namespace {
    int i;
    double x;
    bool c(char, char);
}

Eine derartige Definition wird intern durch folgende Konstruktion realisiert:

namespace EindeutigerBezeichner {
    int i;
    double x;
    bool c(char, char);
}

using namespace EindeutigerBezeichner;

wobei EindeutigerBezeichner ein vom Compiler vergebener Bezeichner ist, der sich von allen anderen Bezeichnern des Programms unterscheidet. Alle unbenannten Namensbereiche einer Programmdatei werden zu "dem" unbenannten Namensbereich dieser Datei zusammengefaßt. (Der interne Bezeichner dieses Namensbereichs enthält i.d.R. den Namen seiner Programmdatei.) Aufgrund der obigen Konstruktion und weil kein Name zur Qualifizierung vorhanden ist, wird auf die Elemente des unbenannten Namensbereichs ohne Qualifizierung zugegriffen. Die Elemente des unbenannten Namensbereichs sind somit nur in ihrer jeweiligen Programmdatei zugreifbar, auch wenn sie externe Bindung haben. Unbenannte Namensbereiche bieten hier also eine Alternative zur Verwendung des Schlüsselworts static, wenn die Sichtbarkeit von globalen Größen (Variablen, Funktionen usw.) auf eine einzelne Programmdatei eingeschränkt werden soll. (Vgl. hierzu die Anmerkung auf S. 138.)

19.9  Bindung

In Ergänzung zu den Herleitungen aus Kapitel 12 können wir jetzt den Unterschied zwischen Namen mit interner Bindung und Namen ohne Bindung besser durch Beispiele veranschaulichen. Einen Namen ohne Bindung kann man nur in seinem Geltungsbereich (und nur innerhalb seiner Programmdatei) verwenden; ein Zugriff aus einem anderen Geltungsbereich ist nicht möglich. Zum Beispiel ist

void f(int);

int main() {
    for (int i = 0; i < 10; ++i)
        f(i);
    return 0;
}

void f(int j) {
    i += j;  // Fehler: kein Zugriff auf i
}

fehlerhaft, da die Variable i lokal für die for-Anweisung in main() ist und man in f() nicht auf sie zugreifen kann. Dagegen kann man auf Namen mit interner Bindung auch aus anderen Geltungsbereichen derselben Programmdatei zugreifen, wie es das folgende Beispiel für die Namen f und a zeigt:

namespace X {
    inline void f(int i) {
        cout << "X::f(" << i << ')' << endl;
    }
    const int a = -707;
    // f und a haben interne Bindung
}

void g(int i) {
    X::f(i);       // Zugriff aus anderem Geltungsbereich
}

int main() {
    int i = X::a;  // Zugriff aus anderem Geltungsbereich
    g(i);
    return 0;
}

Und auf Namen mit externer Bindung kann auch aus anderen Geltungsbereichen anderer Programmdateien zugreifen; siehe die Beispiele in Kapitel 12.

19.10  Übungsaufgaben

  1. Weshalb ist die Einführung sämtlicher Namen eines Namensbereichs mit using-Deklarationen nicht äquivalent zu der Angabe einer using-Direktive? Vergleichen Sie, wie ihr Compiler auf die folgenden beiden Programme reagiert.

    namespace X {
         int i;
         double x;
    }
    
    int main() {
         int i = 1;
         X::i = 10;
         using X::i;
         using X::x;
         cout << i;
         return 0;
    }
    namespace X {
         int i;
         double x;
    }
    
    int main() {
         int i = 1;
         X::i = 10;
         using namespace X;
         cout << i;
         return 0;
    }
  2. Gesetzt den Fall, die beiden Namensbereiche zweier unterschiedlicher Klassenbibliotheken tragen denselben Namen. Ist es dann möglich, in einem Programm beide Bibliotheken gleichzeitig zu benutzen?
  3. Benutzen Sie in Aufgabe 4b von Kapitel 12 statt static unbenannte Namensbereiche.
  4. Bringen Sie das folgende Programm zum Laufen. Überlegen Sie sich dazu verschiedene Möglichkeiten.
    #include <iostream.h>
    
    namespace T {
        enum tag { Mo, Di, Mi, Do, Fr, Sa, So };
        tag naechsterTag(tag);  // Definition wie oben
    }
    
    void fkt() {
        tag x = Fr;
        x = naechsterTag(x);
        cout << x << endl;
    }
    
    int main() {
        fkt();
        return 0;
    }