C++ Kurs

Dynamische Eigenschaften und Objekte

Die Themen:

Dynamische Eigenschaften
Beispiel und Übung
Dynamische Objekte
Dynamische Objektfelder
Vorwärtsdeklaration
Beispiel und Übung

Dynamische Eigenschaften

Anfordern des Speichers

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];
}

Freigeben des Speichers

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;
}

Enthalten Objekte dynamische Eigenschaften, so sollten Sie vorläufig folgende Anweisung vermeiden:

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.

Beispiel und Übung

Beispiel:

Das Beispiel definiert eine Klasse Student, die eine Verknüpfung zwischen einem Studenten und den von ihm belegten Kurs herstellt. Zusätzlich wird noch die Information abgelegt, ob der Student den Kurs bereits bezahlt hat.

Für den Namen und den Kurstitels werden entsprechende string Eigenschaften dynamisch angelegt. Dazu wird dem Konstruktor der Name und der Kurstitel übergeben. Die Information, ob der Kurs bereits bezahlt ist oder nicht, kann ebenfalls mit übergeben werden. Standardmäßig bezahlt der Teilnehmer den Kurs nicht gleich (Default-Parameter).

Damit der Kurs auch zu einem späteren Zeitpunkt bezahlt werden kann, enthält die Klasse eine Memberfunktion Pay(). Da der Code dieser Memberfunktion relativ klein ist, bietet es sich hier an, eine inline-Memberfunktion einzusetzen.

Zur Ausgabe der Daten wird eine weitere Memberfunktion PrintStudent() verwendet.

In main() werden dann zwei Studenten mit den jeweiligen Kursen "definiert", wobei der erste Teilnehmer den Kurs gleich bezahlt und der zweite Teilnehmer nicht. Anschließend werden die beiden Daten ausgeben.

Vor Kursbeginn bezahlt nun auch der erste Teilnehmer die Kursgebühr und die Daten werden nochmals zu Kontrolle ausgegeben.

Studentenliste:
Name: Karl Maier, Kurs: C++, bezahlt!
Name: Agnes Mueller, Kurs: MFC, nicht bezahlt!
Agnes Mueller zahlt nun.

Neue Studentenliste:
Name: Karl Maier, Kurs: C++, bezahlt!
Name: Agnes Mueller, Kurs: MFC, bezahlt!


// 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();
}

Übung:

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
Und jetzt wieder lesen: 69 0 34 67 41

Lösung ansehen!

Dynamische Objekte

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.

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 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);
    ...
}

Löschen von dynamischen Objekten

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;
    ...
}

Dynamische Objektfelder

Erstellen von dynamischen Objektfeldern

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];
    ...
}

Zugriff auf Member in dynamischen Objektfeldern

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();
}

Löschen von dynamischen Objektfeldern

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
ctor von Window
ctor von Window
dtor von Window
dtor von Window
dtor 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
ctor von Window
ctor von Window
dtor von Window

Vorwärtsdeklaration

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;
    ....
};

Beispiel und Übung

Beispiel:

Es wird eine Klasse zur Aufnahme einer Tabelle entwickelt. Da die Anzahl der Reihen und Spalten nicht fest innerhalb der Klasse vorgegeben werden soll, werden diese Daten dem Konstruktor übergeben. Im Konstruktor wird ein entsprechendes 2-dimensionales Feld dynamisch angelegt, das im Destruktor entsprechend wieder freigegeben werden muss.

Über die Memberfunktion SetCell(...) kann eine bestimmte Zelle innerhalb der Tabelle mit einem Wert belegt werden. Zur Ausgabe der gesamten Tabelle wird die Memberfunktion PrintIt() verwendet.

In der main() Funktion wird ein Tabellenobjekt dynamisch angelegt, wobei die Tabellengröße über Konstanten festgelegt wird. Anschließend wird die Tabelle mit Zufallszahlen ausgefüllt und ausgegeben.

Im nächsten Schritt wird die Tabellendiagonale mit dem fixen Wert -1 belegt und danach die gesamte Tabelle nochmals ausgegeben.

Zum Schluss wird das Tabellenobjekt wieder gelöscht.

Tabelleninhalt
==============
41 67 34  0 69
24 78 58 62 64
 5 45 81 27 61
91 95 42 27 36
91  4  2 53 92
Tabelleninhalt
==============
-1 67 34  0 69
24 -1 58 62 64
 5 45 -1 27 61
91 95 42 -1 36
91  4  2 53 -1


// 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;
}

Übung:

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.
Speicher für Fenster reserviert.
Speicher für Fenster reserviert.
Speicher für Fenster reserviert.
Fenster : ' Fenster 1'
Position: 100,100
Grösse : 50,50
Fenster : ' Fenster 2'
Position: 50,50
Grösse : 300,400
Fenster : ' Fenster 3'
Position: 200,300
Grösse : 100,100
Fenster : ' Fenster 4'
Position: 100,150
Grösse : 400,300
Speicher für Fenster freigegeben
Speicher für Fenster freigegeben
Speicher für Fenster freigegeben
Speicher für Fenster freigegeben

Lösung ansehen!