C++-Programmierung/ Eigene Datentypen definieren/ Erstellen und Zerstören
Aus Wikibooks
In C++-Klassen gibt es zwei besondere Arten von Methoden: Konstruktoren und den Destruktor. Ein Konstruktor wird beim Anlegen eines Objektes ausgeführt, der Destruktor vor der „Zerstörung“ desselben. Der Name des Konstruktors ist immer gleich dem Klassennamen, der Destruktor entspricht ebenfalls dem Klassennamen, jedoch mit einer führendem Tilde ( ~).
Konstruktoren und Destruktoren haben keinen Rückgabetyp, auch nicht void. Der Konstruktor kann nicht als Methode aufgerufen werden, beim Destruktor ist dies hingegen möglich (aber nur selten nötig, dazu später mehr).
Inhaltsverzeichnis |
[Bearbeiten] Konstruktor
Jede Klasse hat einen oder mehrere Konstruktoren. Ein solcher Konstruktor dient zur Initialisierung eines Objektes. Im Folgenden wird eine Klasse Bruch angelegt, die über den Konstruktor die Membervariablen m_zaehler und m_nenner initialisiert.
public:
Bruch(int z, int n):
m_zaehler(z), // Initialisierung von m_zaehler
m_nenner(n) // Initialisierung von m_nenner
{}
private:
int m_zaehler;
int m_nenner;
};
int main(){
Bruch objekt(7, 10); // Der Bruch 7/10 oder auch 0.7
}
Wie Sie sehen, ist der Methodenrumpf von Bruch::Bruch leer. Die Initialisierung findet in den beiden Zeilen über dem Rumpf statt. Nach dem Prototyp wird ein Doppelpunkt geschrieben, darauf folgt eine Liste der Werte, die initialisiert werden sollen. Vielleicht erinnern Sie sich noch, dass es zur Initialisierung zwei syntaktische Varianten gibt:
Innerhalb der Initialisierungsliste ist nur die zweite Variante zulässig. Auch in der Hauptfunktion main() findet eine Initialisierung statt. Wie Sie sehen, ist hier ebenfalls nur die zweite Variante möglich, da objekt zwei Werte erwartet.
Beachten Sie, dass die Initialisierung der Variablen einer Klasse in der Reihenfolge erfolgt, in der sie in der Klasse deklariert wurden. Die Initialisierungsreihenfolge ist unabhängig von der Reihenfolge, in der sie in der Initialisierungsliste angegeben wurden. Viele Compiler warnen, wenn man so etwas macht, da derartiger Code möglicherweise nicht das tut, was man erwartet.
Natürlich ist es wie bei Funktionen möglich (und in der Regel zu empfehlen), die Methodendeklaration von der Methodendefinition zu trennen. Die Definition sieht genau so aus, wie bei einer normalen Funktion. Der einzige auffällige Unterschied besteht darin, dass dem Methodennamen der Klassenname vorangestellt wird, getrennt durch den Bereichsoperator ( ::).
public:
Bruch(int z, int n); // Deklaration
private:
int m_zaehler;
int m_nenner;
};
Bruch::Bruch(int z, int n): // Definition
m_zaehler(z), // Initialisierung von m_zaehler
m_nenner(n) // Initialisierung von m_nenner
{}
int main(){
Bruch objekt(7, 10); // Der Bruch 7/10 oder auch 0.7
}
Wie bereits erwähnt, hat der Konstruktor keinen Rückgabetyp, daher wird auch in der Definition keiner angegeben. Bei den Basisdatentypen ist es bezüglich der Performance übrigens egal, ob Sie diese initialisieren oder zuweisen. Sie könnten also den gleichen Effekt erzielen, wenn Sie statt der Initialisierungsliste eine Zuweisung im Funktionsrumpf benutzen. Allerdings gilt dies wirklich nur für die Basisdatentypen, bei komplexen Datentypen ist die Initialisierung oft deutlich schneller. Außerdem können konstante Variablen ausschließlich über die Initialisierungsliste einen Wert erhalten. Nun aber noch mal ein einfaches Beispiel, in dem der Funktionsrumpf des Konstruktors zur Anfangswertzuweisung dient:
public:
Bruch(int z, int n = 1); // Deklaration
private:
int m_zaehler;
int m_nenner;
};
Bruch::Bruch(int z, int n){ // Definition
m_zaehler = z;
m_nenner = n;
}
Wie Sie sehen entfällt der Doppelpunkt, wenn Sie die Initialisierungsliste nicht nutzen. Natürlich können Sie auch Initialisierungsliste und Funktionsrumpf parallel benutzen.
Irgendwann werden Sie sicher einmal in die Verlegenheit kommen ein Array als Membervariable innerhalb Ihrer Klasse zu Deklarieren. Leider gibt es keine Möglichkeit Arrays zu initialisieren, sie müssen immer im Konstruktorrumpf mittels Zuweisung ihren Anfangswert erhalten. Entsprechend ist es auch nicht möglich ein Memberarray mit konstanten Daten zu erstellen. Wenn Sie also zwingend eine Initialisierung benötigen, müssen Sie wohl oder übel Einzelvariablen erstellen.
[Bearbeiten] Defaultparameter
Sie können einem Konstruktor ebenso Defaultparameter vorgeben wie einer gewöhnlichen Funktion. Die Syntax ist hierbei identisch.
public:
Bruch(int z, int n = 1); // Deklaration
private:
int m_zaehler;
int m_nenner;
};
Bruch::Bruch(int z, int n): // Definition
m_zaehler(z),
m_nenner(n)
{}
[Bearbeiten] Mehrere Konstruktoren
Auch das Überladen des Konstruktors funktioniert wie das Überladen einer Funktion. Deklarieren Sie mehrere Prototypen innerhalb der Klasse und schreiben für jeden eine Definition:
public:
Bruch(int z); // Deklaration
Bruch(int z, int n); // Deklaration
private:
int m_zaehler;
int m_nenner;
};
Bruch::Bruch(int z): // Definition
m_zaehler(z),
m_nenner(1)
{}
Bruch::Bruch(int z, int n): // Definition
m_zaehler(z),
m_nenner(n)
{}
Wenn mehrere Konstruktoren das gleiche tun, ist es oft sinnvoll, diese gleichen Teile in eine eigene Methode (üblicherweise mit dem Namen init()) zu schreiben. Das ist kürzer, übersichtlicher, meist schneller und hilft auch noch bei der Vermeidung von Fehlern. Den wenn Sie den Code nachträglich ändern, dann müssten Sie diese Änderungen sonst für jeden Konstruktor vornehmen. Nutzen Sie eine init()-Methode, müssen Sie denn Code nur innerhalb von dieser einmal ändern.
public:
A(double x);
A(int x);
private:
void init();
int m_x;
int m_y;
};
A::A(double x):
m_x(x) {
init();
}
A::A(int x):
m_x(x) {
init();
}
void A::init(){
m_y=7000;
}
Dieses Beispiel ist zugegebenermaßen ziemlich sinnfrei, aber das Prinzip der init()-Funktion wird deutlich. Wenn Sie sich nun irgendwann entscheiden, m_y als Anfangswert 3333 zuzuweisen, dann müssen Sie dies nur in der init()-Funktion ändern und nicht in jedem einzelnen Konstruktor.
[Bearbeiten] Standardkonstruktor
Als Standardkonstruktor bezeichnet man einen Konstruktor, der keine Parameter erwartet. Ein Standardkonstruktor für unsere Bruch-Klasse könnte zum Beispiel folgendermaßen aussehen:
public:
Bruch(); // Deklaration Standardkonstruktor
private:
int m_zaehler;
int m_nenner;
};
Bruch::Bruch(): // Definition Standardkonstruktor
m_zaehler(0),
m_nenner(1)
{}
Natürlich könnten wir in diesem Beispiel den Konstruktor überladen, um neben dem Standardkonstruktor auch die Möglichkeiten zur Initialisierung zu haben. In diesem Beispiel bietet es sich jedoch an, dafür Defaultparameter zu benutzen. Das folgende kleine Beispiel erlaubt die Initialisierung mit einer ganzen Zahl und mit einer gebrochenen Zahl (also zwei Ganzzahlen: Zähler und Nenner). Außerdem wird auch gleich noch der Standardkonstruktor bereitgestellt, welcher den Bruch mit 0 initialisiert, also den Zähler auf Null und den Nenner auf Eins setzt.
public:
// Erlaubt 3 verschiedene Aufrufmöglichkeiten, darunter die des Standardkonstruktors
Bruch(int z = 0, int n = 1);
private:
int m_zaehler;
int m_nenner;
};
Bruch::Bruch(int z, int n): // Definition des Konstruktors
m_zaehler(z),
m_nenner(n)
{}
Im Kapitel "Leere Klassen" werden Sie noch einiges mehr über den Standardkonstruktor erfahren.
[Bearbeiten] Kopierkonstruktor
Neben dem Standardkonstruktor hat der Kopierkonstruktor noch eine besondere Bedeutung. Er erstellt, wie der Name bereits andeutet, ein Objekt einer Klasse, anhand eines bereits vorhanden Objektes. Der Parameter des Kopierkonstruktors ist also immer eine Referenz auf ein konstantes Objekt der selben Klasse. Der Kopierkonstruktor unserer Bruch-Klasse hat folgende Deklaration.
Wenn wir keinen eigenen Kopierkonstruktor schreiben, erstellt der Compiler einen für uns. Dieser implizite Kopierkonstruktor initialisiert alle Membervariablen mit den entsprechenden Werten der Membervariablen im übergebenen Objekt. Für den Moment ist diese vom Compiler erzeugte Variante ausreichend. Später werden Sie Gründe kennenlernen, die es notwendig machen, einen eigenen Kopierkonstruktor zu schreiben. Wenn Sie die Nutzung des Kopierkonstruktors verbieten wollen, dann schreiben Sie seine Deklaration in den private-Bereich der Klassendeklaration. Das bewirkt einen Kompilierfehler, wenn jemand versucht den Kopierkonstruktor aufzurufen.
Neben dem Kopierkonstruktor erzeugt der Compiler übrigens auch noch eine Kopierzuweisung; auch zu dieser werden Sie später noch mehr erfahren. Auch diese können Sie verbieten, indem Sie sie im private deklarieren. Wie dies geht, erfahren Sie im Kapitel zu Operatorüberladung.
[Bearbeiten] Destruktor
Im Gegensatz zum Konstruktor, gibt es beim Destruktor immer nur einen pro Klasse. Das liegt daran, dass ein Destruktor keine Parameter übergeben bekommt. Sein Aufruf erfolgt in der Regel implizit durch den Compiler bei der Zerstörung eines Objektes. Für den Anfang werden Sie mit dem Destruktor wahrscheinlich nicht viel anfangen können, da es mit den Mitteln, die Sie bis jetzt kennen, kaum nötig werden kann, dass bei der Zerstörung eines Objektes Aufräumungsarbeiten ausgeführt werden. Das folgende kleine Beispiel enthält einen Destruktor, der einfach gar nichts tut:
public:
~A(); // Deklaration Destruktor
};
A::~A(){ // Definition Destruktor
// Hier könnten "Aufräumungsarbeiten" ausgeführt werden
}
Wenn Sie den Abschnitt über Speicherverwaltung gelesen haben, werden Sie wissen, wie nützlich der Destruktor ist. Im Moment reicht es, wenn Sie mal von ihm gehört haben. Auch über den Destruktor werden Sie im Kapitel "Leere Klassen" weitere Informationen erhalten.
[Bearbeiten] Beispiel mit Ausgabe
Um noch einmal deutlich zu machen, an welchen Stellen Konstruktor und Destruktor aufgerufen werden, geben wir einfach innerhalb der Methoden eine Nachricht aus:
class A{
public:
A(); // Deklaration Konstruktor
~A(); // Deklaration Destruktor
void print(); // Deklaration einer Methode
};
A::A(){ // Definition Konstruktor
std::cout << "Innerhalb des Konstruktors" << std::endl;
}
A::~A(){ // Definition Destruktor
std::cout << "Innerhalb des Destruktors" << std::endl;
}
void A::print(){ // Definition der print()-Methode
std::cout << "Innerhalb der Methode print()" << std::endl;
}
int main(){
A objekt; // Anlegen des Objekts == Konstruktoraufruf
objekt.print(); // Aufruf der Methode print()
return 0;
} // Ende von main(), objekt wird Zerstört == Destruktoraufruf
Innerhalb der Methode print()
Innerhalb des Destruktors
[Bearbeiten] Objekte Erstellen und Zerstören
[Bearbeiten] Im Stack (deutsch: Stapel)
Effektive Objekte können, nur für den aktuellen Gültigkeitsbereich, auf dem sog. Stack erstellt werden. Der Stack ist ein Speicherbereich für lokale Variablen eines Moduls. Beim Verlassen eines Gültigkeitsbereichs werden diese Objekte automatisch zerstört. Alle vorigen Beispiele in diesem Abschnitt zeigen wie Objekte auf dem Stack erstellt und zerstört werden. Größere Objekte, große Speicherblocke, sollten nur in Beispielanwendungen auf dem Stack erstellt werden, dieser Bereich ist stark begrenzt und sollte ausschließlich für lokale und temporäre Daten genutzt werden. Moderne Compiler begrenzen diesen Bereich auf 1MegaByte. Wenn Sie größere Objekte auf den Stack legen wollen, müssen Sie die maximale Heapgröße modifizieren! Tun Sie dies nicht, erhalten Sie höchst bemerkenswerte Meldungen von Laufzeitumgebung und/oder Betriebssystem.
[Bearbeiten] Auf dem Heap (deutsch: Halde)
Effektive Objekte können dynamisch und permanent, bis zum Ende der Laufzeit des Moduls, erstellt werden. Dies erfolgt im sog. Heap. Der Heap entspricht meistens dem nicht vorgespeicherten Datensegment für das gesamte Programm. Dazu verwendet man operator new. Wenn ein Objekt nicht mehr benötigt wird, muss es bei dieser Variante manuell zerstört werden und zwar mit operator delete. Weiterführende Konzepte wie Smart-Pointer, können das Zerstören beim Verlassen von Gültigkeitsbereichen automatisieren. Diese Operatoren kann man auch für Felder verwenden, dann muss bei der Zerstörung operator delete [] verwendet werden.
Der Heap hat den eklatanten Vorteil, das die Grenzen des zuteilbaren Speichers nur vom Betriebssystem und physikalischer Speichermenge gezogen werden und nicht von Compiler- und Linkereinstellungen. Ein weiterer Vorteil ist, dass alle Elemente einer Klasse dann auch auf dem Heap liegen.
Wir verwenden Klasse 'a' aus vorigem Beispiel:
{
A * pObjekt(0); // Zeiger auf ein A-Objekt
pObjekt = new A; // Instantiieren auf dem Heap, Standardkonstruktor verwenden
delete A; // Zerstören
char * pszMemory = new char[0x100000]; // 1 Megabyte auf dem Heap alloziieren
delete [] pszMemory; // Speicherblock wieder freigeben
A * ar_Objekte = new A[50]; // 50 Objekte von A anlegen
delete [] ar_Objekte;
return 0;
}
Weitere Optionen zur Verwendung von new, delete, new [] und delete [] gibt es auch.