Виртуальные функции и их переопределение


В прошлой теме были определены следующие классы Person и Employee:

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
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;
}
private:
std::string company;
};
Класс Employee наследует функцию display и может использовать ее для вывода информации о сотруднике. Однако реализация этой функции в классе Person выводит на консоль только имя и возраст. Тогда как в классе Employee определена дополнительная переменная - company, которая хранит компанию, в которой работает объект Employee. И было бы хорошо, если бы для объекта Employee функция display также выводила бы значения переменной company.

Мы можем решить эту проблему с помощью виртуальных функций. То есть чтобы в производном классе можно было бы переопределить функцию, в базовом классе подобная функция должна быть объявлена с ключевым словом virtual:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person
{
public:
Person(std::string n, int a)
{
name = n; age = a;
}
virtual void display()
{
std::cout << "Name: " << name << "\tAge: " << age << std::endl;
}
protected:
std::string name;
private:
int age;
};
Теперь функция display может быть переопределена в производных классах. Причем производный класс может и не переопределять функцию, а использовать унаследованный функционал.

Для переопределения виртуальной функции в производном классе она должна быть определена с ключевым словом override, которое помещается после списка параметров функции:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Employee : public Person
{
public:
Employee(std::string n, int a, std::string c):Person(n, a)
{
company = c;
}
void display() override
{
std::cout << "Name:" << name << "\tCompany: " << company << std::endl;
}
private:
std::string company;
};
Класс, который определяет или наследует виртуальную функцию, еще назвается полиморфным (polymorphic class). То есть в данном случае Person и Employee являются полиморфными классами.

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

Обращение к базовому классу
Теперь в классе Employee функция display выводит имя и компанию сотрудника. Однако функция display в классе Person выводит еще и возраст человека. Но поскольку переменная age в классе Person определена как закрытая, то класс Employee не имеет к ней доступа.

Мы могли бы в данном случае определить переменную age как защищенную, то есть со спецификатором protected, однако есть и другой способ - мы можем просто обратиться к реализации функции базового класса. Применим данный способ:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Employee : public Person
{
public:
Employee(std::string n, int a, std::string c):Person(n, a)
{
company = c;
}
void display() override
{
Person::display();
std::cout << "Company: " << company << std::endl;
}
private:
std::string company;
};
С помощью вызова Person::display(); будет выполняться реализация функции display из класса 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
40
41
42
43
44
#include <iostream>
#include <string>

class Person
{
public:
Person(std::string n, int a)
{
name = n; age = a;
}
virtual 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 display() override
{
Person::display();
std::cout << "Company: " << company << std::endl;
}
private:
std::string company;
};

int main()
{
Person tom("Tom", 23);
tom.display();
Employee bob("Bob", 31, "Microsoft");
bob.display();

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

Name: Tom Age: 23
Name: Bob Age: 31
Company: Microsoft
Также мы можем вызывать реализацию из базового класса через переменную производного класса:

1
2
Employee bob("Bob", 31, "Microsoft");
bob.Person::display();
Сокрытие функций
Производный класс может определить функцию с тем же именем, что и виртуальная функция в базовом классе, с тем же или другим списком параметров. Для компилятора такая функция будет существовать независимо от базового класса. И подобное определение функции в производном классе не будет переопределением функции из базового класса.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Employee : public Person
{
public:
Employee(std::string n, int a, std::string c) :Person(n, a)
{
company = c;
}
void display()
{
std::cout << company << std::endl;
}
private:
std::string company;
};
В данном случае функция display представляет совершенно новую реализацию, которая не имеет ничего общего с базовым классом Person. Она скрывает по сути виртуальную функцию из базового класса.

Нередко подобные функции являются результатом невнимательности - хотели переопределить функцию базового класса, но забыли поставить спецификатор override. В то же время ошибки в данном случае не будет. И мы также сможем использовать данную функцию:

1
2
Employee bob("Bob", 31, "Microsoft");
bob.display();
Запрет переопределения
С помощью спецификатора final мы можем запретить определение в производных классах функций, которые имеют то же самое имя, возвращаемый тип и список параметров, что и виртуальная функция в базовом классе. Например:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person
{
public:
virtual void display() final
{

}
};
class Employee : public Person
{
public:
void display() // Ошибка!!!
{

}
};