Szukaj…


Zdefiniuj klasy polimorficzne

Typowym przykładem jest abstrakcyjna klasa kształtów, którą można następnie wyprowadzić na kwadraty, koła i inne konkretne kształty.

Klasa nadrzędna:

Zacznijmy od klasy polimorficznej:

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(); } 
};

Jak czytać tę definicję?

  • Możesz zdefiniować zachowanie polimorficzne poprzez wprowadzenie funkcji składowych ze słowem kluczowym virtual . Tutaj get_surface() i describe_object() będzie oczywiście być realizowane inaczej niż na placu na kole. Gdy funkcja zostanie wywołana na obiekcie, funkcja odpowiadająca rzeczywistej klasie obiektu zostanie określona w czasie wykonywania.

  • Nie ma sensu definiować get_surface() dla abstrakcyjnego kształtu. Dlatego po funkcji następuje = 0 . Oznacza to, że funkcja jest czystą funkcją wirtualną .

  • Klasa polimorficzna powinna zawsze definiować wirtualny destruktor.

  • Możesz zdefiniować nie wirtualne funkcje składowe. Gdy te funkcje zostaną wywołane dla obiektu, funkcja zostanie wybrana w zależności od klasy użytej w czasie kompilacji. Tutaj get_double_surface() jest zdefiniowane w ten sposób.

  • Klasa zawierająca co najmniej jedną czystą funkcję wirtualną jest klasą abstrakcyjną. Klasy abstrakcyjne nie mogą być tworzone. Możesz mieć tylko wskaźniki lub referencje abstrakcyjnego typu klasy.

Wyprowadzone klasy

Po zdefiniowaniu polimorficznej klasy bazowej możesz ją uzyskać. Na przykład:

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; 
    }  
};

Niektóre wyjaśnienia:

  • Możesz zdefiniować lub zastąpić dowolną funkcję wirtualną klasy nadrzędnej. Fakt, że funkcja była wirtualna w klasie nadrzędnej, czyni ją wirtualną w klasie pochodnej. Nie trzeba ponownie informować kompilatora o słowie virtual . Zaleca się jednak dodanie override słowa kluczowego na końcu deklaracji funkcji, aby uniknąć subtelnych błędów spowodowanych niezauważalnymi zmianami w sygnaturze funkcji.
  • Jeśli zdefiniowane są wszystkie funkcje wirtualne klasy nadrzędnej, można utworzyć instancję obiektów dla tej klasy, w przeciwnym razie stanie się ona również klasą abstrakcyjną.
  • Nie musisz zastępować wszystkich funkcji wirtualnych. Możesz zachować wersję rodzica, jeśli odpowiada ona twoim potrzebom.

Przykład wystąpienia

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;
}

Bezpieczne downcasting

Załóżmy, że masz wskaźnik do obiektu klasy polimorficznej:

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);

spuszczony byłby rzut z ogólnego Shape polimorficznego na jeden z jego pochodnych i bardziej specyficznych kształtów, takich jak Square lub Circle .

Dlaczego spuszczać?

Przez większość czasu nie trzeba wiedzieć, jaki jest prawdziwy typ obiektu, ponieważ funkcje wirtualne pozwalają manipulować obiektem niezależnie od jego typu:

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

Jeśli nie potrzebujesz spuszczonego materiału, Twój projekt byłby idealny.

Czasami jednak może być konieczne obniżenie. Typowym przykładem jest, gdy chcesz wywołać funkcję nie wirtualną, która istnieje tylko dla klasy potomnej.

Rozważmy na przykład kręgi. Tylko koła mają średnicę. Tak więc klasa byłaby zdefiniowana jako:

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; }
};

Funkcja get_diameter() istnieje tylko dla kręgów. Nie został zdefiniowany dla obiektu Shape :

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

Jak spuszczać?

Jeśli na pewno wiesz, że ps wskazuje na okrąg, możesz wybrać static_cast :

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

To załatwi sprawę. Ale jest to bardzo ryzykowne: jeśli ps wydaje się być czymkolwiek innym niż Circle zachowanie twojego kodu będzie niezdefiniowane.

Dlatego zamiast grać w rosyjską ruletkę, powinieneś bezpiecznie korzystać z dynamic_cast . Dotyczy to w szczególności klas polimorficznych:

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; 
}        

Zauważ, że dynamic_cast nie jest możliwy w klasie, która nie jest polimorficzna. Aby móc z niego korzystać, potrzebujesz co najmniej jednej funkcji wirtualnej w klasie lub jej rodziców.

Polimorfizm i niszczyciele

Jeśli klasa ma być używana polimorficznie, a pochodne instancje są przechowywane jako podstawowe wskaźniki / referencje, to destruktor jej klasy podstawowej powinien być virtual lub protected . W pierwszym przypadku spowoduje to zniszczenie obiektu, aby sprawdzić vtable , automatycznie wywołując właściwy destruktor na podstawie typu dynamicznego. W tym drugim przypadku niszczenie obiektu przez wskaźnik / referencję klasy bazowej jest wyłączone, a obiekt można usunąć tylko wtedy, gdy jawnie traktowany jest jako jego rzeczywisty typ.

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.

Obie te praktyki gwarantują, że destruktor klasy pochodnej będzie zawsze wywoływany w instancjach klasy pochodnej, zapobiegając wyciekom pamięci.



Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow