17.2 – Основы наследования в C++

Добавлено 30 июля 2021 в 23:38

Теперь, когда мы поговорили о том, что такое наследование в абстрактном смысле, давайте поговорим о том, как оно используется в C++.

В C++ наследование выполняется между классами. В связи наследование («является чем-либо») класс, от которого выполняется наследование, называется родительским классом, базовым классом или суперклассом, а класс, выполняющий наследование, называется дочерним классом, производным классом или подклассом.

Рисунок 1 Диаграмма связей между яблоками, бананами и фруктами
Рисунок 1 – Диаграмма связей между яблоками, бананами и фруктами

На приведенной выше диаграмме фрукт является родительским элементом, а яблоко и банан – дочерними элементами.

Рисунок 2 Диаграмма иерархии фигур
Рисунок 2 – Диаграмма иерархии фигур

На этой диаграмме треугольник является дочерним элементом (для фигуры) и родительским элементом (для прямоугольного треугольника).

Дочерний класс наследует от родительского класса как поведение (функции-члены), так и свойства (переменные-члены) (с учетом некоторых ограничений доступа, которые мы рассмотрим в следующем уроке).

Эти переменные и функции становятся членами производного класса.

Поскольку дочерние классы являются полноценными классами, они (конечно) могут иметь свои собственные, специфичные для себя члены. Пример этого мы скоро увидим.

Класс Person

Вот простой класс, представляющий типового человека:

#include <string>
 
class Person
{
// В этом примере для простоты мы делаем члены открытыми
public: 
    std::string m_name{};
    int m_age{};
 
    Person(const std::string& name = "", int age = 0)
        : m_name{ name }, m_age{ age }
    {
    }
 
    const std::string& getName() const { return m_name; }
    int getAge() const { return m_age; }
 
};

Поскольку этот класс Person предназначен для обобщенного представления человека, мы определили только те члены, которые будут общими для любого типа человека. У каждого человека (независимо от пола, профессии и т.д.) есть имя и возраст, поэтому они здесь представлены.

Обратите внимание, что в этом примере мы сделали все наши переменные и функции открытыми. Это сделано исключительно для того, чтобы сейчас эти примеры были простыми. Обычно мы делаем переменные закрытыми. Мы поговорим об управлении доступом и о том, как оно взаимодействуют с наследованием, позже в этой главе.

Класс BaseballPlayer

Допустим, мы хотели написать программу, которая отслеживает информацию о некоторых бейсболистах. Бейсболисты должны содержать информацию, относящуюся к бейсболистам – например, мы можем захотеть сохранить среднее значение ударов игрока и количество хоумранов, которые он совершил.

Вот наш неполный класс бейсболиста:

class BaseballPlayer
{
// В этом примере для простоты мы делаем члены открытыми
public:
    double m_battingAverage{};
    int m_homeRuns{};
 
    BaseballPlayer(double battingAverage = 0.0, int homeRuns = 0)
       : m_battingAverage{battingAverage}, m_homeRuns{homeRuns}
    {
    }
};

Теперь мы также хотим отслеживать имя и возраст бейсболиста, и у нас уже есть эта информация как часть нашего класса Person.

У нас есть три варианта, как добавить имя и возраст в BaseballPlayer:

  1. Добавить имя и возраст в класс BaseballPlayer непосредственно в качестве членов. Вероятно, это худший вариант, поскольку мы дублируем код, который уже существует в нашем классе Person. Любые обновления для Person также должны быть сделаны и в BaseballPlayer.
  2. Добавить Person в качестве члена BaseballPlayer, используя композицию. Но мы должны спросить себя: «Есть ли Person у BaseballPlayer»? Нет, это не так. Так что это неправильная парадигма.
  3. Заставить BaseballPlayer наследовать эти атрибуты от Person. Помните, что наследование представляет собой связь «является чем-либо». BaseballPlayer – это Person? Бейсболист – это человек? Да. Поэтому наследование здесь – подходящий выбор.

Делаем BaseballPlayer производным классом

Чтобы BaseballPlayer наследовался от нашего класса Person, необходимо применить довольно простой синтаксис. После объявления класса BaseballPlayer мы используем двоеточие, слово «public» и имя класса, от которого мы хотим наследоваться. Это называется публичным (открытым) наследованием. О том, что означает открытое наследование, мы поговорим подробнее на следующем уроке.

// BaseballPlayer открыто наследует Person
class BaseballPlayer : public Person
{
public:
    double m_battingAverage{};
    int m_homeRuns{};
 
    BaseballPlayer(double battingAverage = 0.0, int homeRuns = 0)
       : m_battingAverage{battingAverage}, m_homeRuns{homeRuns}
    {
    }
};

На диаграмме наше наследование выглядит так:

Рисунок 3 BaseballPlayer наследуется от Person
Рисунок 3 – BaseballPlayer наследуется от Person

Когда BaseballPlayer наследуется от Person, BaseballPlayer получает от Person функции-члены и переменные-члены. Кроме того, BaseballPlayer определяет два собственных члена: m_battingAverage и m_homeRuns. Это имеет смысл, поскольку эти свойства специфичны для BaseballPlayer, а не для любого человека Person.

Таким образом, объекты BaseballPlayer будут иметь 4 переменных-члена: m_battingAverage и m_homeRuns от BaseballPlayer, а m_name и m_age от Person.

Это легко доказать:

#include <iostream>
#include <string>
 
class Person
{
public:
    std::string m_name{};
    int m_age{};
 
    Person(const std::string& name = "", int age = 0)
        : m_name{name}, m_age{age}
    {
    }
 
    const std::string& getName() const { return m_name; }
    int getAge() const { return m_age; }
 
};
 
// BaseballPlayer открыто наследует Person
class BaseballPlayer : public Person
{
public:
    double m_battingAverage{};
    int m_homeRuns{};
 
    BaseballPlayer(double battingAverage = 0.0, int homeRuns = 0)
       : m_battingAverage{battingAverage}, m_homeRuns{homeRuns}
    {
    }
};
 
int main()
{
    // Создаем новый объект BaseballPlayer
    BaseballPlayer joe{};
    // Присваиваем ему имя (мы можем сделать это напрямую, потому что m_name открытая)
    joe.m_name = "Joe";
    // Печатаем имя
    // используем функцию getName(), которую мы получили из базового класса Person
    std::cout << joe.getName() << '\n';
 
    return 0;
}

Этот код печатает:

Joe

Этот код компилируется и запускается, потому что joe – это BaseballPlayer, а все объекты BaseballPlayer имеют переменную-член m_name и функцию-член getName(), унаследованные от класса Person.

Производный класс Employee

Теперь давайте напишем еще один класс, который также наследуется от Person. На этот раз мы напишем класс сотрудника, Employee. Employee «является» Person (сотрудник является человеком), поэтому использование наследования уместно:

// Employee публично наследуется от Person
class Employee: public Person
{
public:
    double m_hourlySalary{};
    long m_employeeID{};
 
    Employee(double hourlySalary = 0.0, long employeeID = 0)
        : m_hourlySalary{hourlySalary}, m_employeeID{employeeID}
    {
    }
 
    void printNameAndSalary() const
    {
        std::cout << m_name << ": " << m_hourlySalary << '\n';
    }
};

Employee наследует m_name и m_age от Person (а также две функции доступа) и добавляет еще две переменные-члены и собственную функцию-член. Обратите внимание, что printNameAndSalary() использует переменные как из класса, которому она принадлежит (Employee::m_hourlySalary), так и из родительского класса (Person::m_name).

Это дает нам диаграмму наследования, которая выглядит следующим образом:

Рисунок 4 Диаграмма наследования
Рисунок 4 – Диаграмма наследования

Обратите внимание, что Employee и BaseballPlayer не имеют прямых связей, хотя оба они наследуются от Person.

Вот полный пример использования Employee:

#include <iostream>
#include <string>
 
class Person
{
public:
    std::string m_name{};
    int m_age{};
 
    const std::string& getName() const { return m_name; }
    int getAge() const { return m_age; }
 
    Person(const std::string& name = "", int age = 0)
        : m_name{name}, m_age{age}
    {
    }
};
 
// Employee публично наследуется от Person
class Employee: public Person
{
public:
    double m_hourlySalary{};
    long m_employeeID{};
 
    Employee(double hourlySalary = 0.0, long employeeID = 0)
        : m_hourlySalary{hourlySalary}, m_employeeID{employeeID}
    {
    }
 
    void printNameAndSalary() const
    {
        std::cout << m_name << ": " << m_hourlySalary << '\n';
    }
};
 
int main()
{
    Employee frank{20.25, 12345};
    frank.m_name = "Frank"; // мы можем это сделать, потому что m_name открытая
 
    frank.printNameAndSalary();
    
    return 0;
}

Этот код печатает:

Frank: 20.25

Цепочки наследования

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

Например, давайте напишем класс руководителя, Supervisor. Руководитель (Supervisor) является сотрудником (Employee), который является человеком (Person). Мы уже написали класс Employee, поэтому давайте использовать его в качестве базового класса для наследования Supervisor:

class Supervisor: public Employee
{
public:
    // Этот руководитель может контролировать максимум 5 сотрудников
    long m_overseesIDs[5]{};
};

Теперь наша диаграмма наследования выглядит так:

Рисунок 5 Диаграмма наследования
Рисунок 5 – Диаграмма наследования

Все объекты Supervisor наследуют функции и переменные как от Employee, так и от Person, и добавляют свою собственную переменную-член m_overseesIDs.

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

Чем полезен такой вид наследования?

Наследование от базового класса означает, что в наших производных классах нам не нужно переопределять информацию из базового класса. Мы автоматически получаем функции-члены и переменные-члены базового класса через наследование, а затем просто добавляем необходимые нам, дополнительные функции или переменные-члены. Это не только экономит время, но также означает, что если мы когда-либо обновим или изменим базовый класс (например, добавим новые функции или исправим ошибку), все наши производные классы автоматически унаследуют изменения!

Например, если мы когда-нибудь добавим новую функцию в Person, и Employee, и Supervisor автоматически получат к ней доступ. Если мы добавим новую переменную в Employee, Supervisor также получит к ней доступ. Это позволяет нам создавать новые классы простым, интуитивно понятным и не требующим сложной поддержки способом!

Заключение

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

Теги

C++ / CppLearnCppДля начинающихКласс (программирование)НаследованиеОбучениеОбъектно-ориентированное программирование (ООП)Программирование

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.