← Повернутися до лекцій

Лекція 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², у квадрата — , а у "просто фігури" — невизначено. Такі класи називаються Абстрактними. Щоб зробити клас абстрактним, у ньому має бути хоча б одна чиста віртуальна функція. Синтаксис: = 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: Геометричні фігури

  1. Створіть абстрактний клас Shape з чистими віртуальними методами getArea() та print().
  2. Створіть класи Rectangle (прямокутник) та Circle (коло), які успадковують Shape.
  3. Реалізуйте метод getArea():
    • Для Rectangle: a * b (де a та b — сторони).
    • Для Circle: π * r² (де π ≈ 3.14, r — радіус).
  4. У main створіть масив вказівників на Shape, додайте туди один Circle та один Rectangle.
  5. У циклі виведіть площу кожної фігури.

Завдання 2: Музичні інструменти

  1. Створіть інтерфейс (абстрактний клас) Musician з чистою віртуальною функцією play().
  2. Створіть класи Guitarist та Drummer:
    • Guitarist виводить: "Бринь-бринь".
    • Drummer виводить: "Бах-бух".
  3. Напишіть функцію concert(Musician* band[], int size), яка приймає масив музикантів і змушує кожного грати.

Завдання 3: Платіжна система

Це реальний бізнес-кейс.

  1. Створіть абстрактний клас PaymentMethod з методом pay(double amount).
  2. Створіть класи CreditCard та PayPal:
    • CreditCard::pay виводить: "Оплата [сума] грн картою... Комісія 2%".
    • PayPal::pay виводить: "Оплата [сума] грн через PayPal... Без комісії".
  3. У main користувач вводить суму покупки, потім вибирає спосіб оплати (1 — Карта, 2 — PayPal).
  4. Створіть відповідний об'єкт у вказівнику PaymentMethod* ptr і викличте ptr->pay(sum).