зачем нужны виртуальные функции

Урок №163. Виртуальные функции и Полиморфизм

Обновл. 15 Сен 2021 |

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

Виртуальные функции и Полиморфизм

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

rParent is a Parent

На этом уроке мы рассмотрим, как можно решить эту проблему с помощью виртуальных функций.

Виртуальная функция в языке С++ — это особый тип функции, которая, при её вызове, выполняет «наиболее» дочерний метод, который существует между родительским и дочерними классами. Это свойство еще известно, как полиморфизм. Дочерний метод вызывается тогда, когда совпадает сигнатура (имя, типы параметров и является ли метод константным) и тип возврата дочернего метода с сигнатурой и типом возврата метода родительского класса. Такие методы называются переопределениями (или «переопределенными методами»).

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

rParent is a Child

Рассмотрим пример посложнее:

Как вы думаете, какой результат выполнения этой программы?

Рассмотрим всё по порядку:

Сначала создается объект c класса C.

Вызов rParent.GetName() приводит к вызову A::getName(). Однако, поскольку A::getName() является виртуальной функцией, то компилятор ищет «наиболее» дочерний метод между A и C. В этом случае — это C::getName().

Обратите внимание, компилятор не будет вызывать D::getName(), поскольку наш исходный объект был класса C, а не класса D, поэтому рассматриваются методы только между классами A и C.

Результат выполнения программы:

Более сложный пример

Рассмотрим класс Animal из предыдущего урока, добавив тестовый код:

Результат выполнения программы:

А теперь рассмотрим тот же класс, но сделав метод speak() виртуальным:

Результат выполнения программы:

Matros says Meow
Barsik says Woof

Обратите внимание, мы не сделали Animal::GetName() виртуальной функцией. Это из-за того, что GetName() никогда не переопределяется ни в одном из дочерних классов, поэтому в этом нет необходимости.

Аналогично со следующим примером с массивом животных:

Matros says Meow
Barsik says Woof
Ivan says Meow
Tolik says Woof
Martun says Meow
Tyzik says Woof

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

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

Использование ключевого слова virtual

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

Типы возврата виртуальных функций

Типы возврата виртуальной функции и её переопределений должны совпадать. Рассмотрим следующий пример:

В этом случае Child::getValue() не считается подходящим переопределением для Parent::getValue(), так как типы возвратов разные (метод Child::getValue() считается полностью отдельной функцией).

Не вызывайте виртуальные функции в теле конструкторов или деструкторов

Вот еще одна ловушка для новичков. Вы не должны вызывать виртуальные функции в теле конструкторов или деструкторов. Почему?

Помните, что при создании объекта класса Child сначала создается родительская часть этого объекта, а затем уже дочерняя? Если вы будете вызывать виртуальную функцию из конструктора класса Parent при том, что дочерняя часть создаваемого объекта еще не была создана, то вызвать дочерний метод вместо родительского будет невозможно, так как объект child для работы с методом класса Child еще не будет создан. В таких случаях, в языке C++ будет вызываться родительская версия метода.

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

Правило: Никогда не вызывайте виртуальные функции в теле конструкторов или деструкторов.

Недостаток виртуальных функций

«Если всё так хорошо с виртуальными функциями, то почему бы не сделать все методы виртуальными?» — спросите Вы. Ответ: «Это неэффективно!». Обработка и выполнение вызова виртуального метода занимает больше времени, чем обработка и выполнение вызова обычного метода. Кроме того, компилятор также должен выделять один дополнительный указатель для каждого объекта класса, который имеет одну или несколько виртуальных функций.

Какой результат выполнения следующих программ? Не нужно запускать/выполнять следующий код, вы должны определить результат, без помощи своих IDE.

Источник

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

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

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

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

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

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

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

В следующем примере показано поведение виртуальной и невиртуальной функций при вызове с помощью указателей.

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

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

Оба вызова PrintBalance в предыдущем примере подавляют механизм вызова виртуальных функций.

Источник

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

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

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

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

Результат выполнения
зачем нужны виртуальные функции. 2018 11 04 12 31 05. зачем нужны виртуальные функции фото. зачем нужны виртуальные функции-2018 11 04 12 31 05. картинка зачем нужны виртуальные функции. картинка 2018 11 04 12 31 05.

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

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

Результат выполнения
зачем нужны виртуальные функции. 2018 11 04 12 40 42. зачем нужны виртуальные функции фото. зачем нужны виртуальные функции-2018 11 04 12 40 42. картинка зачем нужны виртуальные функции. картинка 2018 11 04 12 40 42.

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

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

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

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

Источник

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

В этом руководстве мы узнаем о виртуальной функции C++ и ее использовании с помощью примеров.

Что такое виртуальная функция в C++?

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

Виртуальную функцию в C++ используют в базовом классе, чтобы гарантировать, что функция переопределена. Это относится к случаям, когда указатель базового класса указывает на объект производного класса.

Например, рассмотрим код ниже:

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

Другими словами, функция-член Base не переопределяется.

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

Виртуальные функции являются неотъемлемой частью полиморфизма в С++.

Пример 1

Здесь мы объявили функцию print() в Base, как виртуальную.

зачем нужны виртуальные функции. img 36. зачем нужны виртуальные функции фото. зачем нужны виртуальные функции-img 36. картинка зачем нужны виртуальные функции. картинка img 36.

Идентификатор переопределения

C++ 11 дал нам новое переопределение идентификатора, которое очень полезно, во избежание ошибок при использовании виртуальных функций.

Этот идентификатор определяет функции-члены производных классов, которые переопределяют функцию-член базового класса.

Если мы используем прототип функции в классе Derived и определяем эту функцию вне класса, то мы используем следующий код:

Использование переопределения

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

Использование идентификатора переопределения заставляет компилятор отображать сообщения об ошибках, когда эти ошибки сделаны.

В противном случае программа просто скомпилируется, но виртуальная функция не будет отменена.

Вот некоторые из этих возможных ошибок:

Использование функции

У нас есть базовый класс Animal и производные классы Dog и Cat.

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

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

Или мы могли бы сделать getType() виртуальным в классе Animal, а затем создать одну отдельную функцию print(), которая принимает указатель типа Animal в качестве аргумента. Затем мы можем использовать эту единственную функцию, чтобы переопределить виртуальную.

Это сделает код короче, чище и менее повторяющимся.

Пример 2: демонстрация

В main() мы создали 3 указателя Animal для динамического создания объектов классов Animal, Dog и Cat.

Затем мы вызываем функцию print(), используя эти указатели:

Источник

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

Когда-то давным давно я собирался и даже обещал написать про механизм виртуальных функций относительно деструкторов. Теперь у меня наконец появилось свободное время и я решил воплотить эту затею в жизнь. На самом деле эта мини-статья служит «прологом» к моей следующей статье. Но я постарался изложить доходчиво и понятно основные моменты по текущей теме. Если вы чувствуете, что еще недостаточно разобрались в механизме виртуальных вызовов, то, возможно, вам следует для начала прочитать мою предыдущую статью.

Сразу же, как обычно, оговорюсь, что: 1) статья моя не претендует на полноту изложения материала; 2) мегапрограммеры ничего нового здесь не узнают; 3) материал не новый и давно описан во многих книгах, но если явно об этом не прочитать и самому специально не задумываться, то можно о некоторых моментах даже не подозревать (до поры, до времени). Также прошу прощения за надуманные примеры 🙂

Виртуальные деструкторы

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

Основное правило: если у вас в классе присутствует хотя бы одна виртуальная функция, деструктор также следует сделать виртуальным. При этом не следует забывать, что деструктор по умолчанию виртуальным не будует, поэтому следует объявить его явно. Если этого не сделать, у вас в программе почти наверняка будут утечки памяти (memory leaks). Чтобы понять почему, опять же много ума не надо. Рассмотрим несколько примеров.

В первом случае создадим объект производного класса в стеке:

using std :: cout ;
using std :: endl ;

Всем ясно, что вывод программы будет следующим:

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

Попробуем теперь создать тот же объект в динамической памяти, используя при этом указатель на объект базового класса (код классов не изменился, поэтому привожу только код функции main()):

int main ( )
<
A * pA = new B ;
delete pA ;
return EXIT_SUCCESS ;
>

На сей раз конструируется объект так, как и надо, а при разрушении происходит утечка памяти, потому как деструктор производного класса не вызывается:

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

Чтобы этого избежать, деструктор в базовом классе должен быть объявлен как виртуальный:

using std :: cout ;
using std :: endl ;

int main ( )
<
A * pA = new B ;
delete pA ;
return EXIT_SUCCESS ;
>

Теперь-то мы получим желаемый порядок вызовов:

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

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

Давайте для начала рассмотрим ситуацию с вызовом виртуальных функций внутри класса. Предположим, что у нас есть Кот, который просит покушать мяуканьем, а затем приступает к процессу 🙂 Так поступают многие коты, но не Чеширский! Чеширский, как известно, мало того что вечно улыбается, так еще и довольно разговорчив, поэтому мы научим его говорить, переопределив метод speak():

using std :: cout ;
using std :: endl ;

class Cat
<
public :
void askForFood ( ) const
<
speak ( ) ;
eat ( ) ;
>
virtual void speak ( ) const < cout "Meow! " ; >
virtual void eat ( ) const < cout "*champing*" endl ; >
> ;

class CheshireCat : public Cat
<
public :
virtual void speak ( ) const < cout "WTF?! Where \' s my milk? =) " ; >
> ;

delete cats [ 0 ] ; delete cats [ 1 ] ;
return EXIT_SUCCESS ;
>

Вывод этой программы будет следующим:

Ordinary Cat: Meow! *champing*
Cheshire Cat: WTF?! Where’s my milk? =) *champing*

Рассмотрим код более подробно. Есть класс Cat с парой виртуальных методов, один из которых переопределен в производном CheshireCat. Но всё самое интересное происходит в методе askForFood() класса Cat.

Как видно, метод всего лишь содержит вызовы двух других методов, однако конструкция speak() в данном контексте эквивалента this->speak(), то есть вызов происходит через указатель, а значит — будет использовано позднее связывание. Вот почему при вызове метода askForFood() через указатель на CheshireCat мы видим то, что и хотели: механизм виртуальных функций работает исправно даже несмотря на то, что вызов непосредственно виртуального метода происходит внутри другого метода класса.

А теперь самое интересное: что будет, если попытаться воспользоваться этим в деструкторе? Модернизируем код так, чтобы при деструкции наши питомцы прощались, кто как умеет:

using std :: cout ;
using std :: endl ;

class Cat
<
public :
virtual

Cat ( ) < sayGoodbye ( ) ; >
void askForFood ( ) const
<
speak ( ) ;
eat ( ) ;
>
virtual void speak ( ) const < cout "Meow! " ; >
virtual void eat ( ) const < cout "*champing*" endl ; >
virtual void sayGoodbye ( ) const < cout "Meow-meow!" endl ; >
> ;

class CheshireCat : public Cat
<
public :
virtual void speak ( ) const < cout "WTF?! Where \' s my milk? =) " ; >
virtual void sayGoodbye ( ) const < cout "Bye-bye! (:" endl ; >
> ;

delete cats [ 0 ] ; delete cats [ 1 ] ;
return EXIT_SUCCESS ;
>

Можно ожидать, что, как и в случае с вызовом метода speak(), будет выполнено позднее связывание, однако это не так:

Ordinary Cat: Meow! *champing*
Cheshire Cat: WTF?! Where’s my milk? =) *champing*
Meow-meow!
Meow-meow!

Почему? Да потому что при вызове виртуальных методов из деструктора компилятор использует не позднее, а раннее связывание. Если подумать, зачем он делает именно так, всё становится очевидным: нужно просто рассмотреть порядок конструирования и разрушения объектов. Все помнят, что конструирование объекта происходит, начиная с базового класса, а разрушение идет в строго обратном порядке. Таким образом, когда мы создаем объект типа CheshireCat, порядок вызовов конструкторов/деструкторов будет таким:

Если же мы захотим внутри деструктора

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

Мораль: если в вашей голове витают помыслы выделить какой-то алгоритм «зачистки» в отдельный метод, переопределяемый в производных классах, а затем виртуально вызывать его в деструкторе, у вас ничего не выйдет.

Источник

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

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