Лекція 16: Поліморфізм (Virtual Functions)
Слово "поліморфізм" означає "багато форм". У програмуванні це означає здатність об'єктів різних класів реагувати на одну й ту ж команду по-різному.
Уявіть універсальний пульт. У нього є кнопка "Ввімкнути".
- Якщо направити на телевізор — вмикається екран.
- Якщо на кондиціонер — дме повітря.
- Якщо на музичний центр — грає музика.
Команда одна ("Ввімкнути"), а дії різні залежно від об'єкта.
1. Проблема: Вказівники на базовий клас
У С++ є правило: Вказівник на базовий клас може зберігати адресу об'єкта-нащадка.
#include <iostream>
using namespace std;
class Animal {
public:
void speak() {
cout << "???" << endl;
}
};
class Dog : public Animal {
public:
void speak() {
cout << "Гав!" << endl;
}
};
int main() {
Animal* ptr = new Dog();
ptr->speak(); // Виведе "???" (метод Animal), а не "Гав!"
return 0;
}
Чому так сталося? Це називається раннє зв'язування (Static Binding). Компілятор бачить, що вказівник має тип Animal*. Він не знає, що там насправді лежить Dog (це з'ясується тільки під час запуску програми). Тому він "перестраховується" і прив'язує метод класу Animal.
2. Рішення: Ключове слово virtual
Щоб це виправити, ми маємо сказати компілятору: "Не прив'язуй функцію зараз. Подивись, що це за об'єкт, під час виконання програми".
Для цього ми додаємо слово virtual у базовому класі.
#include <iostream>
using namespace std;
class Animal {
public:
// Virtual каже: цей метод може бути перевизначений дітьми
virtual void speak() {
cout << "???" << endl;
}
// ВАЖЛИВО: Деструктор базового класу ЗАВЖДИ має бути віртуальним
virtual ~Animal() {}
};
class Dog : public Animal {
public:
// Override (не обов'язково, але бажано) перевіряє, чи правильно ми перевизначили
void speak() override {
cout << "Гав!" << endl;
}
};
int main() {
Animal* ptr = new Dog();
ptr->speak(); // Тепер виведе "Гав!", хоча вказівник типу Animal*
delete ptr;
return 0;
}
3. Навіщо це потрібно? (Масиви різних об'єктів)
Це найголовніша фішка. Завдяки поліморфізму ми можемо створити один масив, у якому будуть лежати абсолютно різні об'єкти (які мають спільного предка), і керувати ними однаково.
#include <iostream>
#include <vector> // Динамічний масив С++
using namespace std;
class Enemy {
public:
virtual void attack() {
cout << "Ворог просто стоїть." << endl;
}
};
class Zombie : public Enemy {
public:
void attack() override {
cout << "Зомбі кусається!" << endl;
}
};
class Robot : public Enemy {
public:
void attack() override {
cout << "Робот стріляє лазером!" << endl;
}
};
int main() {
// Масив вказівників на базовий клас
Enemy* enemies[3];
enemies[0] = new Zombie();
enemies[1] = new Robot();
enemies[2] = new Zombie();
cout << "Бій почався!" << endl;
// Цикл один, а дії різні!
for (int i = 0; i < 3; i++) {
enemies[i]->attack();
}
// Результат:
// Зомбі кусається!
// Робот стріляє лазером!
// Зомбі кусається!
return 0;
}
4. Абстрактні класи та Чисті віртуальні функції
Іноді базовий клас занадто загальний, щоб створювати його екземпляри. Наприклад, клас Shape (Фігура). Що таке "просто фігура"? Яка в неї площа? Невідомо. У кола — π * r², у квадрата — a², а у "просто фігури" — невизначено. Такі класи називаються Абстрактними. Щоб зробити клас абстрактним, у ньому має бути хоча б одна чиста віртуальна функція. Синтаксис: = 0;.
#include <iostream>
using namespace std;
class Shape {
public:
// Ми не знаємо, як рахувати площу тут.
// Нехай це роблять нащадки.
virtual double getArea() = 0;
};
// Shape s; // ПОМИЛКА! Не можна створити об'єкт абстрактного класу
class Square : public Shape {
public:
double side;
Square(double s) : side(s) {}
// Ми ЗОБОВ'ЯЗАНІ реалізувати цей метод, інакше Square теж буде абстрактним
double getArea() override {
return side * side;
}
};
int main() {
Square sq(5);
cout << "Площа квадрата: " << sq.getArea() << endl;
return 0;
}
5. Віртуальний деструктор
Запам'ятайте правило: Якщо у класі є хоча б одна virtual функція, деструктор теж має бути virtual.
Чому? Якщо ви видаляєте об'єкт через базовий вказівник (delete ptr), і деструктор НЕ віртуальний, спрацює тільки деструктор Animal, а деструктор Dog не запуститься. Пам'ять, виділена всередині Dog, не очиститься → витік пам'яті.
#include <iostream>
using namespace std;
class Animal {
public:
virtual void speak() {
cout << "???" << endl;
}
virtual ~Animal() { // Віртуальний деструктор!
cout << "Деструктор Animal" << endl;
}
};
class Dog : public Animal {
private:
int* data; // Припустимо, тут виділяється пам'ять
public:
Dog() {
data = new int[100];
}
void speak() override {
cout << "Гав!" << endl;
}
~Dog() { // Це теж має спрацювати!
delete[] data;
cout << "Деструктор Dog" << endl;
}
};
int main() {
Animal* ptr = new Dog();
delete ptr; // Тепер викликаються ОБА деструктора
return 0;
}
Практичні завдання до Лекції 16
Виконайте ці завдання в своєму середовищі розробки.
Завдання 1: Геометричні фігури
- Створіть абстрактний клас
Shapeз чистими віртуальними методамиgetArea()таprint(). - Створіть класи
Rectangle(прямокутник) таCircle(коло), які успадковуютьShape. - Реалізуйте метод
getArea():- Для
Rectangle:a * b(деaтаb— сторони). - Для
Circle:π * r²(де π ≈ 3.14,r— радіус).
- Для
- У
mainстворіть масив вказівників наShape, додайте туди одинCircleта одинRectangle. - У циклі виведіть площу кожної фігури.
Завдання 2: Музичні інструменти
- Створіть інтерфейс (абстрактний клас)
Musicianз чистою віртуальною функцієюplay(). - Створіть класи
GuitaristтаDrummer:Guitaristвиводить: "Бринь-бринь".Drummerвиводить: "Бах-бух".
- Напишіть функцію
concert(Musician* band[], int size), яка приймає масив музикантів і змушує кожного грати.
Завдання 3: Платіжна система
Це реальний бізнес-кейс.
- Створіть абстрактний клас
PaymentMethodз методомpay(double amount). - Створіть класи
CreditCardтаPayPal:CreditCard::payвиводить: "Оплата [сума] грн картою... Комісія 2%".PayPal::payвиводить: "Оплата [сума] грн через PayPal... Без комісії".
- У
mainкористувач вводить суму покупки, потім вибирає спосіб оплати (1 — Карта, 2 — PayPal). - Створіть відповідний об'єкт у вказівнику
PaymentMethod* ptrі викличтеptr->pay(sum).