11. Наследование и полиморфизм

Что такое наследование

Наследование — это механизм, позволяющий одному классу (наследнику) использовать функциональность другого класса (родителя). Это позволяет не дублировать код и создавать более гибкие структуры.
Пример из жизни: у вас есть класс «Животное». От него можно унаследовать «Кошку», «Собаку», «Птицу». Все они умеют двигаться и дышать, но делают это по-своему.

Основной синтаксис


class Parent {
public:
    void sayHello() {
        cout << "Привет из базового класса!" << endl;
    }
};

class Child : public Parent { public: void sayBye() { cout << "Пока из дочернего класса!" << endl; } };

int main() { Child c; c.sayHello(); // метод унаследован от Parent c.sayBye(); // собственный метод Child return 0; }

Результат

Привет из базового класса! Пока из дочернего класса!

Типы наследования

В C++ существуют три типа наследования:
  • public — открытое: публичные члены родителя остаются доступными.
  • protected — защищённое: члены становятся доступными только внутри наследников.
  • private — закрытое: наследник получает копию, но не даёт доступ дальше.

Пример


class Base {
public:
    int x = 10;
};

class Derived : protected Base { public: void show() { cout << "x = " << x << endl; } };

int main() { Derived d; d.show(); // cout << d.x; // Ошибка! x защищённый. return 0; }

Конструкторы и наследование

Конструктор базового класса вызывается автоматически при создании объекта наследника.

#include <iostream>
using namespace std;

class Animal { public: Animal() { cout << "Создано животное" << endl; } };

class Dog : public Animal { public: Dog() { cout << "Создана собака" << endl; } };

int main() { Dog d; return 0; }

Создано животное Создана собака

Переопределение методов

Иногда нужно, чтобы дочерний класс переопределял поведение родителя.

class Animal {
public:
    void speak() {
        cout << "Животное издаёт звук" << endl;
    }
};

class Cat : public Animal { public: void speak() { cout << "Кошка говорит: Мяу!" << endl; } };

int main() { Cat c; c.speak(); // Мяу! return 0; }

Проблема без виртуальных функций

Если вы используете указатель на базовый класс, переопределённый метод может не вызываться:

Animal* a = new Cat();
a->speak(); // Выведет: Животное издаёт звук (а не Мяу!)

Это поведение можно исправить с помощью виртуальных функций.

Полиморфизм и виртуальные функции

Полиморфизм — это возможность вызывать разные реализации одного метода через один интерфейс.

#include <iostream>
using namespace std;

class Animal { public: virtual void speak() { cout << "Животное издаёт звук" << endl; } };

class Cat : public Animal { public: void speak() override { cout << "Кошка говорит: Мяу!" << endl; } };

class Dog : public Animal { public: void speak() override { cout << "Собака говорит: Гав!" << endl; } };

int main() { Animal* zoo[2]; zoo[0] = new Cat(); zoo[1] = new Dog();

for (int i = 0; i < 2; i++) { zoo[i]->speak(); }

for (int i = 0; i < 2; i++) delete zoo[i]; return 0; }

Результат

Кошка говорит: Мяу! Собака говорит: Гав!

Ключевые слова

  • virtual — объявляет метод виртуальным, позволяя переопределять его в наследниках.
  • override — указывает, что метод переопределяет родительский.
  • final — запрещает дальнейшее переопределение.

Чисто виртуальные функции и абстрактные классы

Иногда базовый класс задаёт только общий интерфейс, но не содержит реализацию. Такие классы называются абстрактными.

class Shape {
public:
    virtual void draw() = 0; // чисто виртуальная функция
};

class Circle : public Shape { public: void draw() override { cout << "Рисуем круг" << endl; } };

int main() { Circle c; c.draw(); return 0; }

Абстрактный класс нельзя создать напрямую — только через его наследников.

Мини-проект: зоопарк


#include <iostream>
#include <vector>
#include <memory>
using namespace std;

class Animal { public: virtual void speak() = 0; virtual ~Animal() {} };

class Cat : public Animal { public: void speak() override { cout << "Мяу!" << endl; } };

class Dog : public Animal { public: void speak() override { cout << "Гав!" << endl; } };

int main() { vector<unique_ptr<Animal>> zoo; zoo.push_back(make_unique<Cat>()); zoo.push_back(make_unique<Dog>());

for (auto& animal : zoo) { animal->speak(); } return 0; }

Что здесь происходит

  • Базовый класс Animal содержит чисто виртуальную функцию speak().
  • Классы Cat и Dog переопределяют её по-своему.
  • Мы используем unique_ptr — умные указатели для безопасного управления памятью.

Типичные ошибки новичков

  • Забывают ключевое слово virtual в базовом классе, и методы не переопределяются.
  • Создают объект абстрактного класса (что невозможно).
  • Не делают виртуальный деструктор в базовом классе, что может привести к утечкам памяти.

Советы новичкам

  • Начинайте с простого: наследование — это "родитель-ребёнок" в коде.
  • Всегда добавляйте virtual в базовый класс, если собираетесь использовать указатели.
  • Используйте override — это помогает компилятору находить ошибки.
  • Учитесь думать абстрактно: определяйте общие действия, а детали реализуйте в наследниках.

Итоги урока

Теперь вы знаете:
  • Что такое наследование и как оно помогает избегать дублирования кода.
  • Как работает полиморфизм и виртуальные функции.
  • Что такое абстрактные классы и чисто виртуальные методы.
  • Как создавать гибкие, масштабируемые программы с использованием ООП.