Отже, наша подорож в спадкування і віртуальні функції в мові C++ підійшла до кінця. Пора закріпити пройдений матеріал.
Теорія
Мова C++ дозволяє створювати вказівники/посилання батьківського класу на об’єкти дочірніх класів. Це корисно при використанні функцій або масивів, які повинні працювати з об’єктами дочірніх класів.
Без віртуальних функцій вказівники/посилання батьківського класу на об’єкт дочірнього класу матимуть доступ лише до членів батьківського класу.
Віртуальна функція — це особливий тип функції, яка, при зверненні до неї, викликає “найдочірніший” метод (перевизначення), який існує між батьківським і дочірніми класами. Щоб вважатися перевизначенням, метод дочірнього класу повинен мати ту ж сигнатуру і тип повернення, що і віртуальна функція батьківського класу. Єдиний виняток — коваріантний тип повернення, який дозволяє перевизначенню повертати вказівник або посилання на дочірній клас, якщо метод батьківського класу повертає вказівник або посилання на себе.
Модифікатор override використовується для позначення методу перевизначенням.
Модифікатор final забороняє перевизначати віртуальну функцію або наслідувати певний клас.
Використовуючи віртуальні функції, не забувайте додавати в батьківський клас віртуальний деструктор, щоб в разі видалення вказівника на батьківський клас викликався відповідний деструктор.
Ви можете ігнорувати виклик перевизначень віртуальної функції, використовуючи оператор дозволу області видимості, щоб безпосередньо вказати, яку функцію ви хочете викликати. Наприклад, parent.Parent::GetName()
.
Раннє зв’язування відбувається, коли компілятор зустрічає прямий виклик функції. Компілятор або лінкер можуть безпосередньо обробляти прямі виклики функцій. Пізнє зв’язування відбувається при виклику вказівника на функцію. У таких випадках неможливо знати наперед, яка функція викликатиметься першою. Віртуальні функції використовують пізнє зв’язування і віртуальні таблиці для визначення того, яку версію функції слід викликати.
Відносні недоліки віртуальних функцій:
Виклик віртуальних функцій займає більше часу.
Необхідність наявності віртуальної таблиці збільшує розмір кожного об’єкта класу, що містить віртуальну функцію, на розмір одного вказівника.
Віртуальну функцію можна зробити чистою віртуальною/абстрактною функцією, додавши = 0
в кінець її прототипу. Клас, який містить чисту віртуальну функцію, називається абстрактним класом. Об’єкти абстрактного класу не можуть бути створені. Клас, який наслідує чисті віртуальні функції, повинен надати свої перевизначення цих функцій, або він також буде вважатися абстрактним. Чисті віртуальні функції можуть мати тіло (визначення, записане окремо), але вони як і раніше вважаються абстрактними функціями.
Інтерфейс (або «інтерфейсний клас») — це клас без змінних-членів, всі методи якого є чистими віртуальними функціями. Імена інтерфейсів часто починаються з I
.
Віртуальний базовий клас — це батьківський клас, об’єкт якого є загальним для використання всіма дочірніми класами.
При присвоюванні об’єкта дочірнього класу об’єкту батьківського класу, в об’єкт батьківського класу копіюється лише батьківська частина об’єкта, що копіюється, а дочірня частина просто обрізається. Цей процес називається обрізкою об’єктів.
Динамічне приведення використовується для конвертації вказівника батьківського класу у вказівник дочірнього класу. Це називається понижуючим приведенням типу. Якщо конвертація пройшла невдало, то повертається нульовий вказівник.
Найпростіший спосіб перевантажити оператор виводу <<
для класів зі спадкуванням — записати перевантаження оператора <<
в батьківському класі, а виконання операції виводу делегувати віртуальному методу.
Тест
Завдання №1
Кожна з наступних програм має якусь помилку. Ваше завдання полягає в тому, щоб знайти цю помилку візуально (не запускаючи код). Передбачуваний результат виконання кожної програми:
Child
a)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
#include <iostream> class Parent { protected: int m_value; public: Parent(int value) : m_value(value) { } const char* getName() const { return "Parent"; } }; class Child: public Parent { public: Child(int value) : Parent(value) { } const char* getName() const { return "Child"; } }; int main() { Child ch(7); Parent &p = ch; std::cout << p.getName(); return 0; } |
Відповідь №1.a)
Parent::getName() не є віртуальною функцією, тому p.getName()
не викличе Child::getName().
b)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
#include <iostream> class Parent { protected: int m_value; public: Parent(int value) : m_value(value) { } virtual const char* getName() { return "Parent"; } }; class Child: public Parent { public: Child(int value) : Parent(value) { } virtual const char* getName() const { return "Child"; } }; int main() { Child ch(7); Parent &p = ch; std::cout << p.getName(); return 0; } |
Відповідь №1.b)
Хоча метод Child::getName() є константним, метод Parent::getName() не є константним, тому Child::getName() не рахується перевизначенням і, відповідно, не викликається.
c)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
#include <iostream> class Parent { protected: int m_value; public: Parent(int value) : m_value(value) { } virtual const char* getName() { return "Parent"; } }; class Child: public Parent { public: Child(int value) : Parent(value) { } virtual const char* getName() override { return "Child"; } }; int main() { Child ch(7); Parent p = ch; std::cout << p.getName(); return 0; } |
Відповідь №1.c)
Об’єкт ch
присвоюється об’єкту p
по значенню (а не по посиланню), що призводить до обрізки об’єкта ch
.
d)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
#include <iostream> class Parent final { protected: int m_value; public: Parent(int value) : m_value(value) { } virtual const char* getName() { return "Parent"; } }; class Child: public Parent { public: Child(int value) : Parent(value) { } virtual const char* getName() override { return "Child"; } }; int main() { Child ch(7); Parent &p = ch; std::cout << p.getName(); return 0; } |
Відповідь №1.d)
Клас Parent був оголошений як final, тому клас Child не може наслідувати клас Parent. Результат — помилка компіляції.
e)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
#include <iostream> class Parent { protected: int m_value; public: Parent(int value) : m_value(value) { } virtual const char* getName() { return "Parent"; } }; class Child: public Parent { public: Child(int value) : Parent(value) { } virtual const char* getName() = 0; }; const char* Child::getName() { return "Child"; } int main() { Child ch(7); Parent &p = ch; std::cout << p.getName(); return 0; } |
Відповідь №1.e)
Child::getName() є абстрактною функцією, хоча має тіло (записане окремо), тому клас Child є абстрактним, а об’єкти абстрактного класу створювати заборонено.
f)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
#include <iostream> class Parent { protected: int m_value; public: Parent(int value) : m_value(value) { } virtual const char* getName() { return "Parent"; } }; class Child: public Parent { public: Child(int value) : Parent(value) { } virtual const char* getName() { return "Child"; } }; int main() { Child *ch = new Child(7); Parent *p = ch; std::cout << p->getName(); delete p; return 0; } |
Відповідь №1.f)
Ця програма виводить вірний результат, але має іншу проблему. В кінці функції main() ми видаляємо p
, який є вказівником класу Parent, але у нас немає віртуального деструктора в класі Parent. Отже, видаляється лише частина Parent об’єкта класу ch
, а частина Child об’єкта ch
залишається у вигляді “витоку пам’яті”.
Завдання №2
a) Створіть абстрактний клас Shape. Цей клас повинен мати три методи:
чисту віртуальну функцію print() з параметром типу std::ostream;
перевантаження operator<<;
пустий віртуальний деструктор.
Відповідь №2.a)
1 2 3 4 5 6 7 8 9 10 11 |
class Shape { public: virtual std::ostream& print(std::ostream &out) const = 0; friend std::ostream& operator<<(std::ostream &out, const Shape &p) { return p.print(out); } virtual ~Shape() {} }; |
b) Створіть два класи: Triangle і Circle, які наслідують клас Shape.
Triangle повинен мати 3 точки в якості змінних-членів.
Circle повинен мати одну центральну точку і цілочисельний радіус в якості змінних-членів.
Перевантажте функцію print(), щоб наступний код:
1 2 3 4 5 6 7 8 9 10 |
int main() { Circle c(Point(1, 2, 3), 7); std::cout << c << '\n'; Triangle t(Point(1, 2, 3), Point(4, 5, 6), Point(7, 8, 9)); std::cout << t << '\n'; return 0; } |
Видавав наступний результат:
Circle(Point(1, 2, 3), radius 7)
Triangle(Point(1, 2, 3), Point(4, 5, 6), Point(7, 8, 9))
Ось клас Point, який ви можете використовувати:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class Point { private: int m_x = 0; int m_y = 0; int m_z = 0; public: Point(int x, int y, int z) : m_x(x), m_y(y), m_z(z) { } friend std::ostream& operator<<(std::ostream &out, const Point &p) { out << "Point(" << p.m_x << ", " << p.m_y << ", " << p.m_z << ")"; return out; } }; |
Відповідь №2.b)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
#include <iostream> class Point { private: int m_x = 0; int m_y = 0; int m_z = 0; public: Point(int x, int y, int z) : m_x(x), m_y(y), m_z(z) { } friend std::ostream& operator<<(std::ostream &out, const Point &p) { out << "Point(" << p.m_x << ", " << p.m_y << ", " << p.m_z << ")"; return out; } }; class Shape { public: virtual std::ostream& print(std::ostream &out) const = 0; friend std::ostream& operator<<(std::ostream &out, const Shape &p) { return p.print(out); } virtual ~Shape() {} }; class Triangle : public Shape { private: Point m_p1; Point m_p2; Point m_p3; public: Triangle(const Point &p1, const Point &p2, const Point &p3) : m_p1(p1), m_p2(p2), m_p3(p3) { } virtual std::ostream& print(std::ostream &out) const override { out << "Triangle(" << m_p1 << ", " << m_p2 << ", " << m_p3 << ")"; return out; } }; class Circle: public Shape { private: Point m_center; int m_radius; public: Circle(const Point ¢er, int radius) : m_center(center), m_radius(radius) { } virtual std::ostream& print(std::ostream &out) const override { out << "Circle(" << m_center << ", radius " << m_radius << ")"; return out; } }; int main() { Circle c(Point(1, 2, 3), 7); std::cout << c << '\n'; Triangle t(Point(1, 2, 3), Point(4, 5, 6), Point(7, 8, 9)); std::cout << t << '\n'; return 0; } |
c) Використовуючи код з попередніх завдань (класи Point, Shape, Circle і Triangle) завершіть наступну програму:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#include <iostream> #include <vector> int main() { std::vector<Shape*> v; v.push_back(new Circle(Point(1, 2, 3), 7)); v.push_back(new Triangle(Point(1, 2, 3), Point(4, 5, 6), Point(7, 8, 9))); v.push_back(new Circle(Point(4, 5, 6), 3)); // Виведення елементів вектора v тут std::cout << "The largest radius is: " << getLargestRadius(v) << '\n'; // реалізуйте цю функцію // Видалення елементів вектора v тут } |
Підказка: Вам потрібно додати метод getRadius() в Circle і виконати понижуюче приведення Shape* в Circle*, щоб отримати доступ до цього методу.
Відповідь №2.c)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
#include <iostream> #include <vector> class Point { private: int m_x = 0; int m_y = 0; int m_z = 0; public: Point(int x, int y, int z) : m_x(x), m_y(y), m_z(z) { } friend std::ostream& operator<<(std::ostream &out, const Point &p) { out << "Point(" << p.m_x << ", " << p.m_y << ", " << p.m_z << ")"; return out; } }; class Shape { public: virtual std::ostream& print(std::ostream &out) const = 0; friend std::ostream& operator<<(std::ostream &out, const Shape &p) { return p.print(out); } virtual ~Shape() {} }; class Triangle : public Shape { private: Point m_p1; Point m_p2; Point m_p3; public: Triangle(const Point &p1, const Point &p2, const Point &p3) : m_p1(p1), m_p2(p2), m_p3(p3) { } virtual std::ostream& print(std::ostream &out) const override { out << "Triangle(" << m_p1 << ", " << m_p2 << ", " << m_p3 << ")"; return out; } }; class Circle: public Shape { private: Point m_center; int m_radius; public: Circle(const Point ¢er, int radius) : m_center(center), m_radius(radius) { } virtual std::ostream& print(std::ostream &out) const override { out << "Circle(" << m_center << ", radius " << m_radius << ")"; return out; } int getRadius() { return m_radius; } }; int getLargestRadius(const std::vector<Shape*> &v) { int largestRadius { 0 }; // Перебираємо кожний елемент вектора for (auto const &element : v) { // Виконуємо перевірку на нульовий вказівник результату динамічного приведення if (Circle *c = dynamic_cast<Circle*>(element)) { if (c->getRadius() > largestRadius) largestRadius = c->getRadius(); } } return largestRadius; } int main() { std::vector<Shape*> v; v.push_back(new Circle(Point(1, 2, 3), 7)); v.push_back(new Triangle(Point(1, 2, 3), Point(4, 5, 6), Point(7, 8, 9))); v.push_back(new Circle(Point(4, 5, 6), 3)); for (auto const &element : v) std::cout << *element << '\n'; std::cout << "The largest radius is: " << getLargestRadius(v) << '\n'; for (auto const &element : v) delete element; return 0; } |