C++
Wielopostaciowość
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
. Tutajget_surface()
idescribe_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 dodanieoverride
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.