C++ Polymorphismus

PolymorphismusWörtlich bedeutet es mehrere Formen.

Wenn zwischen Klassen eine hierarchische Struktur besteht und die Klassen durch Vererbung miteinander verbunden sind, wird Polymorphismus verwendet.

In C++ ist Polymorphismus eines der wichtigen Merkmale der objektorientierten Programmierung.

C++-Polymorphismus ermöglicht es, Zeiger oder Referenzen auf die Basisklasse zu verwenden, um überschriebene Methoden abgeleiteter Klassen aufzurufen, sodass dieselbe Schnittstelle unterschiedliche Verhaltensweisen zeigen kann.

Polymorphismus macht Code flexibler und allgemeiner. Das Programm kann über Zeiger oder Referenzen auf die Basisklasse mit Objekten unterschiedlicher Typen arbeiten, ohne Objekttypen explizit unterscheiden zu müssen. Dadurch wird Code erweiterbarer; wenn beispielsweise eine neue Formklasse hinzugefügt wird, muss das Hauptprogramm nicht geändert werden.

Hier sind einige wichtige Punkte zum Polymorphismus:

Virtuelle Funktionen:

  • Das Deklarieren einer Funktion in der Basisklasse als virtuelle Funktion verwendet das Schlüsselwortvirtual。
  • Eine abgeleitete Klasse kann diese virtuelle Funktion überschreiben.
  • Wenn eine virtuelle Funktion aufgerufen wird, wird anhand des tatsächlichen Typs des Objekts bestimmt, welche Version der Funktion aufgerufen wird.

Dynamische Bindung:

  • Auch als späte Bindung bekannt, wird die konkrete Implementierung eines Funktionsaufrufs zur Laufzeit bestimmt.
  • Es erfordert die Verwendung eines Zeigers oder einer Referenz auf die Basisklasse, um virtuelle Funktionen aufzurufen; zur Laufzeit entscheidet der Compiler anhand des tatsächlichen Typs des Objekts, welche Funktion aufgerufen wird.

Reine virtuelle Funktionen:

  • Eine Klasse, die reine virtuelle Funktionen enthält, wird als abstrakte Klasse bezeichnet, und sie kann nicht direkt instanziiert werden.
  • Eine rein virtuelle Funktion hat keinen Funktionskörper; bei der Deklaration verwendet sie das Schlüsselwort= 0。
  • Sie zwingt abgeleitete Klassen, konkrete Implementierungen bereitzustellen.

Der Implementierungsmechanismus der Polymorphie:

  • Virtuelle Funktionstabelle (V-Table): Die C++-Laufzeit verwendet eine virtuelle Funktionstabelle, um Polymorphie zu implementieren. Jede Klasse, die virtuelle Funktionen enthält, besitzt eine virtuelle Funktionstabelle, die Zeiger auf alle virtuellen Funktionen in der Klasse speichert.
  • Virtueller Funktionszeiger (V-Ptr): Das Objekt enthält einen Zeiger auf die virtuelle Funktionstabelle der Klasse.

Die Vorteile der Verwendung von Polymorphie:

  • Code-WiederverwendungDurch Basisklassenzeiger oder Basisklassenreferenzen können Objekte unterschiedlicher Typen abgeleiteter Klassen manipuliert werden, wodurch Code-Wiederverwendung erreicht wird.
  • ErweiterbarkeitWenn eine neue abgeleitete Klasse hinzugefügt wird, muss Code, der von der Basisklasse abhängt, nicht geändert werden; man muss nur sicherstellen, dass die neue Klasse die virtuellen Funktionen korrekt überschreibt.
  • EntkopplungPolymorphie ermöglicht einen modulareren Programmentwurf und reduziert die Kopplung zwischen Klassen.

Hinweise:

  • Polymorphie tritt nur auf, wenn eine virtuelle Funktion über einen Basisklassenzeiger oder eine Basisklassenreferenz aufgerufen wird.
  • Wenn ein Objekt einer abgeleiteten Klasse direkt verwendet wird, um eine Funktion aufzurufen, dann wird die Version in der abgeleiteten Klasse aufgerufen, nicht die Version in der Basisklasse.
  • Polymorphie erfordert Laufzeit-Typinformationen (RTTI), was den Programm-Overhead erhöhen kann.

Beispiel 1

Betrachten wir die Anwendung der Polymorphie anhand eines einfachen Beispiels:

Beispiel 1

#include <iostream>
using namespace std;

// Basisklasse Animal
class Animal {
public:
    // Virtuelle Funktion sound, die eine Schnittstelle für verschiedene Tiere bereitstellt, um Geräusche zu machen
    virtual void sound() const {
        cout << "Ein Tier macht ein Geräusch" << endl;
    }
   
    // Virtueller Destruktor stellt sicher, dass Objekte abgeleiteter Klassen ordnungsgemäß zerstört werden
    virtual ~Animal() {
        cout << "Tier zerstört" << endl;
    }
};

// Abgeleitete Klasse Dog, die von Animal erbt
class Dog : public Animal {
public:
    // Die Methode sound überschreiben
    void sound() const⟦⟧ override {
        cout << "Hund bellt" << endl;
    }
   
    ~Dog() {
        cout << "Dog zerstört" << endl;
    }
};

// Abgeleitete Klasse Cat, die von Animal erbt
class Cat : public Animal {
public:
    // Die Methode sound überschreiben
    void sound() const⟦⟧ override {
        cout << "Cat miaut" << endl;
    }
   
    ~Cat() {
        cout << "Cat zerstört" << endl;
    }
};

// Polymorphismus testen
int main() {
    Animal* animalPtr;  // Zeiger auf Basisklasse

    // Dog-Objekt erstellen und mit einem Animal-Zeiger darauf zeigen
    animalPtr = new Dog();
    animalPtr->sound();  // sound-Methode von Dog aufrufen
    delete animalPtr;    // Speicher freigeben, Destruktoren von Dog und Animal aufrufen

    // Cat-Objekt erstellen und mit einem Animal-Zeiger darauf zeigen
    animalPtr = new Cat();
    animalPtr->sound();  // sound-Methode von Cat aufrufen
    delete animalPtr;    // Speicher freigeben, Destruktoren von Cat und Animal aufrufen

    return 0;
}

Die Ausgabe der Programmausführung ist:

Dog barks
Dog destroyed
Animal destroyed
Cat meows
Cat destroyed
Animal destroyed

Code-Erklärung

BasisklasseAnimal:

  • AnimalDie Klasse definiert eine virtuelle Funktionsound(), die eine virtuelle Funktion ist (virtual), die verwendet wird, um das Verhalten von Tieren, die Geräusche machen, darzustellen.
  • ~Animal()Ein virtueller Destruktor stellt sicher, dass beim Freigeben eines Objekts einer abgeleiteten Klasse, auf das ein Basisklassenzeiger zeigt, der Destruktor der abgeleiteten Klasse korrekt aufgerufen wird, wodurch Speicherlecks vermieden werden.

abgeleitete KlasseDogundCat:

  • DogundCatAlle Klassen erben vonAnimalKlassen leiten sich davon ab, und jede implementiertsound()Methode.
  • Dogvonsound()Ausgabe "Dog barks";Catvonsound()Gibt "Cat meows" aus. Dadurch zeigt dieselbe Methode (sound()) in verschiedenen Klassen unterschiedliche Verhaltensweisen.

main-Funktionmain():

  • Erstelle einen Zeiger auf eine BasisklasseanimalPtr。
  • Nutzungnew Dog()ErstelleDogObjekt, weisen Sie seine Adresse zuanimalPtr. Zu diesem Zeitpunkt der AufrufanimalPtr->sound()Es wird "Dog barks" ausgeben, weilanimalPtrzeigt tatsächlich aufDogObjekt.
  • releaseDogwenn das Objekt, zuerst aufrufenDogDestruktor, dann aufrufenAnimalden Destruktor von.
  • Nutzungnew Cat()ErstelleCatObjekt und zuweisen ananimalPtr, dann aufrufenanimalPtr->sound(), gibt "Cat meows" aus, was polymorphes Verhalten zeigt.

Schlüsselkonzepte

  • virtuelle Funktion: Durch die Verwendung in der BasisklassevirtualSchlüsselwort, um virtuelle Funktionen zu deklarieren, können abgeleitete Klassen diese Funktion überschreiben, sodass zur Laufzeit die richtige Funktion basierend auf dem Objekttyp aufgerufen wird.

  • dynamische Bindung: C++-Polymorphismus wird durch dynamische Bindung implementiert. Zur Laufzeit wird der BasisklassenzeigeranimalPtrabhängig vom tatsächlichen Typ des Objekts, auf das er zeigt (DogoderCat) die entsprechende aufrufensound()Methode.

  • virtueller DestruktorIn einer Basisklasse mit polymorphem Verhalten sollte der Destruktor deklariert werden alsvirtual, um sicherzustellen, dass beim Löschen eines Objekts einer abgeleiteten Klasse der Destruktor der abgeleiteten Klasse aufgerufen wird, wodurch Ressourcenlecks vermieden werden.


Beispiel 2

Im folgenden Beispiel implementieren wir eine gemeinsame Basisklasse Shape und zwei abgeleitete Klassen, Rectangle und Triangle, mittels Polymorphie.

Das Aufrufen verschiedener Methoden abgeleiteter Klassen über einen Basisklassenzeiger demonstriert das Merkmal der dynamischen Bindung von Polymorphie.

Beispiel 2

#include <iostream> using namespace std; // Basisklasse Shape, stellt eine Form dar class Shape { protected: int width, height; // Breite und Höhe public: // Konstruktor mit Standardparametern Shape(int a = 0, int b = 0) : width(a), height(b) { } // Virtuelle Funktion area, dient zur Berechnung der Fläche // Verwende das Schlüsselwort virtual, um Polymorphie zu implementieren virtual int area() { cout << "Fläche der Shape-Klasse: " << endl; return 0; } }; // Abgeleitete Klasse Rectangle, stellt ein Rechteck dar class Rectangle : public Shape { public: // Konstruktor, verwendet den Konstruktor der Basisklasse, um Breite und Höhe zu initialisieren Rectangle(int a = 0, int b = 0) : Shape(a, b) { } // Überschreibe die area-Funktion, um die Rechteckfläche zu berechnen int area() override { cout << "Fläche der Rectangle-Klasse: " << endl; return width * height; } }; // Abgeleitete Klasse Triangle, repräsentiert ein Dreieck class Triangle : public Shape { public: // Konstruktor, verwendet den Konstruktor der Basisklasse, um width und height zu initialisieren Triangle(int a = 0, int b = 0) : Shape(a, b) { } // Überschreibe die area-Funktion, um die Dreiecksfläche zu berechnen int area() override { cout << "Fläche der Triangle-Klasse: " << endl; return (width * height / 2); } }; // Hauptfunktion int main() { Shape *shape; // Basisklassenzeiger Rectangle rec(10, 7); // Rechteck-Objekt Triangle tri(10, 5); // Dreieck-Objekt // Den Basisklassenzeiger auf das Rechteck-Objekt zeigen lassen und die area-Funktion aufrufen shape = &rec; cout << "Rechteckfläche: " << shape->area() << endl; // Den Basisklassenzeiger auf das Dreieck-Objekt zeigen lassen und die area-Funktion aufrufen shape = &tri; cout << "Dreieckfläche: " << shape->area() << endl; return 0; }

Wenn der obige Code kompiliert und ausgeführt wird, liefert er die folgenden Ergebnisse:

Rectangle Area: Rectangle class area: 
70
Triangle Area: Triangle class area: 
25

Codeanalyse

Definition der Klasse Shape:

  • Shapeist eine abstrakte Basisklasse, die eine virtuelle Funktion definiertarea()。area()ist eine virtuelle Funktion zur Berechnung der Fläche und verwendet dasvirtualSchlüsselwort, damit in abgeleiteten Klassen die Funktion überschrieben werden kann, wodurch Polymorphie erreicht wird.
  • widthundheightJaprotectedEigenschaft, kann nurShapein der Klasse und ihren abgeleiteten Klassen zugegriffen werden.
// 基类 Shape,表示形状
class Shape {
   protected:
      int width, height; // 宽度和高度

   public:
      // 构造函数,带有默认参数
      Shape(int a = 0, int b = 0) : width(a), height(b) { }

      // 虚函数 area,用于计算面积
      virtual int area() {
         cout << "Shape class area: " << endl;
         return 0;
      }
};

Definition der Rectangle-Klasse:

  • RectangleerbtShapeKlasse und überschriebarea()Methode berechnet die Fläche des Rechtecks.
  • area()Die Methode verwendetoverrideSchlüsselwort, das angibt, dass dies eine Überschreibung der Basisklasse istShapevonarea()Methodenüberschreibung.
  • Rectangle::area()returnwidth * height, d. h. die Fläche des Rechtecks.
// 派生类 Rectangle,表示矩形
class Rectangle : public Shape {
   public:
      // 构造函数,使用基类构造函数初始化 width 和 height
      Rectangle(int a = 0, int b = 0) : Shape(a, b) { }

      // 重写 area 函数,计算矩形面积
      int area() override { 
         cout << "Rectangle class area: " << endl;
         return width * height;
      }
};

Definition der Triangle-Klasse:

  • Triangledie Klasse erbt auch vonShape, und überschreibtarea()Methode, die zur Berechnung der Fläche des Rechtecks verwendet wird.
  • Triangle::area()returnwidth * height / 2, dies ist die Formel für die Fläche eines Dreiecks.
// 派生类 Triangle,表示三角形
class Triangle : public Shape {
   public:
      // 构造函数,使用基类构造函数初始化 width 和 height
      Triangle(int a = 0, int b = 0) : Shape(a, b) { }

      // 重写 area 函数,计算三角形面积
      int area() override { 
         cout << "Triangle class area: " << endl;
         return (width * height / 2); 
      }
};

Polymorphes Verhalten in der main-Funktion:

  • Ein Zeiger auf die Basisklasse wird definiertshape, dieser Zeiger kann auf jedesShapeKlassenobjekt oder ein Objekt seiner abgeleiteten Klassen zeigen.
  • Zuerst richten Sieshapeden Zeiger aufRectangleObjektrec, dann aufrufenshape->area(). Daarea()eine virtuelle Funktion ist, und an dieser Stelle wird er dynamisch anRectangle::area()gebunden, wodurch die Fläche des Rechtecks ausgegeben wird.
  • Dann richten Sieshapeden Zeiger aufTriangleObjekttri, rufen Sieshape->area()wird dynamisch gebunden anTriangle::area(), gibt die Fläche des Dreiecks aus.
// 主函数
int main() {
   Shape *shape;           // 基类指针
   Rectangle rec(10, 7);   // 矩形对象
   Triangle tri(10, 5);    // 三角形对象

   // 将基类指针指向矩形对象,并调用 area 函数
   shape = &rec;
   cout << "Rectangle Area: " << shape->area() << endl;

   // 将基类指针指向三角形对象,并调用 area 函数
   shape = &tri;
   cout << "Triangle Area: " << shape->area() << endl;

   return 0;
}

Schlüsselkonzepte

  • virtuelle Funktion: in der BasisklasseShapevirtuelle Funktionen werden definiert inarea(). Die Rolle einer virtuellen Funktion besteht darin, abgeleiteten Klassen zu ermöglichen, diese Funktion zu überschreiben und zur Laufzeit die passende Funktionsimplementierung basierend auf dem tatsächlichen Objekttyp, auf den der Zeiger zeigt, aufzurufen.

  • dynamische Bindung: weilarea()ist eine virtuelle Funktion,shape->area()beim Aufruf, zur Laufzeit, basierend aufshapetatsächlichen Typ des Objekts, auf das er zeigt (RectangleoderTriangle), um die entsprechendearea()Implementierung aufzurufen. Dieser Mechanismus, bei dem zur Laufzeit entschieden wird, welche Funktion aufgerufen wird, heißt dynamische Bindung und ist der Kern der Polymorphie.

  • Polymorphie von Basisklassenzeigern: der Basisklassenzeigershapekann auf jedes Objekt zeigen, das abgeleitet ist vonShapedes Objekts. Wennshapebeim Zeigen auf unterschiedliche Objekte abgeleiteter Klassen, das Aufrufenshape->area()wird unterschiedliche Verhaltensweisen erzeugen, was die Eigenschaften der Polymorphie widerspiegelt.

  • virtuelle Funktion

    Virtuelle Funktionbedeutet, in der Basisklasse das Schlüsselwortvirtualzur Deklaration der Funktion zu verwenden.

    Virtuelle Funktionermöglicht es Unterklassen, sie zu überschreiben, sodass zur Laufzeit die überschriebene Version der Unterklasse über einen Zeiger oder eine Referenz auf die Basisklasse aufgerufen wird, wodurch dynamische Bindung umgesetzt wird.

    Was wir wollen, ist, dass an jedem Punkt im Programm die aufzurufende Funktion entsprechend dem Typ des aufgerufenen Objekts ausgewählt werden kann. Diese Operation wirddynamische Bindungoderspäte Bindung genannt.

    Funktionen:

    • kann eine Implementierung in der Basisklasse haben. Normalerweise bieten virtuelle Funktionen eine Standardimplementierung in der Basisklasse, aber abgeleitete Klassen können wählen, sie zu überschreiben.
    • dynamische Bindung: zur Laufzeit die entsprechende Funktionsversion basierend auf dem tatsächlichen Typ des Objekts aufrufen.
    • Optionales ÜberschreibenAbgeleitete Klassen können virtuelle Funktionen optional überschreiben, aber es ist nicht zwingend erforderlich.

    Beispiel

    #include <iostream>
    using namespace std;

    class Animal {
    public:
        virtual void sound() {  // Virtuelle Funktion
            cout << "Ein Tier macht ein Geräusch" << endl;
        }
    };

    class Dog : public Animal {
    public:
        void sound() override {  // Virtuelle Funktion überschreiben
            cout << "Hund bellt" << endl;
        }
    };

    int main() {
        Animal *animal = new Dog();
        animal->sound();  // Ausgabe: Dog barks
        delete animal;
    }

    Im obigen Code ist sound eine virtuelle Funktion der Klasse Animal. Wenn sound() über den Animal*-Zeiger animal aufgerufen wird, wählt das Programm Dog::sound() basierend auf dem tatsächlichen Objekttyp (Dog) aus.

    Rein virtuelle Funktion

    Möglicherweise möchten Sie eine virtuelle Funktion in der Basisklasse definieren, damit Sie die Funktion in einer abgeleiteten Klasse neu definieren können, um besser zum Objekt zu passen, aber Sie können keine sinnvolle Implementierung für die virtuelle Funktion in der Basisklasse bereitstellen. In diesem Fall wird eine rein virtuelle Funktion verwendet.

    Eine rein virtuelle Funktion ist eine virtuelle Funktion ohne Implementierung; in der Basisklasse wird sie mit= 0deklariert.

    Eine rein virtuelle Funktion bedeutet, dass die Basisklasse eine Schnittstelle definiert, aber die spezifische Implementierung liegt in der Verantwortung der abgeleiteten Klasse.

    Rein virtuelle Funktionen machen die Basisklasse zu einer abstrakten Klasse, die nicht instanziiert werden kann.

    Funktionen:

    • muss in der Basisklasse deklariert werden als= 0, was anzeigt, dass es keine Implementierung gibt, und abgeleitete Klassen müssen sie überschreiben.
    • Abstrakte Klasse: Eine Klasse, die reine virtuelle Funktionen enthält, kann nicht direkt instanziiert werden; ein Objekt kann nur erstellt werden, wenn alle reinen virtuellen Funktionen von einer abgeleiteten Klasse implementiert werden.
    • SchnittstellendefinitionReine virtuelle Funktionen werden normalerweise verwendet, um Schnittstellen zu definieren, sodass abgeleitete Klassen spezifische Verhaltensweisen implementieren können.

    Wir können die virtuelle Funktion area() in der Basisklasse wie folgt umschreiben:

    #include <iostream> using namespace std; class Shape { public: virtual int area() = 0; // Reine virtuelle Funktion, zwingt Unterklassen, diese Methode zu implementieren }; class Rectangle : public Shape { private: int width, height; public: Rectangle(int w, int h) : width(w), height(h) { } int area() override { // Die reine virtuelle Funktion implementieren return width * height; } }; int main() { Shape *shape = new Rectangle(10, 5); cout << "Rechteckfläche: " << shape->area() << endl; // Ausgabe: Rechteckfläche: 1 delete shape; }

    = 0teilt dem Compiler mit, dass die Funktion keinen Rumpf hat; die obige virtuelle Funktion istReine virtuelle Funktion。

    Vergleich von virtuellen Funktionen und reinen virtuellen Funktionen

    FunktionenVirtuellReine virtuelle Funktion
    DefinitionWird in der Basisklasse verwendetvirtualDeklaration, mit ImplementierungWird in der Basisklasse verwendet= 0Deklaration, ohne Implementierung
    Wird in der abgeleiteten Klasse überschriebenUnterklassen können überschreibenDie Unterklasse muss implementieren
    Abstraktheitkann die Klasse instanziierenmacht die Klasse zu einer abstrakten Klasse, die nicht instanziiert werden kann
    Zweckbietet Standardverhalten, das Unterklassen überschreiben könnendefiniert eine Schnittstelle und zwingt abgeleitete Klassen, bestimmte Verhaltensweisen zu implementieren
    Weitere Erweiterungen