Suche…


Definieren Sie polymorphe Klassen

Das typische Beispiel ist eine abstrakte Formklasse, die dann in Quadrate, Kreise und andere konkrete Formen abgeleitet werden kann.

Die übergeordnete Klasse:

Beginnen wir mit der polymorphen Klasse:

class Shape {
public:
    virtual ~Shape() = default;
    virtual double get_surface() const = 0;
    virtual void describe_object() const { std::cout << "this is a shape" << std::endl; }  

    double get_doubled_surface() const { return 2 * get_surface(); } 
};

Wie kann man diese Definition lesen?

  • Sie können polymorphes Verhalten durch eingeführte Elementfunktionen mit dem Schlüsselwort virtual . Hier get_surface() und describe_object() wird offensichtlich für ein Quadrat unterschiedlich implementiert werden , als für einen Kreis. Wenn die Funktion für ein Objekt aufgerufen wird, wird zur Laufzeit eine Funktion bestimmt, die der realen Klasse des Objekts entspricht.

  • Es macht keinen Sinn, get_surface() für eine abstrakte Form zu definieren. Deshalb folgt auf die Funktion = 0 . Dies bedeutet, dass die Funktion eine rein virtuelle Funktion ist .

  • Eine polymorphe Klasse sollte immer einen virtuellen Destruktor definieren.

  • Sie können nicht virtuelle Elementfunktionen definieren. Wenn diese Funktion für ein Objekt aufgerufen wird, wird die Funktion in Abhängigkeit von der zur Kompilierzeit verwendeten Klasse ausgewählt. Hier ist get_double_surface() so definiert.

  • Eine Klasse, die mindestens eine rein virtuelle Funktion enthält, ist eine abstrakte Klasse. Abstrakte Klassen können nicht instanziiert werden. Sie können nur Zeiger oder Referenzen eines abstrakten Klassentyps haben.

Abgeleitete Klassen

Sobald eine polymorphe Basisklasse definiert ist, können Sie sie ableiten. Zum Beispiel:

class Square : public Shape {
    Point top_left;
    double side_length;
public: 
    Square (const Point& top_left, double side)
       : top_left(top_left), side_length(side_length) {}

    double get_surface() override { return side_length * side_length; }   
    void describe_object() override { 
        std::cout << "this is a square starting at " << top_left.x << ", " << top_left.y
                  << " with a length of " << side_length << std::endl; 
    }  
};

Einige Erklärungen:

  • Sie können alle virtuellen Funktionen der übergeordneten Klasse definieren oder überschreiben. Die Tatsache, dass eine Funktion in der übergeordneten Klasse virtuell war, macht sie in der abgeleiteten Klasse virtuell. Dem Compiler muss das Schlüsselwort virtual einmal virtual werden. Aber es wird empfohlen , das Keyword hinzufügen override am Ende der Funktionsdeklaration, um geringfügige Fehler unbemerkt Variationen in der Funktionssignatur zu verhindern.
  • Wenn alle reinen virtuellen Funktionen der übergeordneten Klasse definiert sind, können Sie Objekte für diese Klasse instanziieren, andernfalls wird sie auch zu einer abstrakten Klasse.
  • Sie sind nicht verpflichtet, alle virtuellen Funktionen zu überschreiben. Sie können die Version des übergeordneten Elements beibehalten, wenn es Ihren Bedürfnissen entspricht.

Beispiel für eine Instantiierung

int main() {

    Square square(Point(10.0, 0.0), 6); // we know it's a square, the compiler also
    square.describe_object(); 
    std::cout << "Surface: " << square.get_surface() << std::endl; 

    Circle circle(Point(0.0, 0.0), 5);

    Shape *ps = nullptr;  // we don't know yet the real type of the object
    ps = &circle;         // it's a circle, but it could as well be a square
    ps->describe_object(); 
    std::cout << "Surface: " << ps->get_surface() << std::endl;
}

Sicheres Downcasting

Angenommen, Sie haben einen Zeiger auf ein Objekt einer polymorphen Klasse:

Shape *ps;                       // see example on defining a polymorphic class
ps =  get_a_new_random_shape();  // if you don't have such a function yet, you 
                                 // could just write ps = new Square(0.0,0.0, 5);

Ein Downcast wäre, von einer allgemeinen polymorphen Shape auf eine abgeleitete und spezifischere Form wie Square oder Circle .

Warum niedergeschlagen?

In den meisten Fällen müssen Sie nicht wissen, um welchen realen Objekttyp es sich handelt, da Sie mit den virtuellen Funktionen Ihr Objekt unabhängig von seinem Objekttyp bearbeiten können:

std::cout << "Surface: " << ps->get_surface() << std::endl; 

Wenn Sie keinen Downcast benötigen, wäre Ihr Design perfekt.

Möglicherweise müssen Sie jedoch manchmal einen Downcast durchführen. Ein typisches Beispiel ist, wenn Sie eine nicht virtuelle Funktion aufrufen möchten, die nur für die untergeordnete Klasse vorhanden ist.

Betrachten Sie zum Beispiel Kreise. Nur Kreise haben einen Durchmesser. Die Klasse wäre also definiert als:

class Circle: public Shape { // for Shape, see example on defining a polymorphic class
    Point center;
    double radius;
public: 
    Circle (const Point& center, double radius)
       : center(center), radius(radius) {}

    double get_surface() const override { return r * r * M_PI; }   

    // this is only for circles. Makes no sense for other shapes 
    double get_diameter() const { return 2 * r; }
};

Die Member-Funktion get_diameter() existiert nur für Kreise. Es wurde nicht für ein Shape Objekt definiert:

Shape* ps = get_any_shape();
ps->get_diameter(); // OUCH !!! Compilation error 

Wie niederzuschlagen?

Wenn Sie sicher wissen, dass ps auf einen Kreis zeigt, können Sie sich für einen static_cast :

std::cout << "Diameter: " << static_cast<Circle*>(ps)->get_diameter() << std::endl;

Das wird den Trick tun. Aber es ist sehr riskant: Wenn ps durch etwas anderes als einen Circle das Verhalten Ihres Codes undefiniert.

Anstatt russisches Roulette zu spielen, sollten Sie also einen dynamic_cast . Dies gilt speziell für polymorphe Klassen:

int main() {
    Circle circle(Point(0.0, 0.0), 10);
    Shape &shape = circle;

    std::cout << "The shape has a surface of " << shape.get_surface() << std::endl;

    //shape.get_diameter();   // OUCH !!! Compilation error 

    Circle *pc = dynamic_cast<Circle*>(&shape); // will be nullptr if ps wasn't a circle 
    if (pc) 
        std::cout << "The shape is a circle of diameter " << pc->get_diameter() << std::endl;
    else
        std::cout << "The shape isn't a circle !" << std::endl; 
}        

Beachten Sie, dass dynamic_cast für eine Klasse nicht möglich ist, die nicht polymorph ist. Sie benötigen mindestens eine virtuelle Funktion in der Klasse oder deren Eltern, um sie verwenden zu können.

Polymorphismus und Destruktoren

Wenn eine Klasse polymorph verwendet werden soll und abgeleitete Instanzen als Basiszeiger / -referenzen gespeichert werden sollen, sollte der Destruktor der Basisklasse entweder virtual oder protected . Im ersten Fall führt dies zur Zerstörung des vtable , um die vtable zu überprüfen, wobei automatisch der korrekte Destruktor basierend auf dem dynamischen Typ vtable wird. Im letzteren Fall ist das Zerstören des Objekts durch einen Basisklassenzeiger / -referenz deaktiviert, und das Objekt kann nur gelöscht werden, wenn es explizit als tatsächlicher Typ behandelt wird.

struct VirtualDestructor {
    virtual ~VirtualDestructor() = default;
};

struct VirtualDerived : VirtualDestructor {};

struct ProtectedDestructor {
  protected:
    ~ProtectedDestructor() = default;
};

struct ProtectedDerived : ProtectedDestructor {
    ~ProtectedDerived() = default;
};

// ...

VirtualDestructor* vd = new VirtualDerived;
delete vd; // Looks up VirtualDestructor::~VirtualDestructor() in vtable, sees it's
           // VirtualDerived::~VirtualDerived(), calls that.

ProtectedDestructor* pd = new ProtectedDerived;
delete pd; // Error: ProtectedDestructor::~ProtectedDestructor() is protected.
delete static_cast<ProtectedDerived*>(pd); // Good.

Beide Vorgehensweisen garantieren, dass der Destruktor der abgeleiteten Klasse immer für abgeleitete Klasseninstanzen aufgerufen wird, um Speicherverluste zu vermeiden.



Modified text is an extract of the original Stack Overflow Documentation
Lizenziert unter CC BY-SA 3.0
Nicht angeschlossen an Stack Overflow