C++ Tutorial

Dynamische Eigenschaften und Objekte

Intrinsische dynamische Eigenschaften

Enthalten Objekte z.B. Felder, deren Größe von Objekt zu Objekt variiert, werden diese bei der Definition des Objekts dynamisch angelegt. Ein Beispiel hierfür ist die im Kapitel über Konstruktor und Destruktor im Beispiel erwähnte Klasse Stack. War dort die Stackgröße fest vorgegeben, kann sie durch eine entsprechende dynamische Eigenschaft variable gestaltet werden.

Für eine dynamische Eigenschaft wird innerhalb der Klasse ein entsprechender Zeiger definiert. Der für die Eigenschaft notwendige Speicherplatz wird dann in der Regel im Konstruktor der Klasse mittels new reserviert.

Wird das Objekt gelöscht, wird im Destruktor der reservierte Speicher mit delete wieder freigegeben.

1: // Klassendefinition
2: class Stack
3: {
4:     short *pArray;
5: public:
6:     Stack (int size) // ctor
7:     {
8:        // Stack fuer size-Eintraege anlegen
9:        pArray = new short[size];
10:    }
11:    ~Stack()       // dtor
12:    {
13:       delete [] pArray;
14:    }
15: };

Dynamische Objekte

Um ein Objekt dynamisch zu erstellen, erhält der Operator new als Operand den Namen der entsprechenden Klasse. Besitzt die Klasse einen Konstruktor mit Parameter (wie im nachfolgenden Beispiel), sind die Argumente nach dem Klassennamen in Klammern anzugeben.

Wird das Objekt nicht mehr benötigt, muss es wieder gelöscht werden. Dies erfolgt ebenfalls mit dem Operator delete, der den von new zurückgelieferten Zeiger als Operand erhält.

Der Zugriff auf die Member des erstellten Objekts erfolgt, wie bei Zeiger üblich, über den Zeigeroperator ->.

1: // Klassendefinition
2: class Stack
3: {
4:    ...
5: public:
6:    Stack(int size);
7:    ~Stack();
8:    bool Push(short val);
9:    ...
10: };
11: // main() Funktion
12: int main()
13: {
14:    // Stackobjekt dynamisch anlegen
15:    auto pMyStack = new Stack{10};
16:    // Methode des dyn. Objekts aufrufen
17:    auto ret = pMyStack->Push(5);
18:    ...
19:    // Stackobjekt löschen, ruft dtor von Stack auf
20:    delete pMyStack;
21: }

Dynamische Objektfelder

Definieren von dynamischen Objektfeldern

Das Erstellen eines dynamischen Objektfeldes erfolgt analog zum Erstellen von dynamischen intrinsischen Feldern, nur erhält new jetzt als Operand den Klassennamen.

1: // Klassendefinition
2: class Window
3: {
4:    ...
5: public:
6:    Window();
7:    ...
8: };
9: // main() Funktion
10: int main()
11: {
12:    // Objektfeld für 5 Window-Objekte dynamisch anlegen
13:    auto pWinArray = new Window[5];
14:    ...
15: }

Um Objekte in einem dynamischen Objektfeld zu initialisieren, sind die Daten in einer Initialisiererliste anzugeben. Enthält die Initialisiererliste weniger Daten als Objekte im Objektfeld vorhanden sind, werden die restlichen Objekte mit dem Standardkonstruktor (parameterloser Konstruktor) initialisiert. Enthält die Initialisiererliste mehr Daten als Objekte im Feld, meldet der Compiler beim Übersetzen einen Fehler.

1: class Position
2: {
3:    int xPos,yPos; //Eigenschaften
4: public:
5:    // ctor mit Parameter
6:    Position(int x, int y): xPos(x), yPos(y)
7:    {}
8:    // Standard-ctor
9:    Position(): xPos(0), yPos(0)
10:   {}
11: };
12:
13: int main()
14: {
15:    // Groesse des dynamischen Feldes
16:    constexpr int GROESSE = 5;
17:    // Objektfeld anlegen und die ersten beiden
18:    // 2 Elemente im Feld initialisieren. Die restlichen
19:    // 3 Elemente werden mit dem Standard-ctor initialisert
20:    Position *ptr =
21:              new Position[GROESSE] {{10,20},{110,120}};
22:    ...
23: }

Zugriff auf Member in dynamischen Objektfeldern

Der Zugriff auf Member im Objektfeld kann auf zweierlei Arten erfolgen.

Die erste Zugriffsart verwendet Zeigerarithmetik. Wie erwähnt, wird bei einer Addition eines Werts X auf einen Zeiger vom Typ DTYP der Zeiger um X*sizeof(DTYP) erhöht.

Alternativ kann anstelle des Zugriffs über einen Zeiger der indizierte Zugriff verwendet werden.

1: // Klassendefinition
2: class Window
3: {
4:    ...
5: public:
6:    Window();
7:    void Draw() const;
8:    ...
9: };
10: // main() Funktion
11: int main()
12: {
13:    // Objektfeld dynamisch anlegen
14:    auto pWinArray = new Window[5];
15:    ...
16:    // Aufruf der Methode Draw für das 2. Fenster
17:    (pWinArray+1)->Draw();
18:    // Aufruf der Methode Draw für das 4. Fenster
19:    pWinArray[3].Draw();
20:    ...
21: }

Löschen von dynamischen Objektfeldern

Wird ein Objektfeld nicht mehr benötigt, ist es zu löschen. Hierbei ist darauf zu achten, dass beim delete Operator die für Felder notwendigen eckigen Klammern angegeben werden. Sehen wir uns zur Demonstration einmal an was passiert, wenn diese eckigen Klammern aus Versehen vergessen werden.

Zunächst der korrekte Fall. Beim Anlegen des Feldes wird für jedes Objekt dessen Konstruktor aufgerufen. Wird das Feld gelöscht, muss für jedes Objekt im Feld auch dessen Destruktor aufgerufen werden.

1: // Klassendefinition
2: class Window
3: {
4:    ...
5: public:
6:    Window()
7:    { std::cout << "ctor von Window\n"; }
8:    ~Window()
9:    { std::cout << "dtor von Window\n"; }
10:   ...
11: };
12: // main() Funktion
13: int main()
14: {
15:    // Objekt dynamisch anlegen
16:    auto pWinArray = new Window[3];
17:    ...
18:    // Objektfeld loeschen
19:    delete [] pWinArray;
20: }

ctor von Window
ctor von Window
ctor von Window
dtor von Window
dtor von Window
dtor von Window

Nun der Fall, dass beim Löschen des Feldes die eckigen Klammern vergessen wurden. Da der delete Operator wegen der fehlenden eckigen Klammern nicht wissen kann, ob ein einzelnes Objekt oder ein Objektfeld zu löschen ist, wird nur der Destruktor für das erste Objekt im Feld aufgerufen.

ctor von Window
ctor von Window
ctor von Window
dtor von Window

Vorwärtsdeklaration

Im Zusammenspiel von dynamischen Objekten kann es zu Problemen mit Objektabhängigkeiten kommen. Sehen Sie sich dazu das nachfolgende Beispiel an.

1: // 1. Klassendefinitionen
2: class CAny
3: {
4:    Another *pAnother;
5:    ...
6: };
7: // 2. Klassendefinition
8: class Another
9: {
10:    CAny *pAny;
11:    ...
12: };

Hier werden zwei Klassen definiert, wobei jede Klasse einen Zeiger auf die andere Klasse enthält. Da der C++-Compiler den Quellcode in einem Durchlauf übersetzt, wird er die Definition des Zeigers pAnother in der Klasse CAny mit einem Fehler quittieren, da die Klasse Another noch nicht bekannt ist.

Um dieses Problem zu lösen, wird eine sogenannte Vorwärtsdeklaration eingesetzt. Bei der Vorwärtsdeklaration wird die Klasse nicht definiert, d.h. deren Member aufgeführt, sondern nur deklariert. Durch diese Deklaration wird dem Compiler in unserem Beispiel lediglich mitgeteilt, dass Another eine später zu definierende Klasse ist. Und damit kann der Compiler die Klasse CAny richtig instanziieren.

1: // Vorwärtsdeklaration
2: class Another;
3:
4: // 1. Klassendefinitionen
5: class CAny
6: {
7:    Another *pAnother;
8:    ...
9: };
10: // 2. Klassendefinition
11: class Another
12: {
13:    CAny *pAny;
14:    ...
15: };

Copyright 2024 © Wolfgang Schröder
E-Mail mit Fragen oder Kommentaren zu dieser Website an: info@cpp-tutor.de
Impressum & Datenschutz