Abstrakte Klassen
Bei der Festlegung von Schnittstellen kommt es häufig vor, dass man Basisklassen hat, die zwar vorgeben, wie ein Objekt auszusehen hat, aber keine sinnvolle Implementierung haben können. Um solche Klassen dann vor der Instanziierung zu schützen macht man sie abstrakt.
Video
Code
#include <iostream>
class Haustier
{
unsigned int beine;
public:
Haustier(unsigned int b) : beine(b) {}
virtual ~Haustier() {};
unsigned int anzahlBeine() const
{
return beine;
}
virtual void gibLaut() const = 0;
};
class Katze : public Haustier
{
public:
Katze() : Haustier(4) {}
void gibLaut() const override
{
std::cout << "Miau!\n";
}
};
class Hund : public Haustier
{
public:
Hund() : Haustier(4) {}
void gibLaut() const override
{
std::cout << "Wuff!\n";
}
};
void geraeusch(Haustier const &h)
{
h.gibLaut();
}
int main()
{
Katze k;
Hund h;
Haustier t(4);
std::cout << "Katze: ";
geraeusch(k);
std::cout << "Hund: ";
geraeusch(h);
}
Erklärung
Der relevante Teil des Codes steckt in Zeile 16:
virtual void gibLaut() const = 0;. Durch die Angabe = 0 zeigt man dem
Compiler an, dass die betreffende Funktion pure virtual ist, also mit keiner
Implementierung hinterlegt. Sie bekommt also nur einen Eintrag in der vtable,
aber es liegt kein Maschinencode vor, auf den der entsprechende Eintrag zeigen
könnte.
Enthält eine Klasse eine rein virtuelle Funktion, so wird sie automatisch
abstrakt: Objekte der Klasse können nicht mehr instanziiert werden. Wo braucht
man das? Ganz einfach: man kann damit wunderbar Schnittstellen festlegen. Ein
Beispiel: wir bauen ein Stück Software, mit welchem Kinder in einem Museum die
Laute von verschiedenen Tieren anhören können (ja, wir kommen wieder zu dem
gibLaut()-Beispiel). Die Kinder ziehen sich auf einem Touch-Screen einen Chor
aus Tieren zusammen und können den dann durcheinander schnattern, bellen,
miauen, gackern, wiehern und sonstnochwas lassen (und wer jetzt denkt, das wäre
Blödsinn, der war offenbar noch nicht in so mancher Kinderabteilung von
Naturkundemuseen...). Damit die Software erweiterbar bleibt, bauen wir einen
Satz von Routinen, um die Tiere anzuzeigen, den Chor zusammenzustellen etc.pp.
Die Tiere selbst sind als einzelne Klassen implementiert. Wenn die Kinder am
Bildschirm auf Play drücken, dann wird die Liste der Tiere im Chor an den
Mischer übergeben, der holt sich von jedem Tier den Laut und mischt diese zu
einem fertigen Ausgangssignal. Damit der Mischer nichts über die Details und
Eigenarten jedes einzelnen Tiers wissen muss, gibt es eine gemeinsame
Basisklasse Tier. Die Liste enthält also nur Tiere (wir werden später bei der
Standard Template Library noch sehen, wieso die Liste nur Objekte gleichen Typs
enthalten kann). Da die Klasse Tier aber nicht sinnvoll instanziiert werden kann
(welches Geräusch sollte ein Tier schon machen? Fragt das mal nen 6-jährigen und
der fragt prompt zurück, welches Tier denn bitteschön), machen wir die Methode
gibLaut() pure virtual und überschreiben sie in jeder Basisklasse. Dann
implementieren wir ein paar Tiere für den Anfang und alle sind glücklich. Einige
Monate später möchte das Museum auf allgemeine Nachfrage hin plötzlich noch ein
zusätzliches Tier integrieren (Wale sind ja soooo cool.). Wir leiten die Klasse
also neu ab, implementieren die Methode für den Wal et voilà: ohne irgendwas am
restlichen Code zu ändern kann unser Wal mit integriert werden.
Das Konzept der Schnittstellenfestlegung mit abstrakten Klassen taucht im objektorientierten Entwurf mit C++ quasi ständig auf. Besonders mächtig wird es (wenn man's richtig anstellt) in Zusammenhang mit Bibliotheken: vorkompilierter Code, in den der Nutzer eigene Objekte einbringen kann, von denen die Bibliothek beim Kompilieren noch nichts wusste.
Eine pure virtual Funktion muss im übrigen nicht zwingend direkt in der Klasse stehen. Sie kann auch aus einer Basisklasse geerbt sein. Wenn wir im Beispiel oben in der Klasse Hund die Methode gibLaut() nicht implementiert hätten, dann wäre auch diese Klasse durch die geerbte pure virtual Methode abstrakt und der Compiler würde uns auf die Finger hauen beim Versuch, diese zu instanziieren. Diese Eigenschaft kann man ausnutzen um allgemeine Interfaces zu verfeinern und evtl. bestimmte Methoden bereits zu implementieren, ohne dabei aber schon zu einem konkreten Objekt zu kommen (so könnten wir bspw. in unserem Museumsbeispiel in der Klasse Tier eine weitere pure virtual Methode haben, die die Tiere auf dem Bildschirm in den Chor laufen/fliegen/schwimmen lässt. Mit einer abgeleiteten Klasse Vogel können wir so für alle Vögel das Fliegen implementieren, die Methode gibLaut() aber noch weglassen. So haben wir nur einmal die Implementierung für fliegen, statt das in jeder Klasse zu wiederholen, können aber trotzdem den Vogel nicht instanziieren, weil wir sein Geräusch noch nicht kennen.)