C++
Полиморфизм
Поиск…
Определение полиморфных классов
Типичным примером является абстрактный класс формы, который затем может быть выведен на квадраты, круги и другие конкретные формы.
Родительский класс:
Начнем с полиморфного класса:
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(); }
};
Как читать это определение?
Вы можете определить полиморфное поведение, введя функции-члены с ключевым словом
virtual
. Здесьget_surface()
иdescribe_object()
, очевидно, будут выполняться по-разному для квадрата, чем для круга. Когда функция вызывается на объект, функция, соответствующая реальному классу объекта, будет определена во время выполнения.Нет смысла определять
get_surface()
для абстрактной формы. Вот почему за функцией следует= 0
. Это означает, что функция является чистой виртуальной функцией .Полиморфный класс должен всегда определять виртуальный деструктор.
Вы можете определить не виртуальные функции-члены. Когда эта функция будет вызываться для объекта, функция будет выбрана в зависимости от класса, используемого во время компиляции. Здесь
get_double_surface()
определяется таким образом.Класс, содержащий хотя бы одну чистую виртуальную функцию, является абстрактным классом. Абстрактные классы не могут быть созданы. У вас могут быть только указатели или ссылки абстрактного типа класса.
Производные классы
Как только полиморфный базовый класс определен, вы можете его получить. Например:
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;
}
};
Некоторые объяснения:
- Вы можете определить или переопределить любую из виртуальных функций родительского класса. Тот факт, что функция была виртуальной в родительском классе, делает ее виртуальной в производном классе. Не нужно снова указывать компилятору ключевое слово
virtual
. Но рекомендуется добавить ключевое словоoverride
в конце объявления функции, чтобы предотвратить тонкие ошибки, вызванные незаметными изменениями в сигнатуре функции. - Если все чистые виртуальные функции родительского класса определены, вы можете создавать объекты для этого класса, иначе он также станет абстрактным классом.
- Вы не обязаны отменять все виртуальные функции. Вы можете сохранить версию родителя, если это соответствует вашим потребностям.
Пример создания экземпляра
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;
}
Безопасное понижение
Предположим, что у вас есть указатель на объект полиморфного класса:
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);
нисходящим было бы бросить из общей полиморфной Shape
до одной из ее производной и более конкретной формы, такой как Square
или Circle
.
Почему сваливать?
В большинстве случаев вам не нужно будет знать, каков реальный тип объекта, так как виртуальные функции позволяют вам манипулировать вашим объектом независимо от его типа:
std::cout << "Surface: " << ps->get_surface() << std::endl;
Если вам не нужен нисходящий, ваш дизайн будет идеальным.
Однако иногда вам может потребоваться понижение. Типичным примером является то, что вы хотите вызывать не виртуальную функцию, которая существует только для дочернего класса.
Рассмотрим, например, круги. Только круги имеют диаметр. Таким образом, класс будет определяться как:
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; }
};
Функция get_diameter()
существует только для кругов. Он не был определен для объекта Shape
:
Shape* ps = get_any_shape();
ps->get_diameter(); // OUCH !!! Compilation error
Как сбить?
Если вы точно знаете, что ps
указывает на круг, вы можете выбрать static_cast
:
std::cout << "Diameter: " << static_cast<Circle*>(ps)->get_diameter() << std::endl;
Это сделает трюк. Но это очень рискованно: если ps
появляется ничем иным, как Circle
поведение вашего кода будет неопределенным.
Поэтому вместо того, чтобы играть в русскую рулетку, вы должны безопасно использовать dynamic_cast
. Это специально для полиморфных классов:
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;
}
Обратите внимание, что dynamic_cast
невозможен для класса, который не является полиморфным. Вам понадобится хотя бы одна виртуальная функция в классе или ее родителях, чтобы иметь возможность использовать ее.
Полиморфизм и деструкторы
Если класс предназначен для использования полиморфно, при этом производные экземпляры хранятся в качестве базовых указателей / ссылок, его деструктор его базового класса должен быть либо virtual
либо protected
. В первом случае это приведет к уничтожению объекта, чтобы проверить vtable
, автоматически вызывая правильный деструктор на основе динамического типа. В последнем случае уничтожение объекта с помощью указателя / ссылки базового класса отключено, и объект может быть удален только при явном трактовке его фактического типа.
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.
Обе эти практики гарантируют, что деструктор производного класса всегда будет вызываться на экземплярах производного класса, предотвращая утечку памяти.