-
Notifications
You must be signed in to change notification settings - Fork 0
CPP 08. Полиморфизм в CPP. Виртуальные методы. Чисто виртуальные методы. Виртуальные и чисто виртуальные деструкторы. Понятие абстрактного класса. Ошибки, возникающие при работе с указателем или ссылкой на базовый класс. Дружественные связи.
Полиморфизм – возможность подменять одно другим, не изменяя написанный код. Возможность обработки разных типов данных, с помощью "одной и той же" функции, или метода.
Если в классе определен хотя бы один метод с модификатором virtual, то данный класс считается полиморфом и при создании объектов этих классов в них добавляется указатель на виртуальную таблицу, то есть на те методы, которые использует класс.
Ключевое слово override дает гарантию того, что метод является полиморфом и подменяет метод базового класса.
По сути полиморфизм ~~ безразличие (не важно, кто предоставляет функционал)
Идея виртуальных методов - когда создаются объекты, создавать в памяти специальные таблицы (виртуальные), которые для данного объекта содержат адреса методов, которые надо вызывать. Соответственно объект должен иметь указатель на эту таблицу. Всегда это использовать не удобно.
Плюс:
- Лёгкость подмены одного понятия на другое
Минусы:
- Увеличивается объём памяти. (В памяти хранятся таблицы для каждого объекта).
- Увеличивается время вызова метода. (В таблице нужно найти метод, получить его адрес и по нему вызвать метод).
Полиморфный класс - класс, в котором есть хотя бы один виртуальный метод (тогда в объекты этого класса добавляется указатель на таблицу виртуальных методов).
Правило полиморфных классов:
При наследовании нельзя ни сужать, ни расширять интерфейс базового класса.
Базовый класс всегда должен быть абстрактным. Его объекты создавать нельзя. Для того, чтобы создать абстрактный класс, в нем должен быть хотя бы один чисто виртуальный метод. Он задается следующим образом:
virtual Product CreateProduct() = 0;
Абстрактный класс - класс, имеющий хотя бы один чисто виртуальный метод. Объекты абстрактного класса создавать нельзя.
Если производные классы не будут подменять чисто виртуальные методы, они будут тоже абстрактными.
Для абстрактного класса все методы, которые могут быть подменены в производном, определяем с модификатором virtual
.
Для полиморфного класса мы всегда должны определять деструктор.
-
Пример
class A { public: virtual void f() = 0; virtual ~A() = 0; };
Тут возникает проблема: объект уничтожается в обратном порядке (относительно порядка создания). Поэтому реализовать этот деструктор мы обязаны. (звучит, как костыль)
-
Пример
A::~A() = default; // хотя бы так
Правило: мы не должны вызывать виртуальные методы в конструкторах и деструкторах.
-
Пример
class A { public: virtual ~A() { cout << "Class A destructor called;" << endl; } virtual void f() { cout << "Executing f from A" << endl; } }; class B : public A { public: B() { // прикол в том, что класса C ещё нет // поэтому вызовется метод класса A this->f(); } virtual ~B() { cout<<"Class B destructor called;"<<endl; // прикол в том, что класса C уже нет // поэтому вызовется метод класса A this->f(); } void g() { this->f(); } }; class C : public B { public: virtual ~C() { cout<<"Class C destructor called;"<<endl; } virtual void f() override { cout<<"Executing f from C;"<<endl; } }; void main() { C obj; // this в методе B::g() будет = obj; // т.е отработает штатно obj.g(); }
Желательно, чтобы виртуальным был только интерфейс.
ВАЖНО! В языке С++ происходит неявное преобразование от указателя объекта производного класса к указателю на объект базового класса. То же самое касается ссылок.
-
Пример
A *p = new B; // Неявное преобразование (пример с указателями) B obj; A& alias = obj; // Неявное преобразование (пример со ссылкой) ... delete p;
Класс А – абстрактный, класс В – не абстрактный. Мы можем работать с классом В, вызывая метод.
Вступает правило: для класса А мы все методы, которые могут быть подменены в классе В, должны определить с модификатором virtual
, чтобы один объект можно было подменить другим.
Мы работаем с указателем на А. Мы подменили один объект другим, но правую часть мы вызываем конкретно. Напрашивается виртуальный конструктор, но конструктор - это не метод объекта, а метод класса, конструктор не может быть виртуальным. Получается проблема с подменой (спойлер: решается с помощью порождающих паттернов).
Возникает еще одна проблема – вызывается деструктор, а деструктор должен вызываться для объекта класса В. Но деструктор вызывается для объекта B, поэтому деструктор может быть виртуальным. Соответственно, когда мы определяем какой-либо класс, в любом случае для базового полиморфного класса мы должны определить деструктор. Если мы не можем его определить, то мы делаем деструктор чисто виртуальным.
-
Пример
class A { public: virtual void f() = 0; virtual ~A() = 0; };
Но возникает проблема – у нас создается объект какого-то производного класса, по цепочке отрабатывает конструктор, в обратном порядке отрабатывают деструкторы. А мы удалили этот деструктор! Поэтому реализовать этот деструктор мы обязаны. Реализовать как пустой.
-
Пример
class A { public: virtual void f() = 0; virtual ~A() = 0; }; A::~A() {}
Задав виртуальный деструктор, каждый производный класс определяет для себя этот деструктор.
Итоги: если у нас полиморфный базовый класс, мы должны подменяемые методы определить, как виртуальные, а так же должны объявить и определить виртуальный деструктор. В дальнейшем мы будем говорить, что базовые классы ВСЕГДА должны быть абстрактными. Как раз механизм с виртуальным деструктором дает нам возможность формировать такой базовый класс. Если мы определили чисто виртуальный деструктор в классе, то этот класс тоже является абстрактным, хотя, казалось бы, есть его реализация.
-
Пример. Виртуальный деструктор
class A // Абстрактый класс, несмотря на наличие реализации деструктора { public: virtual ~A() = 0; // Чисто виртуальный деструктор }; A::~A() {} // Реализация деструктора class B : public A { public: virtual ~B() { cout<<"Class B destructor called;"<<endl; } }; void main() { A* pobj = new B(); delete pobj; }
Дружба - это плохо. Дружба даёт доступ ко всем членам класса методов других классов. В классе для объектов указываем, что есть «друг». Схема получается очень зависимая.
Она приводит к тому, что если нужно вносить изменения написанный код (фу позор!!!).
Необходимо, чтобы дружественных отношений было как можно меньше.
-
Пример из лекции
class C { friend void f(C& ac); // друг-функция friend A::f(); // друг-метод friend B; // друг-класс (самый плохой вариант) };
Свойства дружбы:
-
Дружба не наследуется (сын друга - не друг)
-
тык
class C; // forward объявление class A { private: void f1() { cout<<"Executing f1;"<<endl; } friend C; }; class B : public A { private: void f2() { cout<<"Executing f2;"<<endl; } }; class C { public: static void g1(A& obj) { obj.f1(); } static void g2(B& obj) { obj.f1(); obj.f2(); // Error!!! Имеет доступ только к членам A } }; class D : public C { public: static void g2(A& obj) ( obj.f1(); } // Error!!! Дружба не наследуется };
-
-
Дружба не транзитивна (друг моего друга мне не друг).
Есть исключение: при полиморфизме мы получаем доступ к защищённым полям производных классов.
class C; // forward-объявление. C - это класс class A // полиморфный, так как есть виртуальный метод { protected: virtual void f(); friend class C; }; class B: public A { protected: virtual void f() override; }; class C { public: static void g(A& obj) { obj.f(); } }; B obj; C::g(obj);