17.2 – Основы наследования в C++
Теперь, когда мы поговорили о том, что такое наследование в абстрактном смысле, давайте поговорим о том, как оно используется в C++.
В C++ наследование выполняется между классами. В связи наследование («является чем-либо») класс, от которого выполняется наследование, называется родительским классом, базовым классом или суперклассом, а класс, выполняющий наследование, называется дочерним классом, производным классом или подклассом.
На приведенной выше диаграмме фрукт является родительским элементом, а яблоко и банан – дочерними элементами.
На этой диаграмме треугольник является дочерним элементом (для фигуры) и родительским элементом (для прямоугольного треугольника).
Дочерний класс наследует от родительского класса как поведение (функции-члены), так и свойства (переменные-члены) (с учетом некоторых ограничений доступа, которые мы рассмотрим в следующем уроке).
Эти переменные и функции становятся членами производного класса.
Поскольку дочерние классы являются полноценными классами, они (конечно) могут иметь свои собственные, специфичные для себя члены. Пример этого мы скоро увидим.
Класс 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
:
- Добавить имя и возраст в класс
BaseballPlayer
непосредственно в качестве членов. Вероятно, это худший вариант, поскольку мы дублируем код, который уже существует в нашем классеPerson
. Любые обновления дляPerson
также должны быть сделаны и вBaseballPlayer
. - Добавить
Person
в качестве членаBaseballPlayer
, используя композицию. Но мы должны спросить себя: «Есть лиPerson
уBaseballPlayer
»? Нет, это не так. Так что это неправильная парадигма. - Заставить
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}
{
}
};
На диаграмме наше наследование выглядит так:
Когда BaseballPlayer
наследуется от Person
, BaseballPlayer
получает от Person
функции-члены и переменные-члены. Кроме того, BaseballPlayer
определяет два собственных члена: m_battingAverage
и m_homeRuns
. Это имеет смысл, поскольку эти свойства специфичны для BaseballPlayer
, а не для любого человека Person
.
Таким образом, объекты BaseballPlaye
r будут иметь 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
).
Это дает нам диаграмму наследования, которая выглядит следующим образом:
Обратите внимание, что 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]{};
};
Теперь наша диаграмма наследования выглядит так:
Все объекты Supervisor
наследуют функции и переменные как от Employee
, так и от Person
, и добавляют свою собственную переменную-член m_overseesIDs
.
Создавая такие цепочки наследования, мы можем создать набор пригодных для повторного использования классов, которые являются очень обобщенными (вверху) и постепенно становятся более конкретными на каждом уровне наследования.
Чем полезен такой вид наследования?
Наследование от базового класса означает, что в наших производных классах нам не нужно переопределять информацию из базового класса. Мы автоматически получаем функции-члены и переменные-члены базового класса через наследование, а затем просто добавляем необходимые нам, дополнительные функции или переменные-члены. Это не только экономит время, но также означает, что если мы когда-либо обновим или изменим базовый класс (например, добавим новые функции или исправим ошибку), все наши производные классы автоматически унаследуют изменения!
Например, если мы когда-нибудь добавим новую функцию в Person
, и Employee
, и Supervisor
автоматически получат к ней доступ. Если мы добавим новую переменную в Employee
, Supervisor
также получит к ней доступ. Это позволяет нам создавать новые классы простым, интуитивно понятным и не требующим сложной поддержки способом!
Заключение
Наследование позволяет нам повторно использовать классы, заставляя другие классы наследовать их члены. В будущих уроках мы продолжим изучать, как это работает.