Programmieren in C++: Aufbau


Kapitel 2: Spezielle Methoden


Inhaltsverzeichnis

Dieses Buch ist unter einer Creative Commons-Lizenz lizensiert.


2.1 Allgemeines

In die Sprache integrierte Hilfsmittel

Im Kapitel 1, Klassen und Objekte haben Sie gelernt, was Klassen sind, wie Sie Klassen definieren und verwenden und Objekte anlegen. Sie wissen außerdem, dass Klassen grundsätzlich aus Eigenschaften und Methoden bestehen, die ein Objekt beschreiben sollen.

Sehr oft ist es notwendig, nach dem Erstellen eines Objekts dieses zu initialisieren. Dies war auch im Abschnitt 1.6, „Praxis-Beispiel“ der Fall: Das Objekt b vom Typ der Klasse bank musste auf bestimmte Werte gesetzt werden, um mit ihm sinnvoll arbeiten zu können. Hierzu wurde extra eine Methode init() definiert, die nach der Objektdefinition aufgerufen werden musste.

Für die Objektinitialisierung besitzt C++ eine in die Programmiersprache eingebaute Funktionalität: Ein Konstruktor ist eine Methode, die automatisch aufgerufen wird, wenn ein Objekt erstellt wird. Klassen können Konstruktoren überladen, also mehrere Konstruktoren definieren, die sich in Anzahl und Typ der Parameter unterscheiden. Bei der Definition eines Objekts kann angegeben werden, welcher Konstruktor verwendet werden soll. Der standardmäßig verwendete Konstruktor wird ganz einfach Standardkonstruktor genannt und aufgerufen, wenn keine spezielle Angabe bei der Objektdefinition erfolgt. Nachdem der Konstruktor für ein Objekt automatisch aufgerufen wird, hat dies den Vorteil, dass Sie als Entwickler nicht mehr vergessen können, Ihr Objekt zu initialisieren.

Genauso wie der Konstruktor beim Erstellen eines Objekts automatisch ausgeführt wird, gibt es drei andere Methoden, die ebenfalls automatisch in bestimmten Situationen aufgerufen werden: Der Destruktor wird ausgeführt, wenn ein Objekt aus dem Speicher gelöscht wird, also der Gültigkeitsbereich eines Objekts endet. Der Copy-Konstruktor wird aufgerufen, wenn ein Objekt bei einem Funktions- oder Methodenaufruf kopiert werden muss. Und der Zuweisungsoperator wird ausgeführt, wenn ein Objekt mit dem Zuweisungsoperator = kopiert wird.

Gegen Ende des Kapitels werden statische Methoden vorgestellt. Statische Methoden sind Methoden, die nicht nur unter Angabe eines Objekts aufgerufen werden können, sondern auch direkt über den Klassennamen. Statische Methoden haben mit Konstruktoren, Desktruktoren, Copy-Konstruktoren und Assignment-Operatoren nichts zu tun. Während es sich bei letzteren um sogenannte Instanzmethoden handelt, sind statische Methoden Klassenmethoden. Die Bedeutung und der Einsatz von statischen Methoden wird Ihnen am Ende des Kapitels vorgestellt.


2.2 Konstruktor

Automatischer Aufruf bei Objektdefinition

Der Konstruktor ist eine Methode, die automatisch aufgerufen wird, wenn Sie ein Objekt erstellen. Um einen Konstruktor zu definieren, müssen Sie den Klassennamen als Methodennamen verwenden. Der Konstruktor darf außerdem überhaupt keinen Rückgabewert besitzen - noch nicht mal void. Erwartet der Konstruktor darüberhinaus keine Parameter, spricht man vom Standardkonstruktor.

Im folgenden Beispiel sehen Sie die Deklaration eines Standardkonstruktors innerhalb einer Klasse flugzeug.

#include <string> 

class flugzeug 
{ 
  public: 
    flugzeug(); 

  private: 
    std::string Fluggesellschaft; 
}; 

Wie gewohnt befindet sich die Implementation von Methoden, also auch die von Konstruktoren, in einer separaten Datei, die von der Klassendefinition getrennt ist.

#include "flugzeug.h" 

flugzeug::flugzeug() 
{ 
  Fluggesellschaft = "Lufthansa"; 
} 

In dem Standardkonstruktor wird auf die Eigenschaft Fluggesellschaft zugegriffen. Die Eigenschaft wird auf den Wert "Lufthansa" gesetzt.

#include "flugzeug.h" 

flugzeug f; 

Wenn Sie nun in Ihrem C++-Code an irgendeiner Stelle ein Objekt vom Typ flugzeug erstellen, wird automatisch der Standardkonstruktor aufgerufen. Das Objekt f besitzt also eine Eigenschaft, die auf den Wert "Lufthansa" gesetzt ist. Diese Initialisierung wird automatisch durch den Standardkonstruktor vorgenommen - Sie müssen sich innerhalb des Codes nicht zusätzlich um eine korrekte Initialisierung kümmern. Gute Objekte kümmern sich selbst um ihre Initialisierung.

Im Folgenden wird die Klasse flugzeug um einen Konstruktor erweitert, der einen Parameter vom Typ std::string erwartet.

#include <string> 

class flugzeug 
{ 
  public: 
    flugzeug(); 
    flugzeug(std::string fluggesellschaft); 

  private: 
    std::string Fluggesellschaft; 
}; 

Der neue Konstruktor wird in der Datei flugzeug.cpp definiert.

#include "flugzeug.h" 

flugzeug::flugzeug() 
{ 
  Fluggesellschaft = "Lufthansa"; 
} 

flugzeug::flugzeug(std::string fluggesellschaft) 
{ 
  Fluggesellschaft = fluggesellschaft; 
} 

Die Klasse flugzeug besitzt also nun zwei Methoden, die genauso heißen wie die Klasse selbst - zwei Konstruktoren. Der eine Konstruktor erwartet keinen Parameter - der Standardkonstruktor - der andere neue Konstruktor erwartet einen Parameter vom Typ std::string. Der als Parameter übergebene Wert wird verwendet, um die Eigenschaft Fluggesellschaft des Objekts zu initialisieren. Das heißt, die Eigenschaft wird nicht mehr automatisch wie im Standardkonstruktor auf Lufthansa gesetzt, sondern auf den Wert, der als Parameter übergeben wird.

Bleibt die Frage, wie eigentlich ein Konstruktor aufgerufen wird, der einen Parameter vom Typ std::string erwartet.

#include "flugzeug.h"; 

flugzeug f; 
flugzeug f2("British Airways"); 

Zusätzlich zum Objekt f wird nun ein zweites Objekt f2 angelegt. Die Definition dieses Objekts sieht jedoch genaugenommen wie ein Funktionsaufruf aus: Hinter dem Objektnamen werden nämlich runde Klammern angegeben, zwischen denen eine Zeichenkette steht. Diese Zeile bedeutet, dass ein Konstruktor in der Klasse flugzeug verwendet werden soll, der genau einen Parameter vom Typ einer Zeichenkette erwartet. Nachdem Sie eben die Klasse flugzeug um einen Konstruktor erweitert haben, der einen Parameter vom Typ std::string akzeptiert, passt dies also alles gut zusammen. Das Objekt f2 wird demnach nicht mit dem Wert "Lufthansa" initialisiert, sondern mit dem Wert, der dem Konstruktor als Parameter übergeben wird - in diesem Fall also mit "British Airways".

Indem Sie also hinter der Objektdefinition in Klammern Parameter angeben, können Sie den Aufruf verschiedener Konstruktoren erzwingen. Geben Sie keine Klammern an, wird der Standardkonstruktor verwendet. Geben Sie Klammern an und in Klammern keinen Parameter, wird auch der Standardkonstruktor verwendet. Folgende Objektdefinitionen führen also beide den Standardkonstruktor aus.

#include "flugzeug.h" 

flugzeug f; 
flugzeug f2(); 

Es gibt keinen Unterschied zwischen diesen beiden Code-Zeilen. Beachten Sie jedoch, dass Sie leere Klammern bei einer Objektdefinition nur dann angegeben können, wenn der verwendete Typ - also die Klasse - explizit einen Standardkonstruktor definiert. Andernfalls müssen Sie die leeren Klammern weglassen, damit der Compiler sich nicht beschwert.

Konstruktoren werden sehr häufig definiert. Es müssen fast immer die Eigenschaften eines Objekts initialisiert und mit vernünftigen Werten vorbelegt werden, damit bei der erstmaligen Verwendung mit dem Objekt auch sinnvoll gearbeitet werden kann. Für die Initialisierung von Eigenschaften hatten wir bereits folgenden Code für den Standardkonstruktor der Klasse flugzeug gesehen.

flugzeug::flugzeug() 
{ 
  Fluggesellschaft = "Lufthansa"; 
} 

Während dieser Code einwandfrei funktioniert, kann er optimiert werden. Verwenden Sie zur Initialisierung von Eigenschaften in Konstruktoren sogenannte Initialisierungslisten, weil diese schneller sind.

flugzeug::flugzeug() 
  : Fluggesellschaft("Lufthansa") 
{ 
} 

Eine Initialisierungsliste sieht so aus, dass die Eigenschaften, die initialisiert werden sollen, hinter einem Doppelpunkt, der hinter dem Methodenkopf steht, angegeben werden. Es können in einer Initialisierungsliste ausschließlich Eigenschaften initialisiert werden. Zwei oder mehr Eigenschaften werden durch Komma voneinander getrennt. Die Initialisierung findet derart statt, dass der Wert, auf den die jeweilige Eigenschaft gesetzt werden soll, in Klammern hinter der Eigenschaft angegeben wird.

Es handelt sich also genaugenommen um einen Aufruf eines Konstruktors. Im obigen Beispiel wird für das Objekt Fluggesellschaft, das vom Typ std::string ist, ein Konstruktor aufgerufen, der als einzigen Parameter eine Zeichenkette erwartet. Nachdem ein derartiger Konstruktor tatsächlich in der Klasse std::string definiert ist, funktioniert das Beispiel problemlos.

Der C++-Standard sieht übrigens vor, dass Sie auch Initialisierungslisten für Eigenschaften von intrinsischen Datentypen verwenden können, also beispielsweise für int-Eigenschaften. Sie können also auch für int-Eigenschaften in Klammern eine Zahl angeben, mit der die jeweilige Eigenschaft initialisiert werden soll.

Initialisierungslisten können nicht nur für Standardkonstruktoren verwendet werden, sondern für jeden Konstruktor. Der optimierte Code für die Datei flugzeug.cpp, um die Konstruktoren zu implementieren, sieht daher wie folgt aus.

#include "flugzeug.h" 

flugzeug::flugzeug() 
  : Fluggesellschaft("Lufthansa") 
{ 
} 

flugzeug::flugzeug(std::string fluggesellschaft) 
  : Fluggesellschaft(fluggesellschaft) 
{ 
} 

Sie sehen, dass auch ohne Probleme Parameter von Konstruktoren als Initialisierungswerte in einer Initialisierungsliste verwendet werden können.

Verwenden Sie wann immer möglich Initialisierungslisten. Es gibt Situationen, in denen das nicht funktioniert und Sie gezwungen sind, direkt innerhalb des Konstruktors eine Eigenschaft zu initialisieren. Es kann beispielsweise sein, dass Sie erst eine komplexe Rechenoperation durchführen müssen, dessen Ergebnis in einer Eigenschaft gespeichert werden soll. Es macht dann sicher keinen Sinn, die gesamte Rechenoperation in eine Initialisierungsliste zu pressen - was unter Umständen auch gar nicht möglich ist. In diesem Fall führen Sie demnach die Berechnung innerhalb des Konstruktors aus und weisen dann nach der Berechnung das Ergebnis der entsprechenden Eigenschaft im Konstruktor zu.

Wichtig ist in diesem Zusammenhang noch: Bei einem Konstruktoraufruf wird zuerst die Initialisierungsliste ausgeführt, dann der Methodenrumpf. Das heißt, dass Sie innerhalb eines Konstruktors bereits mit Eigenschaften arbeiten können, die über eine Initialisierungsliste gesetzt worden sind.


2.3 Destruktor

Automatischer Aufruf bei Objektzerstörung

So wie es Methoden gibt, die bei einer Objektdefinition automatisch ausgeführt werden - die eben kennengelernten Konstruktoren - gibt es in C++ auch eine Methode, die automatisch bei einer Objektzerstörung aufgerufen wird. Objektzerstörung meint hierbei, dass das Objekt aus dem Speicher gelöscht wird, wenn beispielsweise der Gültigkeitsbereich des Objekts endet.

Objekte können mehrere Konstruktoren besitzen, die sich in Anzahl und Typ der Parameter unterscheiden. Jedes Objekt besitzt jedoch nur einen einzigen Destruktor. Denn beim Löschen eines Objekts aus dem Speicher können keine Parameter übergeben werden - der Gültigkeitsbereich endet einfach.

Die Definition eines Destruktors lernen Sie im Folgenden kennen. Es wird auf die Klasse flugzeug zurückgegriffen, die nun um einen Destruktor erweitert wird.

#include <string> 

class flugzeug 
{ 
  public: 
    flugzeug(); 
    flugzeug(std::string fluggesellschaft); 
    ~flugzeug(); 

  private: 
    std::string Fluggesellschaft; 

}; 

Ein Destruktor ist eine Methode, die den gleichen Namen wie die Klasse erhält und mit einer Tilde beginnt. Diese Methode wird wie gesagt automatisch beim Löschen eines Objekts aus dem Speicher aufgerufen.

#include "flugzeug.h" 

flugzeug::flugzeug() 
  : Fluggesellschaft("Lufthansa") 
{ 
} 

flugzeug::flugzeug(std::string fluggesellschaft) 
  : Fluggesellschaft(fluggesellschaft) 
{ 
} 

flugzeug::~flugzeug() 
{ 
} 

Normalerweise müssen Destruktoren nicht in Klassen definiert werden. Beim Löschen eines Objekts werden selbstverständlich auch alle Eigenschaften des Objekts aus dem Speicher gelöscht. Eigene Aufräumarbeiten in einem Destruktor sind also normalerweise nicht notwendig.

Destruktoren sind immer dann wichtig, wenn beim Löschen eines Objekts aus dem Speicher Aufräumarbeiten erledigt werden müssen. Dies ist zum Beispiel der Fall, wenn das Objekt dynamisch Speicher allokiert hat. Die dynamische Speicherallokation wird in der C++-Standardbibliothek durch einen sogenannten smart pointer namens std::auto_ptr unterstützt, so dass auch in diesem Fall kein Destruktor mehr notwendig ist. Der smart pointer std::auto_ptr verwendet seinerseits einen Destruktor, um dynamisch reservierten Speicher freizugeben, wenn der Gültigkeitsbereich des smart pointers endet.


2.4 Copy-Konstruktor

Automatischer Aufruf bei Objektduplizierung

Beim Copy-Konstruktor handelt es sich um einen Konstruktor, der als einzigen Parameter eine konstante Referenz auf ein Objekt vom Typ der Klasse erhält, in der er definiert ist. Der Copy-Konstruktor für die Klasse flugzeug sieht also wie folgt aus.

#include <string> 

class flugzeug 
{ 
  public: 
    flugzeug(); 
    flugzeug(std::string fluggesellschaft); 
    ~flugzeug(); 
    flugzeug(const flugzeug &f); 

  private: 
    std::string Fluggesellschaft; 
}; 

Der Copy-Konstruktor ist ein Konstruktor, der für jede Klasse implizit definiert ist. Die implizite Definition des Konstruktors sieht so aus, dass Werte der Eigenschaften des Parameters in die Eigenschaften des aktuellen Objekts kopiert werden. Im Folgenden wird der Copy-Konstruktor der Klasse flugzeug so definiert, dass er die implizite Definition der Methode nachvollzieht.

#include "flugzeug.h" 

flugzeug::flugzeug() 
  : Fluggesellschaft("Lufthansa") 
{ 
} 

flugzeug::flugzeug(std::string fluggesellschaft) 
  : Fluggesellschaft(fluggesellschaft) 
{ 
} 

flugzeug::~flugzeug() 
{ 
} 

flugzeug::flugzeug(const flugzeug &f) 
{ 
  Fluggesellschaft = f.Fluggesellschaft; 
} 

Im Copy-Konstruktor wird also nun der Wert der Eigenschaft Fluggesellschaft des als Parameter übergebenen Objekts in die Eigenschaft Fluggesellschaft des aktuellen Objekts kopiert. Das Schlüsselwort const in der Parameterliste des Konstruktors bedeutet, dass innerhalb der Methode der Parameter konstant ist. Der Parameter darf also gelesen, jedoch nicht beschrieben werden.

Wann wird der Copy-Konstruktor aufgerufen? Ein Copy-Konstruktor kommt in zwei Situationen zum Einsatz: Bei einer Objektdefinition kann er explizit aufgerufen werden, indem als einziger Parameter dem neu erstellten Objekt ein bisher existierendes Objekt der gleichen Klasse übergeben wird. Bei einem Funktions- oder Methodenaufruf mit einem Parameter vom Typ der Klasse wird, wenn es sich nicht um eine Referenz handelt, das neu erstellte lokale Objekt in der Funktion oder Methode per Copy-Konstruktor mit dem als Parameter angegebenen Objekt initialisiert.

#include "flugzeug.h" 

flugzeug f; 
flugzeug f2(f); 

Im obigen Beispiel wird der Copy-Konstruktor explizit aufgerufen: Das Objekt f2 wird mit einem Objekt des gleichen Typs, nämlich f, initialisiert. Für f wurde der Standardkonstruktor aufgerufen, so dass es sich demnach um ein Flugzeug der Fluggesellschaft Lufthansa handelt. Per Copy-Konstruktor wird der Wert der Eigenschaft Fluggesellschaft des Objekts f in das Objekt f2 hinüberkopiert. Somit werden alle Eigenschaften von f2 auf die gleichen Werte wie in f gesetzt - das Objekt wird dupliziert. Auch das Flugzeug f2 gehört daher der Fluggesellschaft Lufthansa.

Der zweite Fall, in dem der Copy-Konstruktor Verwendung findet, ist etwas komplizierter. Sehen Sie sich folgenden Code an.

#include "flugzeug.h" 

void starten(flugzeug f2) 
{ 
} 

int main() 
{ 
  flugzeug f; 
  starten(f); 
} 

In diesem kleinen Programm wird ein Objekt f vom Typ flugzeug erstellt. Dieses Objekt wird als Parameter an die Funktion starten() übergeben. Die Übergabe erfolgt hierbei standardmäßig in C++ als Kopie. Das heißt, in starten() ist der Parameter f2 lediglich eine Kopie des übergebenen Objekts. Möchten Sie mit dem Original in der Funktion arbeiten, müssen Sie, wie Sie wissen, den Parameter als Referenz übergeben.

Das lokale Objekt f2 in der Funktion starten() wird dadurch initialisiert, indem der Copy-Konstruktor aufgerufen wird. Es findet also im Hintergrund irgendwo unsichtbar der Aufruf eines Copy-Konstruktors statt.

flugzeug f2(f); 

Bei Aufruf der Funktion starten() wird ein lokales Objekt f2 angelegt, dem bei der Definition das Objekt übergeben wird, das als Parameter an die Funktion starten() übergeben wurde. Dies ist der zweite Anwendungsfall für einen Copy-Konstruktor.

Vergessen Sie nicht, dass der Copy-Konstruktor standardmäßig für Objekte definiert ist. Sie müssen den Copy-Konstruktor also nur dann selbst definieren, wenn Sie die ursprüngliche Arbeitsweise des Konstruktors verändern wollen.


2.5 Zuweisungsoperator

Automatischer Aufruf bei Zuweisung

Der Zuweisungsoperator ist eng mit dem Copy-Konstruktor verwandt: Auch er ist standardmäßig für Objekte definiert und muss nicht extra von Ihnen implementiert werden. Die Standarddefinition des Zuweisungsoperators ist sogar mit der des Copy-Konstruktors identisch. Genau wie der Copy-Konstruktor kopiert auch der Zuweisungsoperator Werte von Eigenschaften eines Objekts in Eigenschaften des anderen Objekts - vorausgesetzt, beide Objekte sind vom Typ der gleichen Klasse.

Die Klasse flugzeug um die Definition des Zuweisungsoperators erweitert sieht wie folgt aus.

#include <string> 

class flugzeug 
{ 
  public: 
    flugzeug(); 
    flugzeug(std::string fluggesellschaft); 
    ~flugzeug(); 
    flugzeug(const flugzeug &f); 
    flugzeug &operator=(const flugzeug &f); 

  private: 
    std::string Fluggesellschaft; 
}; 

Diese Methode hat eine kompliziertere Syntax im Vergleich zu den bisher kennengelernten Methoden. Den genauen Aufbau der Methode werden Sie im Kapitel 4, Überladen von Operatoren kennenlernen, in dem auf das Überladen von Operatoren eingegangen wird. Was Sie jedoch anhand der Methodendeklaration ablesen können, ist, dass diese Methode eine Referenz auf ein Objekt vom Typ der Klasse zurückgibt und als Parameter eine konstante Referenz auf ein Objekt vom Typ der Klasse erwartet.

#include "flugzeug.h" 

flugzeug::flugzeug() 
  : Fluggesellschaft("Lufthansa") 
{ 
} 

flugzeug::flugzeug(std::string fluggesellschaft) 
  : Fluggesellschaft(fluggesellschaft) 
{ 
} 

flugzeug::~flugzeug() 
{ 
} 

flugzeug::flugzeug(const flugzeug &f) 
{ 
  Fluggesellschaft = f.Fluggesellschaft; 
} 

flugzeug &flugzeug::operator=(const flugzeug &f) 
{ 
  Fluggesellschaft = f.Fluggesellschaft; 
  return *this; 
} 

Wie bereits angedeutet geschieht in dieser Methode das gleiche wie im Copy-Konstruktor: Werte werden von Eigenschaften des einen Objekts in Eigenschaften des anderen Objekts kopiert. Nachdem der Zuweisungsoperator jedoch einen Rückgabewert besitzt, muss auch mit return ein passender Wert zurückgegeben werden. Laut Methodendeklaration muss eine Referenz auf ein Objekt zurückgegeben werden. Tatsächlich muss nicht eine Referenz auf irgendein Objekt zurückgegeben werden, sondern eine Referenz auf das aktuelle Objekt. Das ist zwar und kann auch gar nicht technisch durch die Methodendeklaration vorgegeben werden. Es ergibt sich jedoch aus der Verwendungsweise von Zuweisungsoperatoren, wie Sie gleich sehen werden.

Die Angabe hinter dem Schlüsselwort return ist neu. Bei this handelt es sich um einen Zeiger, der auf das aktuelle Objekt zeigt. Einen Zeiger können Sie sich in aller Kürze als Adresse oder Hausnummer eines Objekts im Speicher des Computers vorstellen. Nachdem jedoch nicht die Adresse des Objekts, sondern das Objekt selbst vom Zuweisungsoperator zurückgegeben werden soll, wird vor das Schlüsselwort this ein Sternchen gesetzt. Das bedeutet, dass das Objekt, auf das this zeigt, zurückgegeben werden soll - also das Objekt selber. Da es kein Schlüsselwort in C++ gibt, mit dem ein Objekt auf sich selbst Bezug nehmen kann, sondern nur das Schlüsselwort this existiert, mit dem auf die eigene Adresse eines Objekts im Speicher Bezug genommen wird, ist *this die einzige Möglichkeit, mit der ein Objekt auf sich selber verweisen kann.

Der Aufruf des Zuweisungsoperators geschieht immer dann, wenn ein Objekt einem anderen mit dem Operator = zugewiesen wird.

#include "flugzeug.h" 

int main() 
{ 
  flugzeug f; 
  flugzeug f2("British Airways"); 
  f = f2; 
} 

Während in der ersten Zeile der Funktion main() der Standardkonstruktor und in der zweiten Zeile ein Konstruktor mit einem Parameter ausgeführt wird, wird in der dritten Zeile der Zuweisungsoperator aufgerufen. Das Objekt f2 wird hierbei als Parameter an die Methode operator=() für das Objekt f übergeben. Das Objekt f speichert also nach der dritten Code-Zeile in main() in der Eigenschaft Fluggesellschaft nun auch den Wert British Airways.

Man kann den Zuweisungsoperator anstatt über den Operator = auch explizit als Methode für ein Objekt aufrufen. Folgender Code ist völlig identisch zum obigen Code und verdeutlicht, dass der Zuweisungsoperator letztendlich eine ganz normale Methode ist.

#include "flugzeug.h" 

int main() 
{ 
  flugzeug f; 
  flugzeug f2("British Airways"); 
  f.operator=(f2); 
} 

Warum der Zuweisungsoperator das jeweils aktuelle Objekt zurückgibt, wird klar, wenn Sie sich folgendes Code-Beispiel ansehen.

#include "flugzeug.h" 

int main() 
{ 
  flugzeug f; 
  flugzeug f2("British Airways"); 
  flugzeug f3(f); 
  f3 = f = f2; 
} 

In der letzten Code-Zeile werden mehrere Operatoren kombiniert. Eine derartige Kombination wird gemäß der Präzedenz-Tabelle von C++ aufgelöst. In diesem Fall handelt es sich um Verknüpfungen mit dem Operator =. Dieser Operator wird von rechts nach links aufgelöst. Es findet demnach zuerst eine Zuweisung von f2 an f statt, dann eine Zuweisung des Rückgabewertes an f3. Damit der Rückgabewert der ersten Zuweisung an f3 zugewiesen werden kann, muss es grundsätzlich einen Rückgabewert geben. Wenn dies das aktuelle Objekt ist, wird die Arbeitsweise des Operators = für intrinsische Datentypen in C++ nachgeahmt, und Verkettungen unter Einsatz mehrerer Operatoren wie im obigen Fall sind möglich.

Es gibt noch eine Besonderheit beim Zuweisungsoperator: Im folgenden Beispiel wird nicht der Zuweisungsoperator, sondern der Copy-Konstruktor ausgeführt.

#include <flugzeug.h> 

int main() 
{ 
  flugzeug f("British Airways"); 
  flugzeug f2 = f; 
} 

Wenn der Operator = bei einer Objektdefinition eingesetzt wird, wird nicht der Zuweisungsoperator ausgeführt, sondern der Copy-Konstruktor. Dies ist die einzige Ausnahme, die sich dadurch erklären lässt, dass bei einer Objektdefinition grundsätzlich immer ein Konstruktor ausgeführt wird.

Vergessen Sie nicht: Der Zuweisungsoperator ist für jedes Objekt implizit definiert. Sie brauchen diese Methode nicht selber definieren. Dies kommt nur in Betracht, wenn Sie die Funktionsweise des Zuweisungsoperators ändern wollen.


2.6 Statische Methoden

Klassen-Methoden

Statische Methoden sind Methoden, die nicht einem Objekt oder einer Instanz zugeordnet sind, sondern einer Klasse. In technischer Hinsicht sind statische Methoden Methoden, die ausschließlich auf statische Eigenschaften einer Klasse zugreifen dürfen. Wozu statische Eigenschaften benötigt werden, soll anhand eines Beispiels veranschaulicht werden.

Sie wollen in Ihrem Programm jederzeit darüber Bescheid wissen, wie viele Objekte es vom Typ flugzeug gibt, wie viele Flugzeuge also bisher erstellt wurden. Es bietet sich an, im Konstruktor der Klasse flugzeug eine Variable jeweils um den Wert 1 zu erhöhen und im Destruktor die gleiche Variable jeweils um den Wert 1 zu verringern. Auf diese Weise könnten Sie jederzeit ablesen, wie viele Objekte es vom Typ flugzeug momentan gibt.

Die Frage ist, was für eine Variable Sie am besten verwenden. Lokale Variablen scheiden aus, da diese jeweils nur innerhalb einer Funktion gültig sind. Globale Variablen wären eine Möglichkeit, besitzen jedoch einen sehr großen Gültigkeitsraum, der im Optimalfall immer so weit wie möglich eingeschränkt werden sollte. Bleiben noch Objekt-Eigenschaften. Hier gibt es das Problem, dass jedes Objekt seine eigenen Eigenschaften hat. Wird eine Eigenschaft in einem Objekt verändert, so ändert sich deswegen nicht die Eigenschaft in einem anderen Objekt. Würde man versuchen, in einem Konstruktor und Destruktor jeweils den Wert einer Eigenschaft zu erhöhen und zu verringern, so würde jeweils nur die Eigenschaft im aktuellen Objekt geändert werden. Es handelt sich also jedesmal um völlig unabhängige Eigenschaften. Was benötigt wird, ist eine Eigenschaft, die es ein einziges Mal für alle Objekt gibt. Und wenn ein Objekt diese Eigenschaft ändert, ändert sich diese Eigenschaft auch in anderen Objekten, weil diese Eigenschaft eben von allen Objekten geteilt wird.

Statische Eigenschaften sind solche Eigenschaften, deren Wert von allen Objekten geteilt wird. Ändert ein Objekt den Wert einer statischen Eigenschaft, ist die Änderung in allen anderen Objekten, die auf der gleichen Klasse basieren, sichtbar.

Um eine statische Eigenschaft zu definieren, müssen Sie sie mit dem Schlüsselwort static versehen.

#include <string> 

class flugzeug 
{ 
  public: 
    flugzeug(); 
    flugzeug(std::string fluggesellschaft); 
    ~flugzeug(); 
    flugzeug(const flugzeug &f); 
    flugzeug &operator=(const flugzeug &f); 

  private: 
    std::string Fluggesellschaft; 
    static int Anzahl; 
}; 

Statische Eigenschaften werden nicht nur von allen Objekten geteilt, sondern stehen auch dann zur Verfügung, wenn es gar keine Objekte gibt. Sie werden dann über den Klassennamen und den Zugriffsoperator für Klassen :: angesprochen. Man spricht deshalb bei statischen Eigenschaften auch von Klasseneigenschaften, während die bisher von Ihnen verwendeten Eigenschaften Objekt- oder Instanzeigenschaften waren. Statische Eigenschaften besitzen einen zeitlich uneingeschränkten, jedoch räumlich eingeschränkten Gültigkeitsbereich. Sie sind eine Art globale Variable für Objekte einer Klasse.

Statische Eigenschaften werden nicht über Konstruktoren initialisiert. Konstruktoren werden erst bei einer Objektdefinition aufgerufen. Statische Eigenschaften sind jedoch ab dem Programmstart sofort verfügbar, selbst wenn es noch gar keine Objekte gibt. Die Initialisierung erfolgt bei statischen Eigenschaften daher auf eine andere Art und Weise.

#include "flugzeug.h" 

int flugzeug::Anzahl = 0; 

flugzeug::flugzeug() 
  : Fluggesellschaft("Lufthansa") 
{ 
} 

flugzeug::flugzeug(std::string fluggesellschaft) 
  : Fluggesellschaft(fluggesellschaft) 
{ 
} 

flugzeug::~flugzeug() 
{ 
} 

flugzeug::flugzeug(const flugzeug &f) 
{ 
  Fluggesellschaft = f.Fluggesellschaft; 
} 

flugzeug &flugzeug=(const flugzeug &f) 
{ 
  Fluggesellschaft = f.Fluggesellschaft; 
  return *this; 
} 

Statische Eigenschaften werden außerhalb der Klassendefinition initialisiert. Hierbei wird ähnlich wie bei Methoden die Variable nochmal neu definiert, jedoch wird wiederum die Verbindung zur Klasse über den Zugriffsoperator :: hergestellt. Gleichzeitig kann dieser Variablen über den Zuweisungsoperator = ein Wert zugewiesen werden, mit dem die statische Eigenschaft initialisiert wird.

Eine statische Methode wird ebenfalls wie eine statische Eigenschaft definiert - sie erhält das Schlüsselwort static.

#include <string> 

class flugzeug 
{ 
  public: 
    flugzeug(); 
    flugzeug(std::string fluggesellschaft); 
    ~flugzeug(); 
    flugzeug(const flugzeug &f); 
    flugzeug &operator=(const flugzeug &f); 
    static int anzahl(); 

  private: 
    std::string Fluggesellschaft; 
    static int Anzahl; 
}; 

Das Besondere an statischen Methoden: Sie können ohne Angabe eines Objekts, nämlich direkt über den Klassennamen aufgerufen werden. Daraus resultiert die Beschränkung, dass statische Methoden nur auf statische Eigenschaften zugreifen dürfen. Auf welche Objekteigenschaften soll auch zugegriffen werden, wenn die Methode direkt über den Klassennamen aufgerufen wird und es beispielsweise überhaupt kein Objekt gibt?

Nachdem die Klasse flugzeug nur eine einzige statische Eigenschaft definiert, darf in der statischen Methode anzahl() daher auch nur auf diese Eigenschaft zugegriffen werden.

#include "flugzeug.h" 

int flugzeug::Anzahl = 0; 

flugzeug::flugzeug() 
  : Fluggesellschaft("Lufthansa") 
{ 
} 

flugzeug::flugzeug(std::string fluggesellschaft) 
  : Fluggesellschaft(fluggesellschaft) 
{ 
} 

flugzeug::~flugzeug() 
{ 
} 

flugzeug::flugzeug(const flugzeug &f) 
{ 
  Fluggesellschaft = f.Fluggesellschaft; 
} 

flugzeug &flugzeug::operator=(const flugzeug &f) 
{ 
  Fluggesellschaft = f.Fluggesellschaft; 
  return *this; 
} 

int flugzeug::anzahl() 
{ 
  return Anzahl; 
} 

Die statische Methode anzahl() ist so definiert, dass sie einfach den Wert der Eigenschaft Anzahl zurückgibt. Auf diese Weise kann die Eigenschaft gelesen werden, was anders wegen des Zugriffsattributs private in der Klassendefinition nicht möglich wäre.

Bis dato speichert die statische Eigenschaft Anzahl lediglich den Wert 0, ohne dass sonst etwas passiert. Um in dieser Variable nun mitzuzählen, wie viele Objekte es von der Klasse flugzeug gibt, werden die Konstruktoren und der Destruktor erweitert.

#include "flugzeug.h" 

int flugzeug::Anzahl = 0; 

flugzeug::flugzeug() 
  : Fluggesellschaft("Lufthansa") 
{ 
  ++Anzahl; 
} 

flugzeug::flugzeug(std::string fluggesellschaft) 
  : Fluggesellschaft(fluggesellschaft) 
{ 
  ++Anzahl; 
} 

flugzeug::~flugzeug() 
{ 
  --Anzahl; 
} 

flugzeug::flugzeug(const flugzeug &f) 
{ 
  ++Anzahl; 
  Fluggesellschaft = f.Fluggesellschaft; 
} 

flugzeug &flugzeug::operator=(const flugzeug &f) 
{ 
  Fluggesellschaft = f.Fluggesellschaft; 
  return *this; 
} 

int flugzeug::anzahl() 
{ 
  return Anzahl; 
} 

Sie sehen, es ist kein Problem, von Instanzmethoden auf statische Eigenschaften zuzugreifen. Andersrum ist es jedoch nicht erlaubt, von statischen Methoden auf Instanzeigenschaften zuzugreifen.

Nachdem die Klasse flugzeug nun wie oben gezeigt erweitert wurde, wird sie in folgendem Beispielprogramm eingesetzt.

#include "flugzeug.h" 
#include <iostream> 

int main() 
{ 
  flugzeug f; 
  flugzeug f2; 
  { 
    flugzeug f3; 
    std::cout << f3.anzahl() << std::endl; 
  } 
  std::cout << flugzeug::anzahl() << std::endl; 
} 

In der Funktion main() werden drei Objekte vom Typ flugzeug angelegt. Das Objekt f3 erhält einen eigenen kleineren Gültigkeitsbereich, indem es nochmal in geschweifte Klammern gestellt wird.

Die statische Methode anzahl() der Klasse flugzeug wird zweimal im Code aufgerufen. Das eine Mal wird über das Objekt f3 auf diese Methode zugegriffen, das andere Mal über die Klasse flugzeug selbst. Diese beiden Zugriffsarten stehen bei allen statischen Methoden und auch statischen Eigenschaften zur Verfügung, soweit Zugriffsattribute den Zugriff erlauben.

Im obigen Beispiel wird der Rückgabewert von anzahl() zweimal auf die Standardausgabe ausgegeben. Im ersten Fall wird die Zahl 3 ausgegeben, beim zweiten Aufruf der Methode der Wert 2. Weil der Gültigkeitsbereich des dritten Objekts beim wiederholten Aufruf von anzahl() bereits beendet ist, wurde das Objekt f3 inzwischen aus dem Speicher gelöscht. Der Destruktor wurde aufgerufen und die statische Eigenschaft um den Wert 1 verringert. Es stehen daher nur mehr zwei Objekte vom Typ flugzeug zur Verfügung, so dass die Methode anzahl() den Wert 2 zurückgibt.


2.7 Aufgaben

Übung macht den Meister

Sie können die Lösungen zu allen Aufgaben in diesem Buch als ZIP-Datei erwerben.

  1. Erstellen Sie eine Klasse flughafen und legen Sie ein Objekt vom Typ dieser Klasse in der Funktion main() an. Definieren Sie die Klasse flughafen in der Art, dass beim Anlegen eines Objekts die Meldung "Flughafen eroeffnet" auf den Bildschirm ausgegeben wird. Beim Löschen des Objekts aus dem Speicher soll die Meldung "Flughafen geschlossen" ausgegeben werden.

    Sie müssen in der Klasse flughafen lediglich den Standardkonstruktor und den Destruktor definieren. In diesen Methoden geben Sie die entsprechenden Meldungen auf den Bildschirm aus.

  2. Erweiteren Sie die Klasse flughafen aus Aufgabe 1 um zwei Methoden starten() und landen(). Diese Methoden sollen als einzigen Parameter ein Objekt vom Typ einer Klasse flugzeug erwarten. Der Flughafen soll über diese Methoden mitverfolgen, wie viele Flugzeuge sich zu jedem beliebigen Zeitpunkt am Flughafen befinden und zu welchen Fluggesellschaften sie gehören. Der Einfachheit halber wird empfohlen, als Eigenschaft in der Klasse flughafen ein Array vom Typ flugzeug zu verwenden, das die verfügbaren Stellplätze für Flugzeuge am Flughafen symbolisiert. Ist für ein Flugzeug in diesem Array keine Fluggesellschaft angegeben, soll dies bedeuten, dass dieser Stellplatz frei ist. Über die Methoden starten() und landen() sollen Stellplätze aufgefüllt bzw. freigegeben werden.

    Erstellen Sie zum Test in der Funktion main() mehrere Objekte vom Typ flugzeug, die unterschiedlichen Fluggesellschaften gehören sollen. Lassen Sie Flugzeuge auf dem Flughafen landen und wieder starten. Rufen sie zu unterschiedlichen Zeitpunkten in Ihrem Programm eine Methode flugzeuge() auf, die Sie für die Klasse flughafen definieren sollen und die die Namen der Fluggesellschaften aller momentan am Flughafen stehender Flugzeuge ausgibt. Auf diese Weise können Sie Ihr Programm testen.

    Gehen Sie Schritt für Schritt vor: Erstellen Sie zuerst eine Klasse flugzeug, die laut Aufgabenstellung lediglich eine Eigenschaft Fluggesellschaft benötigt. Diese kann wie in den Beispielen in diesem Kapitel vom Typ std::string sein. Da Flugzeuge unterschiedlichen Fluggesellschaften gehören sollen, können Sie einen Konstruktor definieren, dem als einzigen Parameter der Name einer Fluggesellschaft übergeben werden kann. Dieser Name soll dann in der Eigenschaft Fluggesellschaft gespeichert werden.

    Der Flughafen soll mehrere Stellplätze für Flugzeuge anbieten. Wie in der Aufgabenstellung empfohlen können Sie dazu eine Eigenschaft vom Typ eines Arrays anlegen. So können Sie beispielsweise mit flugzeuge Flugzeuge[10] für Ihren Flughafen zehn Stellplätze definieren. Da das Array aus zehn Objekten vom Typ flugzeug besteht und diese Flugzeuge zu Beginn des Programms freie Stellplätze darstellen sollen, empfiehlt es sich, die Eigenschaft Fluggesellschaft in der Klasse flugzeug im Standardkonstruktor nicht zu initialisieren. Dann wird die Eigenschaft Fluggesellschaft ihrerseits per Standardkonstruktor initialisiert, was für std::string bedeutet, dass sie leer bleibt. Und ein Flugzeug mit einer nicht angegebenen Fluggesellschaft soll einen leeren Stellplatz symbolisieren.

    Definieren Sie als nächstes die beiden Methoden starten() und landen(). Der Trick ist nun, in diesen Methoden mit einer Schleife über die Stellplätze im Array zu iterieren. Für starten() müssen Sie den entsprechenden Stellplatz finden, auf dem das Flugzeug steht, das nun starten soll. Vergleichen Sie dazu den Namen des Flugzeugs, der als Parameter an starten() übergeben wurde, mit den Namen der Flugzeuge im Array. Haben Sie das Flugzeug gefunden, setzen sie den Namen der Fluggesellschaft mit "" auf einen leeren String, um anzugeben, dass das Flugzeug gestartet ist und der Stellplatz somit wieder frei ist. Für den Vergleich und das Setzen der Namen müssen Sie der Klasse flugzeug entsprechende Methoden hinzufügen.

    Für landen() gehen Sie analog vor, nur dass Sie nun nach einem freien Stellplatz suchen und dann den Namen der Fluggesellschaft vom Flugzeug, das als Parameter übergeben wurde, in das Array kopieren.

    Im letzten Schritt definieren Sie die Methode flugzeuge(), die ebenfalls mit einer Schleife über das Array iteriert und die Namen der Fluggesellschaften ausgibt. Flugzeuge im Array, für die keine Fluggesellschaft angegeben ist, sollen ignoriert werden, denn diese Flugzeuge symbolisieren ja freie Stellplätze.

  3. Erweitern Sie Ihre Lösung zu Aufgabe 2 dahingehend, dass mitgezählt werden soll, wie oft am Flughafen Flugzeuge gestartet und gelandet sind. Fügen Sie der Klasse flughafen eine neue Methode sequenzen() hinzu, die auf die Standardausgabe die Anzahl aller Start- und Landesequenzen ausgibt. Sie müssen dabei Start- und Landesequenzen nicht unterscheiden. Es reicht, wenn eine Zahl für alle Start- und Landesequenzen ausgegeben wird. Landen zum Beispiel zwei Flugzeuge und startet eines, soll 3 ausgegeben werden.

    Fügen Sie der Klasse flughafen eine neue Eigenschaft vom Typ int hinzu, die Sie zum Beispiel Sequenzen nennen. Diese Eigenschaft initialisieren Sie im Standardkonstruktor von flughafen mit 0. In den Methoden starten() und landen() erhöhen Sie die Eigenschaft jeweils um 1.

  4. Schreiben Sie Ihre Lösung zu Aufgabe 3 so um, dass die Anzahl der Start- und Landesequenzen nicht mehr in einer Eigenschaft der Klasse flughafen gespeichert und mitgezählt wird, sondern in der Klasse flugzeug. Entfernen Sie die Methode sequenzen() von der Klasse flughafen und fügen Sie sie der Klasse flugzeug hinzu.

    Für diese Aufgabe ist es nötig, auf das Schlüsselwort static zuzugreifen. Denn um die Anzahl der Start- und Landesequenzen für alle Flugzeuge zu speichern, wird eine Variable benötigt, auf die alle Objekte vom Typ flugzeug zugreifen können. Da diese Variable laut Aufgabenstellung nun in der Klasse flugzeug definiert sein soll, wird eine statische Eigenschaft verwendet.

    Entfernen Sie die Eigenschaft Sequenzen in der Klasse flughafen und fügen Sie sie der Klasse flugzeug als statische Eigenschaft hinzu. Initialisieren Sie die statische Eigenschaft, indem Sie sie außerhalb der Klasse flugzeug auf 0 setzen. Denken Sie daran, dass Sie statische Eigenschaften nicht in Konstruktoren initialisieren können.

    Nachdem Sie die Eigenschaft Sequenzen in der Klasse flughafen entfernt haben, stellt sich die Frage, wo nun Start- und Landesequenzen genau mitgezählt werden sollen. Da Objekte vom Typ flugzeug als Parameter an die Methoden starten() und landen() übergeben werden, bietet es sich an, im Copy-Konstruktor der Klasse flugzeug die Eigenschaft Sequenzen jeweils um 1 zu erhöhen. Jedesmal, wenn ein Objekt vom Typ flugzeug an die Methoden starten() und landen() übergeben wird, wird mit dem Copy-Konstruktor eine Kopie erstellt, so dass hier Sequenzen um 1 erhöht werden kann. Das funktioniert natürlich nur, wenn starten() und landen() keine Referenz, sondern tatsächlich eine Kopie erwarten und der Copy-Konstruktor nicht an anderen Stellen im Programm aufgerufen wird.

  5. Entwickeln Sie eine Klasse smart_pointer, die als einzigen Parameter im Konstruktor einen Zeiger auf eine Variable vom Typ int erhält, die Sie mit new erstellen. Definieren Sie die Klasse smart_pointer derart, dass sichergestellt ist, dass der mit new reservierte Speicher freigegeben wird, wenn das Objekt zerstört wird. Testen Sie Ihre Klasse dann in der Funktion main(), indem Sie mit new eine Variable vom Typ int erstellen und ein Objekt Ihrer Klasse smart_pointer instantiieren.

    Die Klasse smart_pointer benötigt einen Konstruktor, der als einzigen Parameter einen Zeiger auf eine Variable vom Typ int erwartet. Dieser Zeiger wird in einer Eigenschaft gespeichert. Damit der dynamisch reservierte Speicher automatisch freigegeben wird, wenn das Objekt zerstört wird, definieren Sie einen Destruktor, in dem Sie für die Eigenschaft delete aufrufen.