Polymorphie

Polymorphie bezeichnet die Vielgestaltigkeit eines Objektes, von dem andere abgeleitet wurden. Insbesondere bedeutet es, dass derselbe Methodenaufruf verschiedene Auswirkungen haben kann. Erweitern wir also das vorherige Beispiel um Polymorphie:

// Klasse für Obst
class CFruit
{
  public: // folgende Elemente sind öffentlich

    // Konstruktor: Standardfarbe schwarz
    CFruit()
    { m_r = m_g = m_b = 0.0f; }

    // Destruktor vituell, d.h. alles klar zum Ableiten
    virtual ~CFruit()
    { }

    // Methode verändert keine Werte, darum const
    void printColor() const
    { cout << m_r << " " << m_g << " " << m_b << endl; }

    // virtuell - kann, muss aber nicht überladen werden
    virtual void printType() const
    { cout << "This is a Fruit" << endl; }

    // = 0: rein virtuell - muss überladen werden
    virtual void printName() const = 0;

  protected: // folgende Elemente sind geschützt

    float m_r, m_g, m_b; // Farbwerte
};

Diese Klasse funktioniert nicht für sich alleine, da sie eine rein virtuelle Methode printName enthält. Virtuelle Methoden sind das C++-Konstrukt, das Polymorphie ermöglicht.

Rein virtuelle Methoden wie printName werden druch ein =0 hinter der Mehodendeklaration gekennzeichnet. Sie werden in der Oberklasse nur deklariert, besitzen dort aber keine Implementierung. Sie zu implementieren ist allerdings Pflicht für alle Klassen, die von der Klasse erben und die als konkrete Objekte erzeugt werden sollen (siehe auch selbständige Programmierung).

Die Methode printType ist "nur" virtuell. Sie besitzt eine Implentierung, die allerdings in einer Unterklasse überladen (also überschrieben) werden kann. Sehen wir uns dazu das Beispiel weiter unten an

Banane

// Klasse für Bananen
class CBanana : public CFruit
{
  public:

    CBanana()
    { m_r = 1.0f; m_g = 1.0f; m_b = 0.0f; }

    virtual ~CBanana()
    { }

    // Methode wird anstelle derjenigen
    // in CFruit aufgerufen
    virtual void printType() const
    { cout << "This is a very special Fruit" << endl; }

    // Methode implementiert printName() aus CFruit
    virtual void printName() const
    { cout << "banana" << endl; }
};

Jedes Mal, wenn jetzt die printType- oder printName-Methoden für eine Frucht aufrufen wird, die zufällig eine Banane ist, verzweigt unser Programm in die Unterroutinen, die wir für die Banane implementiert haben.

Apfel

// Klasse für Äpfel
class CApple : public CFruit
{
  public:

    CApple()
    { m_r = 0.0f; m_g = 1.0f; m_b = 0.0f; m_worm = true;}

    virtual ~CApple()
    { }

    // Methode implementiert printName() aus CFruit
    virtual void printName() const
    {
        if (m_worm)
            cout << "apple with worm" << endl;
        else
            cout << "apple" << endl;
    }

  protected:

    bool m_worm; // ist Apfel von Wurm befallen?
};

Der Apfel bleibt bis auf die Implementierung der printName Methode gleich, d.h. er übernimmt die printType-Methode von CFruit. Das ist sehr praktisch, weil wir die Methode hier nicht noch einmal aufführen müssen.

Zusätzlich besitzt der Apfel noch ein Attribut m_worm, das angibt, ob er von einem Wurm befallen ist. Das Deklarieren von zusätzlichen Attributen oder Methoden in Unterklassen ist also durchaus erlaubt.

In diesem Beispiel gibt es jetzt die Funktion printAll, die in der Lage ist, alle Daten einer beliebigen Frucht auszugeben.

void printAll(const CFruit& fruit)
{
    fruit.printType();
    fruit.printName();
    fruit.printColor();
}

Warum übergeben wir fruit hier als const reference? Dazu ein kleiner Exkurs.

Wenn wir Parameter übergeben, dann entweder als Wert (by value) oder als Referenz bzw. Pointer (by reference).

Wenn wir nur eine Referenz übergeben, ist die Welt in Ordnung, da wir im Grunde nur eine Speicheradresse unseres Objektes weitergeben. Schreiben wir noch ein const vor die Referenz, stellen wir sicher, dass die Unterfunktion das Objekt auch nicht verändern kann. So weit, so gut.

Übergeben wir ein Objekt als Wert, muss der Compiler irgendwo eine Kopie des Objekts anfertigen, während das Programm läuft. Erinnert euch daran, dass wir mit lokalen Variablen (als Wert übergeben) arbeiten können, ohne dass sich die Variable, die ursprünglich übergeben wurde, ändert.

Das funktionierte bisher wunderbar. CFruit aber kann nicht instanziert werden kann, da es eine rein virtuelle Funktion enthält. Wir können weder ein Objekt vom Typen CFruit erzeugen, noch eine Kopie. Darum würde das hier schiefgehen

// GEHT SCHIEF
void printAll(CFruit fruit)
{
    fruit.printType();
}

Ich habe mir angewähnt, komplexere Objekte als const reference zu übergeben, wenn ich in einer Funktion oder Methode nichts an ihnen ändere.

Möchte ich ein Objekt in einer Funktion verändern, übergebe ich es als Pointer. Auf diese Weise wird im Programmtext schon beim Aufruf durch das auftauchende & klar, dass hier etwas verändert wird.

Selbständige Programmierung
  • Ein Objekt, das irgendwo noch eine rein virtuelle Funktion besitzt, kann nicht instanziiert werden. Versuche es, indem du die printName-Methode aus CApple temporär auskommentierst und das Projekt kompilierst.
  • Gebe in Konstruktor und Destruktor der Klassen einen Text aus. Was passiert beim Aufruf der Funktion printColor nicht, im Vergleich zum vorherigen Beispiel?
  • Implementiere die Methode printAll als Bestandteil von CFruit.
  • Was passiert, wenn der Destruktor von CFruit versucht, die Methode printName aufzurufen? Warum? Kann man sie im Destruktor von CApple aufrufen?

zurück