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 . Hier get_surface() en describe_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 te override 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.



Modified text is an extract of the original Stack Overflow Documentation
Licentie onder CC BY-SA 3.0
Niet aangesloten bij Stack Overflow