C++ Kurs

Dynamische Eigenschaften und Objekte

Die Themen:

Dynamische Eigenschaften

Anfordern des Speichers

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

Für ein solches dynamisches Feld wird innerhalb der Klasse anstelle des Feldes zunächst ein entsprechender Zeiger vom Typ des Feldes definiert. Der für das Feld notwendige Speicherplatz wird dann in der Regel im Konstruktor der Klasse reserviert. Die notwendige Feldgröße kann entweder, wie nachfolgend dargestellt, als Parameter an der Konstruktor übergeben werden oder aber im Konstruktor berechnet werden.

PgmHeader
// Klassendefinition
class Stack
{
   short *pArray;
   ...
 public:
   Stack (int size);
   ...
};
// Definition des Konstruktor
Stack::Stack(int size)
{
   pArray = new short[size];
}

Freigeben des Speichers

Wird das Objekt, und damit auch der dynamisch angeforderte Speicher, nicht mehr benötigt, so muss der Speicher auch wieder freigegeben werden. Dies erfolgt sinnvollerweise im Destruktor der Klasse. Beachten Sie bei der Freigabe von Feldern unbedingt die Schreibweise des delete Operators!

PgmHeader
// Klassendefinition
class Stack
{
   short *pArray;
   ...
 public:
   Stack (int size);
   ~Stack();
   ...
};
...
// Definition des Destruktor
Stack::~Stack()
{
   delete [] pArray;
}
AchtungEnthalten Objekte dynamische Eigenschaften, so dürfen solche Objekte vorläufig nicht einander zugewiesen werden. Durch die Zuweisung werden alle Eigenschaften des Objekts Object1 dem Objekt Object2 zugewiesen. Und dies gilt natürlich auch für die Zeiger auf die dynamischen Eigenschaften von Object1, d.h. in beiden Objekten verweisen die Zeiger dann auf die gleichen Speicherbereiche! Wird dann das Objekt Object1 gelöscht, so wird dessen Destruktor aufgerufen, der seinerseits die reservierten Speicherbereiche frei gibt. Und damit enthält das Objekt Object2 jetzt einen Zeiger auf einen ungültigen Speicherbereich, was bis zum Programmabsturz führen kann!

Beispiel und ÜbungDa dynamische Eigenschaften eine wichtige Rolle spielen, folgt nun ausnahmsweise einmal mitten in einem Kapitel ein Beispiel und dann auch eine Übung.

Dynamische Objekte

Genauso wie normale Daten und Felder dynamisch mittels new angelegt werden können, können auch Objekte und Objektfelder dynamisch erstellt werden. Dabei ist zu beachten, dass beim Erstellen eines Objekts stets dessen Konstruktor aufgerufen wird und beim Löschen des Objekts dessen Destruktor.

Sehen wir uns zunächst die Erstellung von dynamischen Objekten an.

Erstellen von dynamischen Objekten und Aufruf von Memberfunktionen

Um ein Objekt dynamisch zu erstellen, erhält der Operator new als Operanden den Namen der Klasse, für die ein Objekt erstellt werden soll. Konnte das Objekt erstellt werden, so liefert new einen Zeiger auf das Objekt zurück. Im Fehlerfall wird auch hier eine Ausnahme vom Typ bad_alloc ausgelöst.

Besitzt die Klasse einen Konstruktor der Parameter erhält (wie im nachfolgenden Beispiel), so werden die Parameter nach dem Klassennamen in Klammern angegeben.

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

PgmHeader// Klassendefinition
class Stack
{
    ....
  public:
    Stack(int size);
    ~Stack();
    bool Push(short val);
    ....
};
// main() Funktion

int main()
{
    // Stackobjekt dynamisch anlegen
    auto pMyStack = new Stack(10);
    ...
    // Memberfunktion des dyn. Objekts aufrufen
    auto ret = pMyStack->Push(5);
    ...
}

Löschen von dynamischen Objekten

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

PgmHeader// Klassendefinition
class Stack
{
    ....
  public:
    Stack(int size);
    ~Stack();
    bool Push(short val);
    ....
};
// main() Funktion
int main()
{
    // Stackobjekt dynamisch anlegen
    auto pMyStack = new Stack(10);
    ...
    // Stackobjekt löschen, ruft dtor von Stack auf
    delete pMyStack;
    ...
}

Dynamische Objektfelder

Erstellen von dynamischen Objektfeldern

Wenn von einer Klasse ein Objektfeld dynamisch erstellt werden soll, so darf die Klasse entweder keinen Konstruktor besitzen oder es muss der Standard-Konstruktor (parameterloser Konstruktor) definiert sein. Von einer Klasse, die nur Konstruktore mit Parametern besitzt, kann kein Objektfeld dynamisch erstellt werden. Der Grund hierfür liegt darin, dass für jedes Objekt im Feld natürlich dessen Konstruktor aufgerufen werden muss. Wenn der Konstruktor jetzt noch Parameter benötigt, so müssten diese ebenfalls für jedes Objekt im Feld dem new Operator mitgegeben werden, damit er die Objekte entsprechend initialisieren kann. Und dies ist nicht möglich.

Das Erstellen eines dynamischen Objektfeldes erfolgt analog zum dynamischen Erstellen von normalen Feldern, nur erhält new jetzt als Operanden den Klassennamen. Und nach dem Klassennamen folgt wieder innerhalb einer eckigen Klammer die Feldgröße.

PgmHeader// Klassendefinition
class Window
{
    ...
 public:
    Window();
    ...
};
// main() Funktion
int main()
{
    // Objektfeld zur Aufnahme von 5 Window-Objekte dynamisch anlegen
    auto pWinArray = new Window[5];
    ...
}

Zugriff auf Member in dynamischen Objektfeldern

Der Zugriff auf Member des Objektfeldes kann dann auf zweierlei Arten erfolgen.

Die erste Zugriffsart verwendet Zeigerarithmetik. Wie bereits erwähnt, wird bei einer Addition eines Werts X auf einen Zeiger vom Typ Y der Zeiger nicht um X erhöht sondern um X*sizeof(Y). Um nun eine Memberfunktion für ein bestimmtes Feldelement X aufzurufen, wir nach der Addition des Wertes X zum Zeiger der Zeigeroperator -> angegeben, gefolgt vom Namen der Memberfunktion und eventueller Parameter. So wird im Beispiel unten die Memberfunktion Draw() für das zweite Windows-Objekt im Feld aufgerufen.

PgmHeader// Klassendefinition
class Window
{
    ...
 public:
    Window();
    void Draw() const;
    ...
};
// main() Funktion
int main()
{
    // Objektfeld dynamisch anlegen
    auto pWinArray = new Window[5];
    ...
    // Aufruf der Memberfunktion Draw für das 2. Fenster
    (pWinArray+1)->Draw();
}

Alternativ kann anstelle des Zugriffs über eine Zeigeraddition auch der indizierte Zugriff verwendet werden. Dieses Verhalten erklärt sich aus der bekannten Tatsache, dass der Name eines eindimensionalen Feldes nichts anderes ist als der Zeiger auf den Beginn des Feldes. Beim indizierten Zugriff ist dann selbstverständlich der Zeigeroperator -> durch den Punktoperator . zu ersetzen.

PgmHeader// Klassendefinition
class Window
{
    ...
 public:
    Window();
    void Draw() const;
    ...
};
// main() Funktion
int main()
{
    // Objektfeld dynamisch anlegen
    auto pWinArray = new Window[5];
    ...
    // Aufruf der Memberfunktion Draw für das 2. Fenster
    pWinArray[1].Draw();
}

Löschen von dynamischen Objektfeldern

Wird ein Objektfeld nicht mehr benötigt, so muss es auch wieder gelöscht werden. Hierbei ist aber unbedingt 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 eines Feldes wird für jedes Objekt zunächst dessen Konstruktor (soweit vorhanden) aufgerufen. Wird das dann Feld gelöscht, dann muss für jedes Objekt im Feld auch dessen Destruktor aufgerufen werden.

PgmHeader// Klassendefinition
class Window
{
    ...
 public:
    Window()
    { cout << "ctor von Window\n"; }
    ~Window()
    { cout << "dtor von Window\n"; }
    ...
};
// main() Funktion
int main()
{
    // Objekt dynamisch anlegen
    auto pWinArray = new Window[3];
    ...
    // Objektfeld löschen
    delete [] pWinArray;
}

Damit ergibt sich für das Beispiel folgende Ausgabe:

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

Nun der Fehlerfall, dass beim Löschen des Feldes die eckigen Klammern vergessen wurden. Da der delete Operator nun wegen der fehlenden eckigen Klammern nicht mehr wissen kann, ob ein einzelnes Objekt oder ein Objektfeld gelöscht werden soll, wird der Destruktor nur noch für das erste Objekt im Feld aufgerufen.

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

Vorwärtsdeklaration

Im Zusammenhang mit dynamischen Objekten soll ein auf den ersten Blick nicht ganz trivialer Fall betrachtet werden. Sehen Sie sich zunächst einmal das nachfolgende Beispiel an.

PgmHeader// 1. Klassendefinitionen
class Any
{
    Another *pAnother;
    ....
};
// 2. Klassendefinition
class Another
{
    Any *pAny;
    ....
};

Hier werden zwei Klassen definiert, wobei jede Klassen einen Zeiger auf die andere Klasse enthält. Da der C++ Compiler in einem Durchlauf den Quellcode übersetzt, wird er die Definition des Zeigers pAnother in der Klasse Any mit einem Fehler quittieren, da die Klasse Another noch nicht bekannt ist. Auch ein Vertauschen der Klassendefinitionen führt nicht zu einer Lösung, da dann die Definition des Zeigers pAny in der Klasse Another fehlschlagen würde. Wir haben hier also das berühmte 'Henne-Ei' Problem.

Um diese Problem zu lösen, wird eine sogenannte Vorwärtsdeklaration eingesetzt. Bei der Vorwärtsdeklaration wird nicht die Klasse definiert, d.h. deren Member aufgeführt, sondern nur deklariert. Durch diese Deklaration wird dem Compiler lediglich mitgeteilt, dass Another eine später noch zu definierende Klasse ist. Und damit kann der Compiler die Klasse Any richtig aufbauen. Selbstverständlich muss die Definition der Klasse Another an anderer Stelle noch erfolgen.

PgmHeader// Vorwärtsdeklaration
class Another;

// 1. Klassendefinitionen
class Any
{
    Another *pAnother;
    ....
};
// 2. Klassendefinition
class Another
{
    Any *pAny;
    ....
};

Beispiel und ÜbungUnd hier geht's zum Beispiel und zur Übung.