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


Наследование (inheritance) представляет один из ключевых аспектов объектно-ориентированного программирования, который позволяет наследовать функциональность одного класса или базового класса (base class) в другом - производном классе (derived class).

Зачем нужно наследование? Рассмотрим небольшую ситуацию, допустим, у нас есть классы, которые представляют человека и работника предприятия:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Person
{
public:
std::string name; // имя
int age; // возраст
void display()
{
std::cout << "Name: " << name << "\tAge: " << age << std::endl;
}
};
class Employee
{
public:
std::string name; // имя
int age; // возраст
std::string company; // компания
void display()
{
std::cout << "Name: " << name << "\tAge: " << age << std::endl;
}
};
В данном случае класс Employee фактически содержит функционал класса Person: свойства name и age и функцию display. И было бы не совсем правильно повторять функциональность одного класса в другом классе, тем более что по сути сотрудник предприятия в любом случае является человеком. Поэтому в этом случае лучше использовать механизм наследования. Унаследуем класс Employee от класса Person:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Person
{
public:
std::string name; // имя
int age; // возраст
void display()
{
std::cout << "Name: " << name << "\tAge: " << age << std::endl;
}
};
class Employee : public Person
{
public:
std::string company; // компания
};
Для установки отношения наследования после название класса ставится двоеточие, затем идет название класса, от которого мы хотим унаследовать функциональность. В этом отношении класс Person еще будет называться базовым классом, а Employee - производным классом.

Перед названием базового класса также можно указать спецификатор доступа, как в данном случае используется спецификатор public, который позволяет использовать в производном классе все открытые члены базового класса. Если мы не используем модификатор доступа, то класс Employee ничего не будет знать о переменных name и age и функции display.

После установки наследования мы можем убрать из класса Employee те переменные, которые уже определены в классе Person. Используем оба класса:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>
#include <string>

class Person
{
public:
std::string name; // имя
int age; // возраст
void display()
{
std::cout << "Name: " << name << "\tAge: " << age << std::endl;
}
};
class Employee : public Person
{
public:
std::string company; // компания
};

int main()
{
Person tom;
tom.name = "Tom";
tom.age = 23;
tom.display();

Employee bob;
bob.name = "Bob";
bob.age = 31;
bob.company = "Microsoft";
bob.display();

return 0;
}
Таким образом, через переменную класса Employee мы можем обращаться ко всем открытым членам класса Person.

Конструкторы
Стоит учитывать, что конструкторы при наследовании не наследуются. И если базовый класс содержит только конструкторы с параметрами, то производный класс должен вызывать в своем конструкторе один из конструкторов базового класса. Например, добавим в классы выше конструкторы:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <iostream>
#include <string>

class Person
{
public:
Person(std::string n, int a)
{
name = n; age = a;
}
void display()
{
std::cout << "Name: " << name << "\tAge: " << age << std::endl;
}
private:
std::string name;
int age;
};
class Employee : public Person
{
public:
Employee(std::string n, int a, std::string c):Person(n, a)
{
company = c;
}
private:
std::string company;
};

int main()
{
Person tom("Tom", 23);
tom.display();

Employee bob("Bob", 31, "Microsoft");
bob.display();

return 0;
}
После списка параметров конструктора производного класса через двоеточие идет вызов конструктора базового класса, в который передаются значения параметров n и a.

1
2
3
4
Employee(std::string n, int a, std::string c):Person(n, a)
{
company = c;
}
Если бы мы не вызвали конструктор базового класса, то это было бы ошибкой.

Консольный вывод программы:

Name: Tom Age: 23
Name: Bob Age: 31
Таким образом, в строке

1
Employee bob("Bob", 31, "Microsoft");
Вначале будет вызываться конструктор базового класса Person, в который будут передаваться значения "Bob" и 31. И таким образом будут установлены имя и возраст. Затем будет выполняться собственно конструктор Employee, который установит компанию.

Также мы могли бы определить конструктор Employee следующим обазом:

1
2
3
Employee(std::string n, int a, std::string c):Person(n, a), company(c)
{
}
Также в примере выше стои отметить, что переменные в обоих классах стали закрытыми, то есть они объявлены со спецификатором private. Производный класс не может обращаться к закрытым членам базового класса. Поэтому, если бы мы попробовали обратиться к закрытым переменным класса Person через переменную класса Employee, то мы бы получили ошибку:

1
2
3
Employee bob("Bob", 31, "Microsoft");
bob.name = "Bobby"; // ошибка
bob.age = 23; // ошибка
Спецификатор protected
С помощью спецификатора public можно определить общедоступные открытые члены классы, которые доступны извне и их можно использовать в любой части программы. С помощью спецификатора private можно определить закрытые переменные и функции, которые можно использовать только внутри своего класса. Однако иногда возникает необходимость в таких переменных и методах, которые были бы доступны классам-наследникам, но не были бы доступны извне. И именно для определения уровня доступа подобных членов класса используется спецификатор protected.

Например, определим переменную name со спецификатором protected:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <iostream>
#include <string>

class Person
{
public:
Person(std::string n, int a)
{
name = n; age = a;
}
void display()
{
std::cout << "Name: " << name << "\tAge: " << age << std::endl;
}
protected:
std::string name;
private:
int age;
};
class Employee : public Person
{
public:
Employee(std::string n, int a, std::string c):Person(n, a)
{
company = c;
}
void showEmployeeName()
{
std::cout << "Employee Name: " << name << std::endl;
}
private:
std::string company;
};

int main()
{
Person tom("Tom", 23);
// tom.name = "Tommy"; ошибка
Employee bob("Bob", 31, "Microsoft");
// bob.name = "Bob Tompson"; ошибка
bob.showEmployeeName();

return 0;
}
Таким образом, мы можем использовать переменную name в производном классе, например, в методе showEmployeeName, но извне мы к ней обратиться по-прежнему не можем.

Запрет наследования
Иногда наследование от класса может быть нежелательно. И с помощью спецификатора final мы можем запретить наследование:

1
2
3
class User final
{
};
После этого мы не сможем унаследовать другие классы от класса User. И, например, если мы попробуем написать, как в случае ниже, то мы столкнемся с ошибкой:

1
2
3
class VipUser : public User
{
};