Наследование

Наследование - это механизм, который позволяет создавать новые классы на основе уже существующих.
Новый класс, называемый подклассом или производным классом, может наследовать поля и методы от другого класса, называемого базовым классом или суперклассом.

Зачем это нужно?

Основная задача механизма наследования - избежать дублирования кода. Взглянем на пример:

Класс для создания котиков:
class Cat { private: std::string nickname; // Кличка животного int age; // Возраст std::string breed; // Порода public: // Конструктор Cat(std::string nickname, int age, std::string breed) : nickname(nickname), age(age), breed(breed) {} // Геттеры и сеттеры std::string getNickname() const { return nickname; } void setNickname(const std::string& new_nickname) { nickname = new_nickname; } // итд... // Другие методы void run() { std::cout << nickname + " побежал(а)" << std::endl; } // итд... }
Класс для создания птичек:
class Bird { private: std::string nickname; // Кличка животного int age; // Возраст std::string breed; // Порода public: // Конструктор Bird(std::string nickname, int age, std::string breed) : nickname(nickname), age(age), breed(breed) {} // Геттеры и сеттеры std::string getNickname() const { return nickname; } void setNickname(const std::string& new_nickname) { nickname = new_nickname; } // итд... // Другие методы void run() { std::cout << nickname + " побежал(а)" << std::endl; } void fly() { std::cout << nickname + " полетел(а)" << std::endl; } // итд... }

Вот мы написали два класса Cat и Bird, а чем они отличаются?

Почти ничем, кроме того, что птица ещё умеет летать, а коту мы можем добавить ещё какие-то методы, например орать под окнами.

Кроме котов и птиц, мы можем создать приложение зоопарк, где у нас будут десятки животных, у всех животных будут примерно одинаковые поля и методы.
В итоге, описывая каждого животного, нам придётся написать довольно много повторяющегося кода...

А что если мы создадим общий класс Animal, запишем в нём основные поля и методы, которые подходят для всех животных. В классах животных укажем лишь то, чем они отличаются.
В итоге мы получим родительский класс Animal и классы животных которые от него наследуются, т.е. имеют те же поля и методы, что и родительский класс.

Базовый класс Animal:
class Animal { protected: std::string nickname; int age; std::string breed; public: Animal(std::string nickname, int age, std::string breed) : nickname(nickname), age(age), breed(breed) {} std::string getNickname() const { return nickname; } void run() { std::cout << nickname << " побежал(а)" << std::endl; } };
Пояснение:
  • Animal - базовый класс, содержащий общие для всех животных свойства

  • Поля объявлены как protected, чтобы они были доступны в производных классах

  • Конструктор инициализирует основные свойства животного

  • Метод run() реализует общее поведение для всех животных

Создание производных классов
Класс Cat (наследует Animal)
class Cat : public Animal { public: // Конструктор вызывает конструктор базового класса Cat(std::string nickname, int age, std::string breed) : Animal(nickname, age, breed) {} // Дополнительные методы, специфичные для кошек void purr() { std::cout << nickname << " мурлычет" << std::endl; } };
Пояснение:
  • Класс Cat наследует все поля и методы класса Animal

  • Конструктор передает параметры в конструктор базового класса через : Animal(...)

  • Добавлен новый метод purr(), специфичный только для кошек

Класс Bird (наследует Animal)
class Bird : public Animal { public: Bird(std::string nickname, int age, std::string breed) : Animal(nickname, age, breed) {} // Переопределение метода run() void run() override { std::cout << nickname << " побежал(а), перебирая лапками" << std::endl; } // Новый метод для птиц void fly() { std::cout << nickname << " полетел(а)" << std::endl; } };
Пояснение:
  • Метод run() переопределен с помощью override для изменения поведения

  • Добавлен новый метод fly(), которого нет в базовом классе

  • Все остальные методы и поля остаются такими же, как у Animal

Использование наследованных классов
int main() { Cat myCat("Барсик", 3, "Персидский"); Bird myBird("Кеша", 2, "Попугай"); // Использование унаследованных методов std::cout << "Кличка кота: " << myCat.getNickname() << std::endl; myCat.run(); myCat.purr(); // Использование переопределенного и нового метода myBird.run(); // Вызовет переопределенную версию myBird.fly(); // Специфичный метод птицы return 0; }
Пояснение:
  • Объекты производных классов могут использовать методы базового класса

  • Переопределенные методы заменяют реализацию базового класса

  • Новые методы добавляют уникальное поведение


Расширение функциональности (пример с базой данных)

В контексте программирования более правильно говорить, что дочерний класс расширяет родительский класс. Т.е. добавляет родительскому классу функциональности.
Однако, в русскоговорящем обществе, больше прижился термин наследование.

Предположим, что мы используем библиотку для работы с базой данных DataBase, и там имеется класс, который умеет посылать основные запросы к БД.

Пример:
class DataBase { public: DataBase() {} void getRowById(std::string tableName, int id) { std::cout << "Получена строка " << id << " из таблицы " << tableName << std::endl; } void getRows(std::string tableName) { std::cout << "Получены все строки из таблицы " << tableName << std::endl; } void insertRow(std::string tableName, std::string rows[], std::string values[]) { std::cout << "Добавлена запись в таблицу " << tableName << std::endl; } }; class MyDataBase : public DataBase { public: MyDataBase() : DataBase() {} void deleteRowById(std::string tableName, int id) { std::cout << "Удалена строка " << id << " из таблицы " << tableName << std::endl; } // Переопределение метода с расширением функциональности void getRowById(std::string tableName, int id) override { std::cout << "[Логирование] Запрос строки " << id << std::endl; DataBase::getRowById(tableName, id); // Вызов родительского метода } };
Пояснение:
  • MyDataBase наследует всю функциональность DataBase

  • Добавлен новый метод deleteRowById()

  • Метод getRowById() переопределен для добавления логирования перед вызовом оригинального метода


Множественное наследование

  • Класс может наследовать от нескольких базовых классов

  • Важно избегать "ромбовидного" наследования без виртуальных базовых классов

  • Может приводить к неоднозначности (если одинаковые имена в разных базовых классах)

Пример:
class Printer { public: void print(string text) { cout << "Printing: " << text << endl; } }; class Scanner { public: void scan() { cout << "Scanning document" << endl; } }; class MultifunctionDevice : public Printer, public Scanner { public: void copy() { scan(); print("Scanned copy"); } }; int main() { MultifunctionDevice mfd; mfd.copy(); return 0; }

Практические рекомендации

  1. Используйте public наследование для отношения "является" (is-a)

  2. Избегайте глубоких иерархий наследования (не более 3-4 уровней)

  3. Применяйте override для явного указания переопределения методов

  4. Делайте деструкторы виртуальными в базовых классах

  5. Используйте protected для членов, которые должны быть доступны в производных классах

  6. Рассмотрите композицию как альтернативу наследованию для отношения "имеет" (has-a)


Наследование в C++ позволяет создавать иерархии классов, где:

  • Базовые классы содержат общую функциональность

  • Производные классы добавляют или изменяют поведение

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

  • Обеспечивается возможность повторного использования кода

Правильное применение наследования делает программу:

  • Более модульной

  • Легче расширяемой

  • Проще для понимания

  • Менее подверженной ошибкам


Комментарии

Добавить комментарий

Чтобы оставить комменатрий необходимо Авторизоваться