C++
polymorfisme
Zoeken…
Definieer polymorfe klassen
Het typische voorbeeld is een abstracte vormklasse, die vervolgens kan worden afgeleid in vierkanten, cirkels en andere concrete vormen.
De ouderklasse:
Laten we beginnen met de polymorfe 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(); }
};
Hoe deze definitie te lezen?
U kunt polymorf gedrag definiëren door lidfuncties te introduceren met het trefwoord
virtual
. Hierget_surface()
endescribe_object()
zal uiteraard verschillend een carré dan een cirkel uitgevoerd. Wanneer de functie op een object wordt aangeroepen, wordt de functie die overeenkomt met de echte klasse van het object tijdens runtime bepaald.Het heeft geen zin om
get_surface()
te definiëren voor een abstracte vorm. Daarom wordt de functie gevolgd door= 0
. Dit betekent dat de functie pure virtuele functie is .Een polymorfe klasse moet altijd een virtuele destructor definiëren.
U kunt niet-virtuele lidfuncties definiëren. Wanneer deze functie voor een object wordt aangeroepen, wordt de functie gekozen afhankelijk van de klasse die tijdens het compileren wordt gebruikt. Hier wordt
get_double_surface()
op deze manier gedefinieerd.Een klasse die ten minste één pure virtuele functie bevat, is een abstracte klasse. Abstracte klassen kunnen niet worden gestart. U mag alleen verwijzingen of verwijzingen van een abstract type klasse hebben.
Afgeleide klassen
Zodra een polymorfe basisklasse is gedefinieerd, kunt u deze afleiden. Bijvoorbeeld:
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;
}
};
Enkele verklaringen:
- U kunt alle virtuele functies van de bovenliggende klasse definiëren of negeren. Het feit dat een functie virtueel was in de bovenliggende klasse, maakt deze virtueel in de afgeleide klasse. U hoeft de compiler het trefwoord
virtual
opnieuw te vertellen. Het is echter aan te bevelen om het trefwoord teoverride
aan het einde van de functiedeclaratie, om subtiele bugs te voorkomen die worden veroorzaakt door onopgemerkte variaties in de functiehandtekening. - Als alle pure virtuele functies van de bovenliggende klasse zijn gedefinieerd, kunt u objecten voor deze klasse instantiëren, anders wordt het ook een abstracte klasse.
- U bent niet verplicht om alle virtuele functies te overschrijven. U kunt de versie van de ouder behouden als deze bij u past.
Voorbeeld van instantiëring
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;
}
Veilige downcasting
Stel dat u een aanwijzer hebt voor een object van een polymorfe 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);
een neergang zou zijn om van een algemene polymorfe Shape
naar een van zijn afgeleide en meer specifieke vorm zoals Square
of Circle
te gieten.
Waarom neerslaan?
Meestal hoeft u niet te weten wat het echte type van het object is, omdat u met de virtuele functies uw object onafhankelijk van het type kunt manipuleren:
std::cout << "Surface: " << ps->get_surface() << std::endl;
Als je geen downcast nodig hebt, zou je ontwerp perfect zijn.
Het is echter mogelijk dat u soms moet downgraden. Een typisch voorbeeld is wanneer u een niet-virtuele functie wilt aanroepen die alleen voor de child class bestaat.
Denk bijvoorbeeld aan cirkels. Alleen cirkels hebben een diameter. Dus de klasse zou worden gedefinieerd 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; }
};
De get_diameter()
bestaat alleen voor cirkels. Het was niet gedefinieerd voor een Shape
object:
Shape* ps = get_any_shape();
ps->get_diameter(); // OUCH !!! Compilation error
Hoe te downgraden?
Als je zeker weet dat ps
naar een cirkel wijst, kun je kiezen voor een static_cast
:
std::cout << "Diameter: " << static_cast<Circle*>(ps)->get_diameter() << std::endl;
Dit is voldoende. Maar het is zeer riskant: als ps
door iets anders dan een Circle
lijkt, is het gedrag van uw code ongedefinieerd.
Dus in plaats van Russisch roulette te spelen, moet u veilig een dynamic_cast
. Dit is specifiek voor polymorfe 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;
}
Merk op dat dynamic_cast
niet mogelijk is op een klasse die niet polymorf is. Je hebt minimaal één virtuele functie in de klas of de ouders nodig om deze te kunnen gebruiken.
Polymorfisme en vernietigers
Als een klasse bedoeld is om polymorf te worden gebruikt, waarbij afgeleide instanties worden opgeslagen als basisaanwijzers / referenties, moet de destructor van de basisklasse virtual
of protected
. In het eerste geval zal dit ervoor zorgen dat objectvernietiging de vtable
en automatisch de juiste destructor vtable
op basis van het dynamische type. In het laatste geval is het vernietigen van het object via een pointer / referentie van de basisklasse uitgeschakeld en kan het object alleen worden verwijderd als het expliciet als het werkelijke type wordt behandeld.
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 methoden garanderen dat de destructor van de afgeleide klasse altijd wordt aangeroepen op afgeleide klasse-instanties, waardoor geheugenlekken worden voorkomen.