namespace cpp

C++ lernen, kennen, anwenden

Benutzer-Werkzeuge

Webseiten-Werkzeuge


kennen:klassen

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(const Bruch& 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(const Bruch& 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
ObjektzeigerKomponente
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 const char* ich_bin() const { return "Basis"; }
};
 
class Abgeleitet : public Basis
{
public:
  const char* 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).

1)
Vor C++11 wurde virtual ~Bruch() {} geschrieben.
2)
Only friends are allowed to handle your private parts. ;-)
3)
Siehe auch most vexing parse und uniform initialization. Besitzt die Klasse einen Konstruktor mit Initialisiererliste, wird dieser bevorzugt.
kennen/klassen.txt · Zuletzt geändert: 2019-01-13 14:17 von rrichter