Как переопределить виртуальную функцию c
Перейти к содержимому

Как переопределить виртуальную функцию c

  • автор:

Виртуальные функции

Виртуальные функции — специальный вид функций-членов класса. Виртуальная функция отличается об обычной функции тем, что для обычной функции связывание вызова функции с ее определением осуществляется на этапе компиляции. Для виртуальных функций это происходит во время выполнения программы.

Для объявления виртуальной функции используется ключевое слово virtual . Функция-член класса может быть объявлена как виртуальная, если

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

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

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

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

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

#include
using namespace std;
class X
protected :
int i;
public :
void seti( int c) < i = c; >
virtual void print() < cout
>;
class Y : public X // наследование
public :
void print() < cout // переопределение базовой функции
>;
int main()
X x;
X *px = &x; // Указатель на базовый класс
Y y;
x.seti(10);
y.seti(15);
px->print(); // класс X: 10
px = &y;
px->print(); // класс Y: 15
cin.get();
return 0;
>

Виртуальная функция

Результат выполнения

В каждом случае выполняется различная версия функции print() . Выбор динамически зависит от объекта, на который ссылается указатель.

Без виртуальной функции

Если в строке 9 (см. код выше) убрать ключевое слово virtual , то результат выполнения будет уже другим, т.к. связывание функций будет происходить на этапе компиляции:

В терминологии ООП «объект посылает сообщение print и выбирает свою собственную версию соответствующего метода». Виртуальной может быть только нестатическая функция-член класса. Для порожденного класса функция автоматически становится виртуальной, поэтому ключевое слово virtual можно опустить.

Пример : выбор виртуальной функции

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

#include
using namespace std;
class figure
protected :
double x, y;
public :
figure( double a = 0, double b = 0) < x = a; y = b; >
virtual double area() < return (0); >// по умолчанию
>;
class rectangle : public figure
public :
rectangle( double a = 0, double b = 0) : figure(a, b) <>;
double area() < return (x*y); >
>;
class circle : public figure
public :
circle( double a = 0) : figure(a, 0) <>;
double area() < return (3.1415*x*x); >
>;
int main()
figure *f[2];
rectangle rect(3, 4);
circle cir(2);
double total = 0;
f[0] = ▭
f[1] = ○
total = f[1]->area();
cout total += f[0]->area();
cout cin.get();
return 0;
>

Переопределение виртуальной функции

Результат выполнения

Чистая виртуальная функция

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

Чистая виртуальная функция — это метод класса, тело которого не определено.

В базовом классе такая функция записывается следующим образом:

virtual ПрототипФункции = 0;
virtual void func() = 0;

Чистая виртуальные функции используются для того, чтобы отложить решение задачи о реализации функции на более поздний срок. В терминологии ООП это называется отсроченным методом . Класс, имеющий по крайней мере одну чистую виртуальную функцию, называется абстрактным базовым классом . Для иерархии типа полезно иметь абстрактный базовый класс. Он содержит общие свойства иерархии типа, но каждый порожденный класс реализует эти свойства по-своему.

Для рассмотренного выше примера (класс Фигура) функцию вычисления площади целесообразно задать чистой виртуальной функцией, которую переопределяет каждый наследуемый класс.
Строка 9 при этом будет иметь вид:

virtual double area() = 0;

Комментариев к записи: 4

Спасибо очень, чётко и просто объяснили в отличие от других сайтов, где авторы статей, словно школьники, которых поймали, когда те курили за гаражами, уходящие от ответа на вопрос: «Зачем?».

Нормальная статья. Вирутуальная функция — это просто указатель на функцию, которую каждый класс потомок может изменить на свою. Это если бы мы писали на С

Fruit Ninja Online

Виртуальные методы — один из важнейших приёмов реализации полиморфизма . Они позволяют создавать общий код, который может работать как с объектами базового класса, так и с объектами любого его класса-наследника. При этом базовый класс определяет способ работы с объектами и любые его наследники могут предоставлять конкретную реализацию этого способа.

Сюда же можно было бы про виртуальный деструктор написать, я думаю. Статья годная, первый пример не очень удачный. С абстрактными классами можно было бы связать второй пример. У Вас там как раз описывается некая «фигура», т.е. тоже абстрактное понятие, для которого нельзя описать метод вычисления площади или чего-то там. Это был бы удачный пример, т.к. это:

virtual double area() < return (0);>// по умолчанию

не совсем верно. Формально, фигура — это множество точек и оно может не иметь площади, возвращать ноль — не правильно. Описать чисто виртуальный метод — правильно. Термин «отсроченный метод» какой-то странный. Он встречается в классических книгах?

Как переопределить виртуальную функцию c

При вызове функции программа должна определять, с какой именно реализацией функции соотносить этот вызов, то есть связать вызов функции с самой функцией. В С++ есть два типа связывания — статическое и динамическое.

Когда вызовы функций фиксируются до выполнения программы на этапе компиляции, это называется статическим связыванием (static binding), либо ранним связыванием (early binding). При этом вызов функции через указатель определяется исключительно типом указателя, а не объектом, на который он указывает. Например:

#include class Person < public: Person(std::string name): name < >void print() const < std::cout private: std::string name; // имя >; class Employee: public Person < public: Employee(std::string name, std::string company): Person, company  < >void print() const < Person::print(); std::cout private: std::string company; // компания >; int main() < Person tom ; Person* person ; person->print(); // Name: Tom Employee bob ; person = &bob; person->print(); // Name: Bob >

В данном случае класс Employee наследуется от класса Person, но оба этих класса определяют функцию print() , которая выводит данные об объекте. В функции main создаем два объекта и поочередно присваиваем их указателю на тип Person и вызываем через этот указатель функцию print. Однако даже если этому указателю присваивается адрес объекта Employee, то все равно вызывает реализация функции из класса Person:

Employee bob ; person = &bob; person->print(); // Name: Bob

То есть выбор реализации функции определяется не типом объекта, а типом указателя. Консольный вывод программы:

Name: Tom Name: Bob

Динамическое связывание и виртуальные функции

Другой тип связывания представляет динамическое связывание (dynamic binding), еще называют поздним связыванием (late binding), которое позволяет на этапе выполнения решать, функцию какого типа вызвать. Для этого в языке С++ применяют виртуальные функции . Для определения виртуальной функции в базовом классе функция определяется с ключевым словом virtual . Причем данное ключевое слово можно применить к функции, если она определена внутри класса. А производный класс может переопределить ее поведение.

Итак, сделаем функцию print в базовом классе Person виртуальной:

#include class Person < public: Person(std::string name): name < >virtual void print() const // виртуальная функция < std::cout private: std::string name; >; class Employee: public Person < public: Employee(std::string name, std::string company): Person, company  < >void print() const < Person::print(); std::cout private: std::string company; >; int main() < Person tom ; Person* person ; person->print(); // Name: Tom Employee bob ; person = &bob; person->print(); // Name: Bob // Works in Microsoft >

Таким образом, базовый класс Person определяет виртуальную функцию print, а производный класс Employee переопределяет ее. В первом же примере, где функция print не была виртуальной, класс Employee не переопределял, а скрывал ее. Теперь при вызове функции print для объекта Employee через указатель Person* будет вызываться реализация функции именно класса Employee. Соответственно тепепрь мы получим другой консольный вывод:

Name: Tom Name: Bob Works in Microsoft

В этом и состоит отличие переопределения виртуальных функций от скрытия.

Класс, который определяет или наследует виртуальную функцию, еще назвается полиморфным (polymorphic class). То есть в данном случае Person и Employee являются полиморфными классами.

Стоит отметить, что вызов виртуальной функции через имя объекта всегда разрешается статически.

Employee bob ; Person p = bob; p.print(); // Name: Bob - статическое связывание

Динамическое связывание возможно только через указатель или ссылку.

Employee bob ; Person &p ; // присвоение ссылке p.print(); // динамическое связывание Person *ptr ; // присвоение адреса указателю ptr->print(); // динамическое связывание

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

Также статические функции не могут быть виртуальными.

Ключевое слово override

Чтобы явным образом указать, что мы хотим переопредлить функцию, а не скрыть ее, в производном классе после списка параметров функции указывается слово override

#include class Person < public: Person(std::string name): name < >virtual void print() const // виртуальная функция < std::cout private: std::string name; >; class Employee: public Person < public: Employee(std::string name, std::string company): Person, company  < >void print() const override // явным образом указываем, что функция переопределена < Person::print(); std::cout private: std::string company; >; int main() < Person tom ; Person* person ; person->print(); // Name: Tom Employee bob ; person = &bob; person->print(); // Name: Bob // Works in Microsoft >

То есть здесь выражение

void print() const override

указывает, что мы явным образом хотим переопределить функцию print. Однако может возникнуть вопрос: в предыдущем примере мы не указывали override для вирутальной функции, но переопределение все равно работало, зачем же тогда нужен override ? Дело в том, что override явным образом указывает компилятору, что это переопределяемая функция. И если она не соответствует виртуальной функции в базовом классе по списку параметров, возвращаемому типу, константности, или в базовом классе вообще нет функции с таким именем, то компилятор при компиляции сгенерирует ошибку. И по ошибке мы увидим, что с нашей переопределенной функцией что-то не так. Если же override не указать, то компилятор будет считать, что речь идет о скрытии функции, и никаких ошибок не будет генерировать, компиляция пройдет успешно. Поэтмоу при переопределении виртуальной функции в производном классе лучше указывать слово override

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

Принцип выполнения виртуальных функций

Стоит отметить, что виртальные функции имеют свою цены — объекты классов с виртуальными функциями требуют немного больше памяти и немного больше времени для выполнения. Поскольку при создании объекта полиморфного класса (который имеет виртуальные функции) в объекте создается специальный указатель. Этот указатель используется для вызова любой виртуальной функции в объекте. Специальный указатель указывает на таблицу указателей функций, которая создается для класса. Эта таблица, называемая виртуальной таблицей или vtable, содержит по одной записи для каждой виртуальной функции в классе.

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

vtable, полиформизм и виртуальные функции в C++

  1. Указатель на vtable в объекте используется для поиска адреса vtable для класса.
  2. Затем в таблице идет поиск указателя на вызываемую виртуальную функцию.
  3. Через найденный указатель функции в vtable вызывается сама функция. В итоге вызов виртуальной функции происходит немного медленнее, чем прямой вызов невиртуальной функции, поэтому каждое объявление и вызов виртуальной функции несет некоторые накладные расходы.

Запрет переопределения

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

class Person < public: virtual void print() const final < >>; class Employee : public Person < public: void print() const override // Ошибка. < >>;

Также можно переопределить функцию базового класса, но запретить ее переопределение в дальнейших производных классах:

class Person < public: virtual void print() const // переопределение разрешено < >>; class Employee : public Person < public: void print() const override final // в классах, производных от Employee переопределение запрещено < >>;

Наследование — обязательно ли переопределять все виртуальные функции?

Вопрос в следующем: как сделать так, чтобы функции не приходилось переопределять в дочернем классе, но при этом сохранилось родительское поведение в случае, если функция не переопределена?
Т.е. мне нужно, чтобы было возможно, не трогая родительский класс, переопределить функцию в дочернем, а до тех пор, пока функция не переопределена, выполнялась родительская. Или добавить в родительскую функцию, которая выполнялась бы во всех дочерних без изменений в них. Это возможно?

Лекция. Виртуальные функции и полиморфизм

Для примеров будем использовать классы из предыдущей лекции. Следует напомнить, что у нас был класс Person, от которого был унаследован класс Student. Рассмотрим следующий пример:

Student s; Person &p = s; s.name(); //Student::name() p.name(); //Person::name()

В 3-й строке вызовется метод класса Student, т.к. s является объектом этого класса. Однако, в строке 4 вызовется метод name базового класса Person, хотя по логике следовало бы тоже ожидать вызов name() класса Student — ведь p — это ссылка на объект производного класса.

Возможность вызова методов производного класса через ссылку или указатель на базовый класс осуществляется с помощью механизма виртуальных функций. Чтобы при вызове p.name() вызвался метод класса Student, реализуем классы следующим образом:

struct Person < virtual string name() const; >; struct Student: Person < string name() const; >;

Перед методом name класса Person мы указали ключевое слово virtual, которое указывает, что метод является виртуальным. Теперь при вызове p.name() произойдет вызов метода класса Student, несмотря на то, что мы его вызываем через ссылку на базовый класс Person. Аналогичная ситуация и с указателями:

Student s; Person *p = &s ; p->name(); //вызовется Student::name(); Person n; p = &n; p->name(); //вызовется Person::name()

Если с некоторого класса в иерархии наследования метод стал виртуальным, то во всех производных от него классах он будет виртуальным, вне зависимости от того, указано ли ключевое слово virtual в классах наследниках.

Механизм виртуальных функций реализует полиморфизм времени выполнения: какой виртуальный метод вызовется будет известно только во время выполнения программы.

В качестве следующего примера можно рассмотреть класс TextFile, от которого наследуются два класса: GZippedTextFile и BZippedTextFile. Базовый класс имеет два метода: name(), возвращающий имя файла, и read(), считывающий данные из файла. В этом случае виртуальным имеет смысл сделать только метод read, т.к. у каждого типа сжатого файла будет свой способ считывания данных:

struct TextFile < string name() const; virtual string read(size_t count); //. >; struct GZippedTextFile : TextFile < string read(size_t count); //. >struct BZippedTextFile : TextFile < string read(size_t count); //. >

Перекрытие методов

Рассмотрим класс A, у которого имеется метод f(int), и класс B, унаследованный от A, у которого есть метод f(long):

struct A < void f(int); >; struct B : A < void f(long); >;

В следующем коде:

B b; b.f(1);

произойдет вызов метода f(long) класса B, несмотря на то, что у родительского класса A есть более подходящий метод f(int). Оказывается, что метод f(int) родительского класса A перекрылся. Для того, чтобы в примере вызвался метод f(int), следует добавить строку using A::f; в определении класса B:

struct B : A < using A::f; void f(long); >;

Абстрактные классы и чистые виртуальные функции

Расширим пример текстового файла. Предположим, что нам нужно сделать для класса TextFile базовый класс File, от которого будет унаследован еще один класс RTFFile. Однако, в такой ситуации неизвестно как реализовать метод read() класса File, т.к. класс File не реализует поведение какого-то конкретного типа файлов, а представляет интерфейс для работы с различными файлами. В этом случае, метод read(. ) этого класса нужно сделать чистым виртуальным, дописав «= 0» после его сигнатуры:

struct File < virtual string read(size_t count) = 0; >;

Это означает, что метод read(. ) должен быть определен в классах наследниках. Теперь класс File стал абстрактным, и его экземпляры невозможно создать. Но зато можно работать через указатель на абстрактный класс с объектами производных классов, например, так:

File *f = new TextFile("text.txt"); //различные действия с файлом text.txt delete f; f = new RTFFile("rich_text.rtf"); //различные действия с файлом rich_text.rtf delete f;

Следует отметить, что в любой иерархии классов деструктор всегда должен быть виртуальным. Рассмотрим пример, поясняющий важность этого факта:

struct Person < public: ~Person() <>private: string name; //. >; struct Student : Person < public: Student() < someData = new Data(); >~Student() < delete someData; >//. private: Data *someData; >; //. Student *s = new Student(); //. delete s; //вызовется деструктор класса Student, память по указателю someData освободится Person *p = new Student(); //. delete p; /*вызовется деструктор класса Person, а не Student, т.к. он не является виртуальным, несмотря на то, что на самом деле объект - экземпляр Student. В этом случае произойдет утечка памяти, т.к. память по указателю someData не освободится */

Деструктор можно также сделать чистым виртуальным, но при этом его тело нужно определить снаружи класса.

Таблица виртуальных функций (Virtual Function Table)

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

Как известно, конструирование объекта происходит поэтапно и начинается созданием объекта самого первого класса в иерархии наследования. Во время этого процесса перед вызовом конструктора каждого класса указатель на VFT устанавливается равным указателю на VFT текущего конструируемого класса. Например, у нас есть 3 класса: A, B, C (B наследуется от A, C наследуется от B). При создании экземпляра С, произойдут 3 последовательных вызова конструкторов: сначала A(), затем B(), и в конце C(). Перед вызовом конструктора A() указатель на VFT будет указывать на таблицу класса A, перед вызовом B() он станет указывать на таблицу класса B() и т.д. Аналогичная ситуация при вызове деструкторов, только указатель будет менятся от таблицы самого младшего класса к самому старшему.

Из этого факта следует правило: в конструкторах и деструкторах нельзя вызывать виртуальные методы. Посмотрим, что произойдет, если нарушить это правило:

struct A < A() < f(); >virtual void f() < cout >; struct B : A < B() < f(); >virtual void f() < cout >; struct C : B < C() < f(); >virtual void f() < cout >; //. C c; //создание объекта класса C

В этом примере на экран будет выведено:

A::f() B::f() C::f()

Модификаторы доступа

  • public — доступ для всех
  • protected — доступ только для самого класса и его наследников
  • private — доступ только для самого класса
struct B : public A ; struct C : protected A ; struct D : private A ;

Если модификатор не указан, то по умолчанию для структур наследование public, для классов private.

public-наследование позволяет установить между классами отношение ЯВЛЯЕТСЯ. Т.е., если класс B открыто унаследован от A, то объект класса B ЯВЛЯЕТСЯ объектом класса A, но не наоборот.

private наследование выражает отношение РЕАЛИЗОВАНО_ПОСРЕДСТВОМ. Если класс B закрыто унаследован от A, то можно говорить, что объект класса B реализован посредством объекта класса A. В большинстве случаев закрытое наследование можно заменить агрегацией.

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

Ковариантность

Пусть имеется класс A, и класс B — его наследник. Тогда возможно определить классы C и D, с виртуальным методом f() следующим образом:

struct A ; struct B : A ; struct C < virtual A * f(); >; struct D : C

Такая возможность в C++ называет ковариантностью по типу возвращаемого значения (return type covariance).

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *