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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#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.)