Dynamische Eigenschaften
Beispiel und Übung
Dynamische Objekte
Dynamische Objektfelder
Vorwärtsdeklaration
Beispiel und Übung
Enthalten Objekte z.B. Felder, deren Größe von Objekt zu Objekt variieren kann, so werden diese Felder bei der Definition des Objekts, d.h. dynamisch, angelegt. Ein Beispiel hierfür ist die in der Lektion über Konstruktor und Destruktor 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 dynamische Feld wird innerhalb der Klasse anstelle des Feldes 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.
|
// Klassendefinition class Stack { short *pArray; ... public: Stack (int size); ... }; // Definition des Konstruktor Stack::Stack(int size) { pArray = new short[size]; } |
Wird das Objekt, und damit auch der dynamisch angeforderte Speicher, nicht mehr benötigt, so muss der Speicher im Destruktor des Objekts auch wieder freigegeben werden. Beachten Sie bei der Freigabe von Feldern unbedingt die Schreibweise des delete Operators!
|
// Klassendefinition class Stack { short *pArray; ... public: Stack (int size); ~Stack(); ... }; ... // Definition des Destruktor Stack::~Stack() { delete [] pArray; } |
|
Object1 = Object2; |
Durch diese Zuweisung werden alle Eigenschaften des Objekts Object1 dem Objekt Object2 zugewiesen. Und dies gilt auch für die Zeiger auf die in Object1 angelegten dynamischen Eigenschaften, d.h. beide Objekte verwenden nach der Zuweisung die gleichen dynamischen Eigenschaften. Daraus folgt, dass die Zeiger in beiden Objekten nun auf die gleichen Speicherbereiche verweisen! Das allein ist schon schlimm genug. Wird dann noch das Objekt Object1 gelöscht, so wird dessen Destruktor aufgerufen, der dann die reservierten Speicherbereiche frei gibt. Und damit enthält das Objekt Object2 jetzt Zeiger auf ungültige, freigegebene Speicherbereiche, was bis zum Programmabsturz führen kann!
Da dynamische Eigenschaften eine wichtige Rolle spielen, folgt nun ausnahmsweise einmal mitten in einer Lektion ein Beispiel und dann auch eine Übung.
|
|
Studentenliste: Neue Studentenliste: |
|
// Lösung zu dynamischen Daten in Objekten // Dateien einbinden #include <iostream> #include <iomanip> #include <string> using std::cout; using std::endl; using std::string; // Klassendefinition class Student { string *pName; // Name string *pCourse; // Kurs bool hasPaid; // Kurs bezahlt public: Student(const char *const pN, const char *const pC, bool paid=false); ~Student(); void Pay(); void PrintStudent(); }; // Definition der Memberfunktionen // Konstruktor Student::Student(const char *const pN, const char *const pC, bool paid) { // Speicher für Namen reservieren pName = new string(pN); // Speicher für Kurstitel reservieren pCourse = new string(pC); // Merken ob Kurs bezahlt ist hasPaid = paid; } // Destruktor Student::~Student() { // Speicher für Name und Kurstitel freigeben delete pName; delete pCourse; } // Hier zahlt der Student inline void Student::Pay() { cout << *pName << " zahlt nun.\n\n"; hasPaid = true; } // Ausgabe der Studentendaten void Student::PrintStudent() { cout << "Name: " << *pName; cout << ", Kurs: " << *pCourse << ", "; if (!hasPaid) cout << "nicht "; cout << "bezahlt!\n"; } // main() Funktion int main() { // Student-Objekte definieren // 1. Student belegt C++ Kurs und zahlt Student Student1("Karl Maier", "C++", true); // 2. Student belegt MFC Kurs und zahlt nicht Student Student2("Agnes Mueller", "MFC"); // Ausgabe der Studentendatem cout << "Studentenliste:\n"; Student1.PrintStudent(); Student2.PrintStudent(); // 2. Student zahlt nun Student2.Pay(); // Ausgabe der Studentendaten cout << "Neue Studentenliste:\n"; Student1.PrintStudent(); Student2.PrintStudent(); } |
Diese Übung basiert auf dem Beispiel aus der Lektion 5 Konstruktor und Destruktor.
Es ist eine Klasse Stack zu schreiben, die es erlaubt, mittels Push(...) short-Werte auf einem Stack abzulegen und mittels Pop(...) die abgelegten Werte wieder auszulesen.
Neu hinzu kommt nun, dass die Anzahl der abzulegenden Werte nicht fest vorgegeben wird (bisher waren es maximal 10 Werte), sondern erst bei der Definition des Stack-Objekts festgelegt wird (Parameter des Konstruktors!).
Standardmäßig soll ein Stack für 10 short-Werte angelegt werden.
Legen Sie im Programm einen Stack für 5 short-Werte an, den Sie anschließend so lange mit Werten füllen, bis er voll ist. Anschließend lesen Sie wieder alle auf dem Stack abgelegten Werte aus und geben diese zur Kontrolle aus.
|
Werte auf Stack : 41 67 34 0 69 |
Genauso wie eben normale Daten und Felder dynamisch mittels new angelegt wurden, können auch Objekte und Objektfelder dynamisch erstellt werden. Sehen wir uns zunächst die Erstellung von dynamischen Objekten an.
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 Exception vom Typ bad_alloc ausgelöst.
Besitzt die Klasse einen Konstruktor der Parameter enthä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 ->.
// Klassendefinition
class Stack
{
....
public:
Stack(int size);
~Stack();
bool Push(short val);
....
};
// main() Funktion int main() { // Stackobjekt dynamisch anlegen Stack *pMyStack = new Stack(10); ... // Memberfunktion des dyn. Objekts aufrufen bool ret = pMyStack->Push(5); ... } |
Wird das Objekt nicht mehr benötigt, so muss es wieder gelöscht werden. Dies erfolgt mit dem bekannten delete Operator, der den von new zurückgelieferten Zeiger als Operanden erhält. Beachten Sie, dass das Löschen des Objekts zum Aufruf des Destruktors führt.
// Klassendefinition
class Stack
{
....
public:
Stack(int size);
~Stack();
bool Push(short val);
....
};
// main() Funktion int main() { // Stackobjekt dynamisch anlegen Stack *pMyStack = new Stack(10); ... // Stackobjekt löschen delete pMyStack; ... } |
Soll von einer Klasse dynamisch ein Objektfeld erstellt werden können, 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 enthält kann kein Objektfeld dynamisch erstellt werden.
Das Erstellen eines dynamischen Objektfeldes erfolgt analog zum dynamischen Erstellen von normalen Feldern, nur erhält new jetzt als Operanden den Klassennamen. Nach dem Klassennamen folgt innerhalb einer eckigen Klammer die Feldgröße.
// Klassendefinition class Window { ... public: Window(); ... }; // main() Funktion int main() { // Objektfeld dynamisch anlegen Window *pWinArray = new Window[5]; ... } |
Der Zugriff auf die Member des Objektfeldes kann dann auf zweierlei Arten erfolgen.
Die erste Zugriffsart verwendet die Zeigeraddition. Wie Sie bestimmt noch wissen, 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 aufzurufen, wir nach der Zeigeraddition der Zeigeroperator -> angegeben, gefolgt vom Namen der Memberfunktion und eventueller Parameter. So wird im Beispiel unten die Memberfunktion Draw() für das zweite Fenster im Feld aufgerufen.
// Klassendefinition class Window { ... public: Window(); void Draw() const; ... }; // main() Funktion int main() { // Objektfeld dynamisch anlegen Window *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 Feldes nichts anderes
ist als der Zeiger auf den Beginn des Feldes. Beachten Sie dabei aber
bitte, dass der Zeigeroperator -> dann durch den
Punktoperator . ersetzt wird.
// Klassendefinition class Window { ... public: Window(); void Draw() const; ... }; // main() Funktion int main() { // Objektfeld dynamisch anlegen Window *pWinArray = new Window[5]; ... // Aufruf der Memberfunktion Draw für das 2. Fenster pWinArray[1].Draw(); } |
Wird das 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 Klammer angegeben werden. Sehen wir uns zur Demonstration einmal an was passiert, wenn Sie diese eckigen Klammern aus Versehen vergessen.
Zunächst der korrekte Fall. Wie Sie schon bei der Behandlung des Konstruktors erfahren haben, wird für jedes Feldelement der Konstruktor (soweit vorhanden) der Klasse aufgerufen. Und das Gleiche gilt natürlich auch für den Aufruf des Destruktors.
// Klassendefinition class Window { ... public: Window() { cout << "ctor von Window\n"; } ~Window() { cout << "dtor von Window\n"; } ... }; // main() Funktion int main() { // Objekt dynamisch anlegen Window *pWinArray = new Window[3]; ... // Objektfeld löschen delete [] pWinArray; } |
Damit ergibt sich für das Beispiel folgende Ausgabe:
|
ctor von Window |
Nun der Fehlerfall. Das Beispiel entspricht bis auf den Aufruf des delete Operators dem vorherigen Beispiel. Lediglich der eckigen Klammern wurden beim delete Operator weggelassen. An der Aufrufreihenfolge der Konstruktore ändert sich nichts. Da aber 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.
|
ctor von Window |
Im Zusammenhang mit dynamischen Objekten soll noch ein auf den ersten Blick nicht ganz trivialer Fall betrachtet werden. Sehen Sie sich zunächst einmal das nachfolgende Beispiel an.
// 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 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 z.B. mitgeteilt, dass Another eine Klasse ist und er kann damit die Klasse Any richtig aufbauen. Selbstverständlich muss die Definition der Klasse Another an anderer Stelle noch erfolgen.
// Vorwärtsdeklaration class Another; // 1. Klassendefinitionen class Any { Another *pAnother; .... }; // 2. Klassendefinition class Another { Any *pAny; .... }; |
|
|
Tabelleninhalt |
// Beispiel zu dynamischen Objekten und Objektfelder // Zuerst Dateien einbinden #include <iostream> #include <iomanip> #include <cstdlib> using std::cout; using std::endl; // Konstantendefinitionen const int ROWS = 5; const int COLS = 5; // Klassendefinition class Grid { short **ppGrid; // Zeiger auf 2-dimensionales Feld short rows; // Anzahl der Reihen short cols; // Anzahl der Spalten public: Grid(short ro, short co); ~Grid(); bool SetCell(short ro, short co, short v); void PrintIt(); }; // Definition der Memberfunktionen // Konstruktor Grid::Grid(short ro, short co) { // Anzahl Reihen/Spalten merken rows = ro; cols = co; // Feld für Tabellenreihen-Zeiger reservieren ppGrid = new short*[rows]; // Speicher für die Spalten reservieren for (int index=0; index<rows; index++) { ppGrid[index] = new short[cols]; } } // Destruktor Grid::~Grid() { // Alle Spaltenfelder freigeben for (int index=0; index<rows; index++) delete [] ppGrid[index]; // Feld mit Tabellenreihen-Zeiger freigeben delete [] ppGrid; } // Zelle in der Tabelle setzen bool Grid::SetCell(short ro, short co, short v) { // Reihen-/Spaltennummer abprüfen if ((ro<0) || (ro>=rows)) return false; if ((co<0) || (co>=cols)) return false; // Zelle setzen ppGrid[ro][co] = v; return true; } // Tabelle ausgeben void Grid::PrintIt() { cout << "Tabelleninhalt\n"; cout << "==============\n"; for (int rIndex=0; rIndex<rows; rIndex++) { for (int cIndex=0; cIndex<cols; cIndex++) cout << std::setw(4) << ppGrid[rIndex][cIndex]; cout << endl; } } // main() Funktion int main() { // Tabelle anlegen Grid *pGrid = new Grid(ROWS, COLS); // Zellen mit Zufallszahlen belegen for (short rIndex=0; rIndex<ROWS; rIndex++) for (short cIndex=0; cIndex<COLS; cIndex++) pGrid->SetCell(rIndex,cIndex,rand()%100); // Tabelle ausgeben pGrid->PrintIt(); // Diagonale der Tabelle mit -1 belegen int maxDim = (ROWS>COLS)?COLS:ROWS; for (short index=0; index<maxDim; index++) pGrid->SetCell(index,index,-1); // Tabelle nochmals ausgeben pGrid->PrintIt(); // Tabelle nun auch wieder löschen! delete pGrid; } |
Erstellen Sie folgende ASCII-Datei, die die Daten für mehrere Fenster enthält. Die Datei besitzt folgenden Inhalt:
![]() 4 100 100 50 50 Fenster 1 50 50 300 400 Fenster 2 200 300 100 100 Fenster 3 100 150 400 300 Fenster 4 |
Der erste Dateieintrag enthält die Anzahl der in der Datei abgelegten Datensätze. Anschließend folgen die Fenster-Datensätze, die folgenden Aufbau besitzen: X/Y Position, Breite/Höhe und Fenstertitel. Beachten Sie bitte, dass im Fenstertitel auch Leerzeichen enthalten sein können.
Erstellen Sie nun eine entsprechende Fensterklasse zum Abspeichern der Fensterdaten. Der für den Fenstertitel benötigte Speicherplatz ist dynamisch als string Objekt anzulegen. Im Konstruktor und Destruktor der Klasse ist eine beliebige Meldung auszugeben, damit deren Aufrufe verfolgt werden können. Desweiteren ist eine Memberfunktion zum Einlesen des Datensatzes für ein Fensterobjekt zu erstellen und eine Memberfunktion zur Ausgabe der Fensterdaten.
In main() ist dynamisch ein entsprechend großes Objektfeld zur Aufnahme der Fensterdaten anzulegen. Anschließend sind die Daten der Fenster über die Memberfunktion der Fensterklasse einzulesen und zur Kontrolle auszugeben.
|
Speicher für Fenster reserviert. |