C++
Polymorphismus
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
. Hierget_surface()
unddescribe_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
einmalvirtual
werden. Aber es wird empfohlen , das Keyword hinzufügenoverride
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.