virtual ~Bruch() {}
geschrieben.Inhaltsverzeichnis
Klassen
Wir versuchen Dinge zu verstehen, um mit ihnen interagieren zu können.— Rebecca Wirfs-Brock
Klassen vereinbaren
Klassen sind nutzerdefinierte, zusammengesetzte Datentypen. Objekte eine Klasse weisen bestimmte gemeinsame Merkmale (Datenkomponenten, Attribute) auf und verhalten sich gleichartig. Sie verfügen über eigene Methoden zur Bearbeitung ihrer eigenen Daten.
Deklaration
Eine Klasse muss zumindest deklariert werden, d.h. ein Bezeichner muss als Klassenname bekannt gemacht werden, bevor Referenzen und Zeiger auf Objekte dieser Klassen gebildet werden können.
Syntax:
class
Klassenname;
class Bruch; void ausgabe(Bruch const& x);
Definition
Die Klassendefinition (öffentliche Schnittstelle) ist eine Typvereinbarung, die Daten-Attribute und Methoden dieses Typs zusammenfasst. Bis auf die Zugriffsrechte sind struct und class gleichwertig. Einer abgeschlossenen Typdefinition kann nichts hinzugefügt werden, jedoch können durch Vererbung Typen abgeleitet werden.
Syntax:
class
Klassenname
{
Zugriffsrechte
Methoden- und
Attributvereinbarungen (in beliebiger Folge)
};
class Bruch { public: Bruch(int zaehler, int nenner); // Konstruktoren Bruch(); Bruch& add(Bruch const& b); // schreibende Methoden int zaehler() const; // lesende Methoden int nenner() const; private: Bruch& kuerzen(); // versteckte Hilfsmethoden int z, n; // Attribute };
Attribute
Die Festlegung, welche Daten zu einem Typ gehören, erfolgt bei der Definition der Klasse. Die Objekte enthalten jeweils unabhängig voneinander einen Satz von Datenkomponenten (Attributen). Ihre Startwerte können durch Konstruktoren festgelegt werden. C++11 erlaubt auch die Zuweisung von Anfangswerten bei der Definition:
class Bruch { // ... int z = 0, n = 1; };
Methoden
Methoden einer Klasse werden in der Definition der Klasse deklariert.
definieren
Der Methodenrumpf kann getrennt von Klassendefinition festgelegt werden. Die Methode hat Zugriff auf alle Komponenten ihrer Klasse.
Syntax:
Rückgabetyp Klassenname::
Methodenname(
Parameterliste)
{
Anweisungen
}
Innerhalb der Klassendefinition aufgeführte Methodenrümpfe gelten als inline definiert.
const-(Lese)-Methoden
Folgt der Parameterliste const und nur lesende Methoden aufrufen. Andererseits dürfen bei konstanten Objekten nur lesende Methoden aktiviert werden.
int Bruch::zaehler() const { return z; }
Besondere Methoden
Jede Klasse enthält, wenn nichts anderes festgelegt wurde, einige Methoden, ohne die keine Klasse auskommt: Konstruktoren, Zuweisungsoperatoren und einen Destruktor. Diese speziellen Methoden werden vom Compiler generiert, solange der Programmierer nichts anderes festlegt.
Konstruktoren
Konstruktoren (Erzeuger-Methoden) erledigen Routineaufgaben beim Erschaffen von Objekten. Eine Klasse kann mehrere Konstruktoren mit unterschiedlichen Parametern besitzen. Für Konstruktoren wird kein Rückgabetyp deklariert.
Syntax:
Klassenname(
Parameterliste);
Vor der Ausführung des Konstruktorrumpfes können (und sollten) die Attribute Startwerte erhalten (Initialisiererliste).
Syntax:
Klassenname::
Klassenname(
Parameterliste)
:
Initialisiererliste
{
Anweisungen}
Bruch::Bruch(int zaehler, int nenner) : z(zaehler), n(nenner) // anstelle von z = zaehler, { // n = nenner im Rumpf kuerzen(); }
Standardkonstruktor
Besitzt eine Klasse nutzerdefinierte Konstruktoren mit Parametern, wird der Standardkonstruktor nicht mehr automatisch erzeugt. Ein Standardkonstruktor ist u.a. notwendig, um Felder von Objekten dieser Klasse anzulegen.
Bruch::Bruch() : z(0), n(1) { }
Konstruktor-Delegation
Ein Konstruktor kann seine Aufgaben an einen anderen delegieren und nach dessen Abarbeiten weitere Anweisungen im Konstruktorrumpf ausführen (C++11):
Bruch::Bruch() : Bruch(0, 1) { }
Destruktor
Der Destruktor wird aufgerufen, wenn ein Objekt verschwindet.
Vernichten ist das Gegenteil von Erzeugen,
deshalb wird die Tilde ~
vor dem Namen der zerstörenden Methode geschrieben.
Syntax:
~
Klassenname();
Eine Klasse besitzt nur einen (parameterlosen) Destruktor. Der Standard-Destruktor vernichtet die im Objekt enthaltenen Attribute, tut aber sonst nichts. Soll beim Vernichten eines Objektes etwas Besonderes geschehen, so muss der Destruktor umdefiniert werden.
Syntax:
Klassenname::~
Klassenname() {
Anweisungen}
In der Klasse Bruch
hat der Destruktor keine besonderen Aufräumarbeiten
zu erledigen. Daher müsste er nicht definiert werden.
Sollen von der Klasse jedoch Untertypen abgeleitet werden,
ist er als virtual
zu deklarieren1):
class Bruch { // ... virtual ~Bruch() = default; }
Zugriffsrechte
Zugriffsrechte auf Attribute und Methoden werden von der Klasse verliehen. In der Klassendefinition stehende Abschnitte markieren nachfolgend aufgeführte Komponenten als
public: | von außerhalb zugreifbar, |
protected: | nur innerhalb dieser Klasse und ihrer Erben (Vererbung), |
private: | nur innerhalb dieser Klasse, aber nicht für Erben zugänglich. |
Bei Strukturen ist das Zugriffsrecht public
voreingestellt,
bei Klassen private
.
Eine Klasse kann andere Klassen oder Funktionen zu Freunden (friend) erklären, die dann Zugriff auf private und geschütze Komponenten haben.2)
Objekte
Objekte sind Variablen eines zusammengesetzten Typs (Klasse oder Struktur). Jedes Objekt hat einen eigenen, von den anderen Objekten unabhängigen Satz von Attributwerten. Beim Anlegen der Objekte können Startwerte entsprechend den Konstruktorparametern angegeben werden. Fehlen Startwerte, wird der Standardkonstruktor aufgerufen. Die Startwerte sind in runden Klammern (oder ab C++11 auch in geschweiften Klammern) zu schreiben.
Bruch a; Bruch b(1, 2); Bruch c{3, 4}; Bruch f(); // Funktion, die einen Bruch liefert Bruch* zeiger = &b;
Bei der Objektdefinition mit dem Standardkonstruktor werden keine runden Klammern geschrieben. Leere runde Klammern werden als Funktionsdeklaration interpretiert. Geschweifte statt runder Klammern haben dieses Problem nicht.3)
Zugängliche Attribute und Methoden von existierenden Objekten werden mittels Punkt- bzw. Pfeil-Operator angesprochen.
Syntax:
Variablenname.
Komponente
Objektzeiger→
Komponente
cout << b.zaehler() << endl; cout << zeiger->nenner() << endl; // b.n = 0; // nicht erlaubt, da n nicht öffentlich // zeiger->n = 0; // ebenfalls nicht erlaubt
Statische Komponenten
Als static definierte Klassenvariablen bilden einen gemeinsam nutzbaren Speicherbereich ("shared memory"). Sie sind im Namensraum der Klasse vorhanden, unabhängig davon, ob und wieviele Objekte dieser Klasse existieren. Statische Klassenfunktionen haben nur Zugriff auf statische Klassendaten.
class Objektzaehler { inline static int anzahl = 0; public: static int instanzen() { return anzahl; } Objektzaehler() { ++anzahl; } ~Objektzaehler() { --anzahl; } };
Statische Komponenten können auch ohne Vorhandensein von Objekten
als Klassenname::
Komponente angesprochen werden.
Statische Klassenvariablen ohne inline
-Deklaration (vor C++17) müssen für den Linker
einmal im Programm global angegeben werden
und sollten dabei initialisiert werden.
int Objektzaehler::anzahl = 0; // erlaubt, obwohl private!
Vererbung
Basisklassen und abgeleitete Klassen
Definierte Klassen können als Basisklassen für die Bildung abgeleiteter Klassen dienen. Erweitern der Schnittstellen und Spezialisieren von abstrakten Aktionen verfeinern das Typensystem.
Syntax:
class
Abgeleiteter_Klassenname:
Basisklassenliste
{
…
};
class GezaehlterBruch : public Bruch { public: GezaehlterBruch(int zaehler, int nenner); // Konstruktoren GezaehlterBruch(); private: Objektzaehler anzahl; };
Die Basisklassenliste kann mehrere Klassen enthalten (Mehrfachvererbung). Vor jedem Basisklassennamen sollte die Vererbungsart durch eines der Wörter private, protected oder public angegeben werden. Die Vererbungsart kann das Zugriffsrecht auf Basisbestandteile in der abgeleiteten Klasse einschränken:
private - | protected - | public -Basiskomponenten |
|
sind bei | im Erben | ||
private -Vererbung | unzugänglich | private | private |
protected -Vererbung | unzugänglich | protected | protected |
public -Vererbung | unzugänglich | protected | public |
Konstruktoren übergeben am Beginn der Initialisiererliste Parameter an Basiskonstruktoren, sofern diese Konstruktoren erfordern:
GezaehlterBruch::GezaehlterBruch(int zaehler, int nenner) : Bruch(zaehler, nenner) { } GezaehlterBruch::GezaehlterBruch() { // Basis-Standardkonstruktor genutzt }
Erweitern
(Hinzufügen von Attributen und Methoden) Abgeleitete Klassen besitzen alle Daten-Komponenten und Methoden der Basisklasse und können auf diese zugreifen, sofern sie geschützt oder öffentlich zugänglich sind. Zusätzlich können in der Definition der abgeleiteten Klasse weitere Attribute und Methoden vereinbart werden.
Spezialisieren
(Überschreiben von Methoden) Abgeleitete Klassen können Methoden der Basisklasse durch erneute Definition überschreiben und damit das Verhalten der Klasse ändern. Der Prototyp der redefinierten Methode muss mit dem in der Basisklasse übereinstimmen.
Virtuelle Methoden
Als virtual markierte Methoden der Basisklasse sind dafür vorgesehen, in abgeleiteten Klassen überschrieben zu werden.
Syntax:
class
Basisklassenname
{
virtual
Methodendeklaration
};
Wird ein abgeleitetes Objekt dann über einen Zeiger oder eine Referenz auf den Basistyp aufgefordert, diese Methode auszuführen, so wird die tatsächliche (engl. virtual) abgeleitete Methode aufgerufen und nicht die Basismethode. Die von der Basis abgeleiteten Objekte verhalten sich polymorph.
class Basis { public: virtual ~Basis() = default; virtual char const* ich_bin() const { return "Basis"; } }; class Abgeleitet : public Basis { public: char const* ich_bin() const override { return "Abgeleitet"; } }; void test() { Basis* p = new Abgeleitet(); cout << p->ich_bin() << endl; // ehrliche Antwort: Abgeleitet delete p; // ruft p->~Abgeleitet() }
Da polymorphe Objekte zumeist auch unterschiedliche Ressourcen nutzen, sollte jede Basisklasse mit virtuellen Methoden einen virtuellen Destruktor besitzen, um die per Zeiger übernommene Instanz korrekt und vollständig abbauen zu können, auch wenn dessen Rumpf vorerst leer bleibt. Sonst besteht die Gefahr eines Ressourcenlecks.
Überschriebene virtuelle Methoden können in C++11 durch override am Ende des Methodenkopfes markiert werden. Als final markierte virtuelle Methoden lassen sich nicht weiter überschreiben. Von als final markierte Klassen können keine weiteren abgeleiteten Klassen gebildet werden.
Abstrakte Methoden
Abstrakte Methoden sind virtuelle Methoden,
für die in der Basisklasse noch keine "vernünftige" Implementierung
angegeben werden kann.
Da der Methodenrumpf fehlt, wird in der Klassendefinition
ein Nullzeiger = 0
in die Methodentabelle eingetragen.
Syntax:
class
Basisklassenname
{
virtual
Methodenkopf= 0;
};
class Process { public: virtual ~Process() {} virtual void run() = 0; };
Klassen mit abstrakten Methoden (abstrakte Klassen) vereinbaren einheitliche Schnittstellen und damit Wurzeln in Klassenhierarchien. Instanzen mit dieser Schnittstelle können nur von abgeleiteten Klassen gebildet werden, die alle abstrakten Methoden überschrieben haben.
Mehrfachvererbung
Eine Klasse kann gleichzeitig von mehreren Basisklassen erben. Konstruktoren müssen die Basisobjekte in der Reihenfolge initialisieren, die durch die Basisklassenliste vorgegeben ist.
class GezaehlterBruch : public Bruch, private ObjektZaehler { public: GezaehlterBruch(int zaehler, int nenner); // Konstruktoren GezaehlterBruch(); };
Mehrfachvererbung bereitet Probleme,
- weil die relativen Adressen (Offsets) der Datenfelder und virtuellen Methodentabellen von der Reihenfolge in der Basisklassenliste abhängen,
- wenn Basisklassen gleichnamige Elemente haben. Dann müssen diese mit ihrem Basisklassennamen qualifiziert werden. Gleichnamige Methoden können überschrieben werden, um den Konflikt aufzulösen.
- wenn Basisklassen von einem gemeinsamen Ahnen abstammen (Vererbungsgraph in Rautenform). In den Objekten der abgeleiteten Klasse sind dann mehrere Ahnen-Instanzen vorhanden, die gesondert angesprochen werden müssen (doppelte Buchführung).
void konflikt() { struct A { int x; }; struct B1 : A {}; struct B2 : A {}; struct C : B1, B2 {} c; c.B1::x = 1; c.B2::x = 2; }
Virtuelle Basisklassen erzeugen nur eine Instanz,
auch wenn sie über mehrere Basisklassen vererbt werden.
Falls es keinen Standardkonstruktor für den Ahnen A
gibt,
muss bei C
ein Konstruktor für A
explizit
und vor den Konstruktoren von B1
und B2
aufgerufen werden.
void virtuelle_Basis() { struct A { int x; }; struct B1 : virtual A {}; struct B2 : virtual A {}; struct C : B1, B2 {} c; c.x = 1; }
In anderen Programmiersprachen wird wegen der bei Mehrfachvererbung auftretenden Probleme nur eine eingeschränkte Form (Einfachvererbung und Implementierung von rein abstrakten Schnittstellen) erlaubt. Dennoch kann Mehrfachvererbung mit einer Rautenstruktur auch in realen Problemstellungen vorkommen (siehe nebenstehende Abbildung).