Konstruktoren und Destruktor
Operationen mit Variablen
Es gibt drei unterschiedliche Grundoperationen, die man mit einer beliebigen Variablen durchführen darf:
Die Operationen Lesen und Schreiben finden wir innerhalb einer Zuweisung. Auf der rechten Seite der Zuweisung wird gelesen, auf der linken geschrieben. Nachdem es sich um eine Anweisung im Programm handelt, wird der Compiler dafür Maschinencode erzeugen.
Rahmen1Eine ganz andere Situation finden wir bei der Initialisierung vor. Das =-Zeichen hat hier eine andere Bedeutung. Es steht für die Initialisierung. Wann und wie nun die Initialisierung durchgeführt wird, hängt von der Variablenart ab. Für globale Variable legt der Compiler einen Datensatz in die Objektdatei ab. Dieser Datensatz wird mit dem Linker weiterverarbeitet und landet schließlich in der ausführbaren Datei (.EXE unter DOS). Beim Laden des Programms wird dann der Datenbereich im Speicher aus der Datei vorbelegt. Es ist hier der Lader des Betriebssystems, der den Wert in die Variable schreibt.
Startet man nun das Programm beim Testen mehrmals mit Hilfe des Debuggers, dann wird nur beim ersten Start die Datei von der Platte geladen und die Variable initialisiert. Beim zweiten und allen folgenden Starts wird mit dem möglicherweise veränderten Wert das Programm begonnen.
|
Konstruktoren und Destruktor Seite 43 |
Programmierer, die Firmware, also Programme in EPROM's oder ROM's schreiben, haben keine Lader und damit keine Initialisierung globaler Variable. Sie müssen dazu spezielle Routinen schreiben. Ein gängiges Verfahren ist es, die Werte im EPROM zu halten und beim Programmstart in den RAM-Bereich zu kopieren.
Ein anderes Verfahren wird verwendet, wenn es sich um eine lokale Variable handelt. Da lokale Variable nur während des Unterprogrammablaufes existieren, muß der Compiler aus der Initialisierung eine Zuweisung machen. Am Anfang des Unterprogramms wird hier zuerst die Variable dynamisch auf dem Stack angelegt und danach mit dem gewünschten Wert vorbelegt. Ein mehrmaliger Durchlauf durch das Unterprogramm führt daher immer wieder zu einer neuen Initialisierung.
Bei vordefinierten Datentypen ist diese Initialisierung einfach möglich. Für Strukturen haben ältere Compiler die Initialisierung lokaler strukturierter Variable verboten. Erst die ANSI-C Compiler erlauben auch die Initialisierung lokaler Strukturen.
Allgemein kann man sagen, daß die Initialisierung ein Auftrag an den Compiler ist, der Variablen beim Anlegen einen Wert zu geben. Eine Initialisierung kann nur ein einziges Mal erfolgen. Sie setzt eine neu angelegte, noch nie benutzte Variable voraus.
Die Zuweisung dagegen verändert den Wert einer bereits vorhandenen Variablen. Sie kann beliebig oft erfolgen.
Vielleicht fragen Sie sich, warum das Thema Initialisierung so wichtig ist. Zum einen können nicht richtig vorbesetzte Variable zu schwer zu findenden Fehlern führen. Zum anderen bietet C++ eine Möglichkeit, mit der Initialisierung weit mehr zu erreichen, als nur Werte vorzubesetzen. Für Objekte wird die Initialisierung verallgemeinert.
|
Seite 44 Einsteigerseminar C++ |
Wir können in der Klasse eine spezielle Methode deklarieren. Sie heißt genauso wie die Klasse und hat keinen Rückgabetyp. Die Parameter können wie üblich verwendet werden. Die Methode wird Konstruktor genannt. Sie wird vom Compiler automatisch aufgerufen, wenn eine Variable angelegt wird.
Konstruktor
Der Konstruktor verallgemeinert die Initialisierung, die Anfangswertzuweisung. Er kann alles erledigen, was ein Objekt am Anfang seiner Lebensdauer benötigt, nicht nur Anfangswerte setzen.
Der Konstruktor ist bis auf die genannten Besonderheiten eine normale Methode. Innerhalb des Konstruktors können wir beliebige Anweisungen schreiben. Für unsere Klasse ratio werden wir uns mit der Vorbesetzung begnügen. In anderen Klassen legen Konstruktoren dynamisch Speicherplatz an oder lassen ganze Fenster am Bildschirm entstehen.
Rahmen3Wenn in einer Klasse ein Konstruktor vorhanden ist, wird er beim Anlegen von Objekten immer automatisch verwendet. In unserem Fall müssen daher bei jedem Anlegen eines Objektes die Initialisierungswerte angegeben werden.
|
Konstruktoren und Destruktor Seite 45 |
Rahmen5Mit Hilfe des Konstruktors kann nun ein Objekt beim Anlegen mit beliebigen Werten vorbesetzt werden. Der Compiler stellt sicher, daß für alle Objekte der Klasse der Konstruktor gerufen wird, falls er vorhanden ist. Bei globalen Objekten wird intern ein zentrales Unterprogramm aufgebaut, das für alle globalen Objekte die Konstruktoren aufruft. Bei lokalen Objekten setzt der Compiler den Konstruktoraufruf in den Code des Blockes ein.
Legen wir nun einmal Objekte an. Es gibt zwei unterschiedliche Schreibweisen für die Initialisierung und damit den Konstruktoraufruf.
Rahmen7Die erste Schreibweise erinnert an eine Mischung aus dem konventionellen Anlegen einer Variablen und einem Funktionsaufruf. Und genau das geschieht ja auch. Die zweite Möglichkeit verwendet das =-Zeichen der Initialisierung. Diese Schreibweise ähnelt einer Zuweisung des Konstruktorergebnisses an ein Objekt. Wir werden hier die erste Schreibweise bevorzugen.
|
Seite 46 Einsteigerseminar C++ |
Objekte und Variablen können an beliebigen Stellen angelegt und automatisch initialisiert werden. Die Trennung zwischen Vereinbarungen und Anweisungen ist aufgehoben.
Da wir in der Klassendefinition einen Konstruktor deklariert haben, fordert der Compiler den Konstruktor bei jedem Anlegen eines Objektes der Klasse ratio an. Zu den rationalen Zahlen gehören auch die ganzen Zahlen. Die Frage ist, ob wir auch eine Initialisierung mit einer einzelnen ganzen Zahl vornehmen könnten? Unser bisheriger Konstruktor eignet sich dazu nicht. C++ kennt zwei unterschiedliche Verfahren, unser Problem zu lösen. Die erste Lösung besteht in der Überlagerung der Methoden (oder allgemein der Funktionen). Die zweite wird initialisierte Parameter benutzen.
Der Begriff Überlagerung besagt, daß der gleiche Funktionsname mehrfach verwendet werden darf. (Manchmal wird auch der Begriff Überladen benutzt.) Damit der Compiler die verschiedenen Funktionen gleichen Namens auseinander halten kann, verwendet er interne Namen. Der interne Name für eine Funktion setzt sich aus dem Namen des Unterprogramms, dem Klassennamen, falls vorhanden, und der Liste der Parametertypen in codierter Form zusammen.
Rahmen9
|
Konstruktoren und Destruktor Seite 47 |
In den beiden Beispielen wurden ein Konstruktor und eine Methode verschlüsselt. Man nennt diese interne Namen auch Signaturen. Für manche Methoden und Operatoren gibt es Kurzbezeichnungen. Hier gehört der Konstruktor zur Klasse ratio, deren Name aus fünf Buchstaben besteht. Er wird nach C++ Spezifikation gebunden und hat zwei Parameter vom Typ int. Der Name für die Methode wurde aus einem Unterstrich, dem Methodennamen, der Länge des Klassennamens, einer Linkinformation und dem Typen des Parameters gebildet.
Mit Hilfe der internen Namensgebung (type safe linkage, name mangling) kann nun ein Name wiederholt verwendet werden. So kann es für jede Klasse eine print()-Methode geben. Oder es kann sogar innerhalb einer Klasse der Name der Methode wiederholt werden, vorausgesetzt, die Parameterschnittstelle ist unterschiedlich.
Die Namensbildung variiert zwischen den einzelnen Compilern. Bitte lesen Sie evtl. im Handbuch nach. Allgemein gilt jedoch, daß der Programmierer keine Namen mit einem Unterstrich am Anfang oder zwei Unterstrichen in der Mitte eines Namens verwenden sollte.
|
Seite 48 Einsteigerseminar C++ |
Interne Namen können sehr lang werden. In C++ wurde daher die Längenbegrenzung für Namen aufgehoben.
Der zweite Konstruktor akzeptiert nun eine ganze Zahl. Er hat den gleichen Namen, nämlich den Klassennamen, wie unser bisher benutzter Konstruktor. Der Unterschied liegt in der Schnittstelle. Er hat einen Parameter vom Typ int, unser bisheriger hatte zwei Parameter des Typs int. Dies ist das Unterscheidungsmerkmal für den Compiler.
Die Implementierung ist einfach. Der Nenner wird mit Hilfe einer Konstanten, der eins, vorbesetzt.
Legen wir nun verschiedene Objekte mit unterschiedlichen Initialisierungslisten an, so wird der Compiler an Hand der Anzahl, der Reihenfolge und der Typen der übergebenen Parameter die richtige Methode auswählen. In unserem Fall gibt es nur den Unterschied zwischen einem oder zwei int-Werten.
|
Konstruktoren und Destruktor Seite 49 |
Die Überlagerung von Funktionsnamen ist ein wichtiger Schritt zur Unterstützung des Programmierers. Bisher konnte er in jedem Unterprogramm immer wieder seine bevorzugten Namen für Variable verwenden. Da diese lokal sind, ist es zulässig, jeder Programmierer seine Zählvariable z.B. i nennt. Nun kann jeder Programmierer in seiner Klasse auch beliebige Funktionsnamen verwenden. Jede Klasse kann nun z.B. ihr print() bekommen. Es können alle Funktionen überlagert werden, nicht nur Methoden.
Ein Problem bleibt bestehen. Wir können noch immer nicht ein Objekt anlegen, ohne einen Initalisierungswert anzugeben. Da wir Konstruktoren geschrieben haben, erwartet der Compiler einen passenden Konstruktor bei jeder Objektdefinition. Wir könnten einen Konstruktor schreiben, der keinen Parameter benötigt. Dieser Konstruktor wird Standardkonstruktor genannt. Da er benötigt wird, sollte man ihn in jeder Klasse anlegen.
Initialisierte Parameter
Eine weitere Möglichkeit, Konstruktoren aufzurufen, ohne Parameter angeben zu müssen, bieten die initialisierten Parameter.
In der Schnittstellendeklaration kann man jedem Parameter einen Initialisierungswert zuordnen. Ein solcher Parameter kann dann beim Aufruf weggelassen werden. Parameter mit Initialisierungswerten stehen immer am Ende der Parameterliste.
Damit wollen wir nun zum letzen Mal unseren Konstruktor umschreiben. Wir ordnen jedem Parameter einen Initialisierungswert zu. Damit können wir diesen Konstruktor dann mit keinem, mit einem oder mit zwei Werten benutzen.
|
Seite 50 Einsteigerseminar C++ |
Rahmen19Die Implementierung ist einfach, da sie von Standardwerten nichts wissen muß. Die Initialsierungswerte werden nur in der Deklaration angegeben. Der Konstruktor mit einem Parameter kann nun entfallen.
Beim Anlegen der Objekte kann der Konstruktor nun mit unterschiedlicher Parameteranzahl aufgerufen werden. Beachten Sie den Fall ohne Parameter. Hier wird kein rundes Klammernpaar benutzt. Dies geschieht, um zu den bisherigen Versionen von C die größtmögliche Kompatibilität zu wahren. Der C++ Compiler soll ja auch in der Lage sein, C-Programme zu übersetzen. Geben Sie runde Klammern ohne einen Parameter an, missversteht Sie der Compiler. Er sieht es dann als Funktionsdeklaration alten Stils (nach K&R: ohne Parameterprüfung).
|
Konstruktoren und Destruktor Seite 51 |
Neben der automatischen Initialisierung bietet C++ noch die Möglichkeit, für jede Klasse eine Methode zu schreiben, die am Ende der Lebensdauer eines Objektes automatisch aufgerufen wird: den Destruktor.
Der Name des Destruktors wird aus dem Klassennamen gebildet. Man stellt eine Tilde voran. Die Tilde soll die logische Invertierung des Konstruktors andeuten. Ein Destruktor hat keinen Parameter.
|
Seite 52 Einsteigerseminar C++ |
Rahmen27Die Objekte der Klasse ratio liegen abhängig von der Definition im globalen oder lokalen Datenbereich. Der globale Bereich ist das Datensegment. Der lokale Bereich ist der Stack. Ein lokal angelegtes Objekt wird beim Verlassen des Funktionsblockes entfernt. Dabei wird der vom Programm benutze Speicherbereich an das Betriebssystem zurückgegeben. Zur Erinnerung: ein Block wird durch ein geschweiftes Klammerpaar gekennzeichnet. Ein globales Objekt lebt bis das Programm beendet wird.
Im Falle der Klasse ratio benötigen wir keinen Destruktor, da nichts aufzuräumen ist. In anderen Klassen kann z.B. die Notwendigkeit bestehen, dynamisch angelegten Speicherplatz zurückzugeben. Bei einem Fenstersystem für den PC könnte man den Destruktor für ein Objekt Fenster benutzen, um das Fenster wieder vom Bildschirm zu entfernen.
Einem Destruktor der Klasse ratio bleibt also nichts zu tun. Nur um seine Existenz zu zeigen, kann man sich seinen Aufruf durch eine "print()"-Anweisung anzeigen lassen.
Das erweiterte ratio-Beispiel
Das ratio- Beispiel, das uns bisher gute Dienste geleistet hat, wollen wir nun noch einmal ausbauen und als ganzes betrachten.
|
Konstruktoren und Destruktor Seite 53 |
Rahmen29In der Klasse ratio gibt es keine Methoden, die ausschließlich intern benutzt werden. Daher finden Sie im geschützten Bereich keine Methodendeklarationen.
Die Schnittstelle zur Außenwelt wird von einem Satz aus Methoden und dem Konstruktor gebildet. Da für beide Parameter Initialisierungswerte angegeben wurden, kann der Konstruktor in drei unterschiedlichen Fällen benutzt werden: wenn kein, ein oder zwei Parameter bei der Definition von Objekten angegeben werden. Im Hauptprogramm wird dies demonstriert.
|
Seite 54 Einsteigerseminar C++ |
Rahmen31In den Methoden wird nur ein Operand als Parameter mitgegeben. Der Aufruf der Methode erfolgt durch die Bindung. Damit liegt der erste Operand fest.
|
Konstruktoren und Destruktor Seite 55 |
Innerhalb der Addition und der Subtraktion mußte eine lokale Ergebnisvariable eingeführt werden. Der Programmierer könnte auch dem eigenen Objekt das Ergebnis direkt zuweisen. Aber dann würde er die Spielregeln verletzen, nach denen eine Addition abläuft. Die Addition oder Subtraktion verändert die Operanden nicht. Daher legt man die Ergebnisvariable an, die auch zurückgegeben wird. Die Rückgabe eines Objektes ist genauso möglich, wie die Rückgabe einer Variablen der vordefinierten Datentypen.
Die Klasse wird in einer Informationsdatei definiert. Um ein mehrfaches Einlesen während einer Übersetzung zu verhindern, wurde die Klassendefinition in eine Präprozessoranweisung eingeschlossen (bedingte Übersetzung).
Für die Klasse ratio gibt es auch eine Methode zum Anzeigen. Diese Methode wurde so geschrieben, daß sie nur die Information ausgibt, die tatsächlich in einem Objekt der Klasse enthalten sind. Auf Führungstexte, wie Zähler ist: wurde bewusst verzichtet. Derartige Texte müssen getrennt ausgeben werden.
Zusätzlich wurde im ratio- Beispiel auch noch eine Zuweisung aufgenommen. Wir hätten sonst keine Möglichkeit, die Werte eines bereits angelegten Objektes zu verändern.
|
Seite 56 Einsteigerseminar C++ |
Rahmen33Im Hauptprogramm werden zuerst die benötigten Informationsdateien eingelesen. Es ist dabei zweckmäßig, zuerst die Standard-Dateien einzulesen und danach die selbst geschriebenen. Der Name der ratio- Informationsdatei hat hier die Erweiterung hpp. Diese Erweiterung ist nicht Pflicht. Sie soll in dieser Einführung zur leichten Unterscheidung benutzt werden.
Die Objekte wurden global definiert. Dabei wurden alle möglichen Aufrufe des Konstruktors verwendet.
Der "()-Aufruf verwendet eine ANSI-Steuersequenz zum Löschen des Bildschirms (\x1b[2J\x1b[1;1H). In der Zeile 11 erfolgt der Aufruf der Additionsmethode. In der OOP sagt man auch, daß man an das Objekt B die Botschaft addiere mit dem Parameter &C sendet.
|
Konstruktoren und Destruktor Seite 57 |
Im Hauptprogramm werden zwei verschiedene Zuweisungen verwendet. In der Zeile 11 oder 14 benutzen wir die Standardzuweisung des Compilers für Strukturen und Objekte. Er wird dabei Datenelement für Datenelement kopieren. In der Zeile 17 wird die selbst geschriebene Zuweisung benutzt.
Der Bildschirm sieht nach Ablauf der Programms wie folgt aus.
Rahmen35Damit haben wir in einem ersten Beispiel einige Möglichkeit kennen gelernt, Klassen zu definieren und zu benutzen.
Hinweise zur Weiterarbeit
1) Schreiben Sie vorhandene Strukturen zu Klassen um.
2) Welche Möglichkeiten bieten Konstruktoren für:
- mathematische Klassen
- Fensterklassen für Benutzeroberflächen
- dynamische Daten
- Kommunikationsklassen?
|
Seite 58 Einsteigerseminar C++ |
3) In den bisherigen Methoden haben wir Objekte mit Hilfe eines Zeigers an Methoden übergeben. Warum können wir ganze Objekte (noch nicht) übergeben?
4) Welche Notwendigkeiten oder welche Möglichkeiten sehen Sie für Destruktoren?
5) Kennen Sie Fälle von Überlagerung in prozeduralen Programmiersprachen?
6) Ein interner Name (die Signatur) wird aus drei Information aufgebaut. Welchen?
7) Definieren Sie eine Klasse Punkt, die u.a. eine Ausgabemethode hat. Schreiben Sie damit ein Hauptprogramm, das mit Hilfe von Objekten der Klasse Punkt eine Linie am Bildschirm ausgibt. Zur Positionierung des Cursors können Sie das folgende Makro POS verwenden
#define POS(zeile,spalte) printf(\x1b[%d;%dH,zeile,spalte)
Im nächsten Kapitel
Die Konstruktoren und Destruktoren eignen sich nicht nur zur Initialisierung sondern auch für beliebige andere Aufgaben. Je komplexer eine Klasse wird, desto größer werden auch die Aufgaben, die am Anfang und am Ende der Lebensdauer eines Objektes erledigt werden müssen. Bei unserem ratio-Beispiel war z.B. der benötigte Speicherplatz bekannt. Bei anderen Objekten ergibt sich der Speicherplatzbedarf erst während des Ablaufs.
Dem Problem der Speicherplatzreservierung während der Laufzeit wenden wir uns im nächsten Kapitel zu.
|
Konstruktoren und Destruktor Seite 59 |
|
Seite 60 Einsteigerseminar C++ |