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

  Юрий  | 

  Обновл. 27 Июн 2019  | 

 9737

 ǀ   3 

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


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

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

Результат:

rParent is a Parent

Поскольку rParent является ссылкой класса Parent, то вызывается Parent::getName(), хотя фактически мы ссылаемся на часть Parent объекта child.

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

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

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

Результат:

rParent is a Child

Поскольку rParent является ссылкой на родительскую часть объекта child, то, обычно, при обработке rParent.getName() вызывался бы Parent::getName(). Тем не менее, поскольку Parent::getName() является виртуальной функцией, то компилятор понимает, что нужно посмотреть, есть ли переопределения этого метода в дочерних классах. И компилятор находит Child::getName()!

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

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

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

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

   rParent — это ссылка класса A, которой мы указываем ссылаться на часть A объекта c.

   Затем вызывается rParent.getName().

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

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

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

rParent is a C

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


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

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

Matros says ???
Barsik says ???

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

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

Matros says Meow
Barsik says Woof

Сработало!

При обработке animal.speak(), компилятор видит, что метод Animal::speak() является виртуальной функцией. Когда animal ссылается на часть Animal объекта cat, то компилятор просматривает все классы между Animal и Cat, чтобы найти наиболее дочерний метод speak(). И находит Cat::speak(). В случае, когда animal ссылается на часть Animal объекта dog, компилятор находит Dog::speak().

Обратите внимание, мы не сделали 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.

a)

Ответ a)

Результат:

B

rParent — это ссылка класса A на объект c. rParent.getName() вызывает A::getName(), но, поскольку A::getName() является виртуальной функцией, то вызываться будет наиболее дочерний метод между классами A и C. А это B::getName(), так как в классе C метода getName() нет.

b)

Ответ b)

Результат:

C

Всё довольно просто, C::getName() — наиболее дочерний метод между классами B и C.

c)

Ответ c)

Результат:

A

Поскольку getName() класса A не является виртуальным методом, то при обработке rParent.getName() вызовется A::getName().

d)

Ответ d)

Результат:

C

Хотя B и C не являются виртуальными функциями, но A::getName() является виртуальной функцией, а B::getName() и C::getName() являются переопределениями. Следовательно, B::getName() и C::getName() считаются неявно виртуальными, и, следовательно, вызов rParent.getName() вызовет C::getName().

e)

Ответ e)

Результат:

A

Это уже несколько сложнее. rParent — это ссылка класса A на объект c, поэтому rParent.getName() вызывает A::getName(). Но, поскольку A::getName() является виртуальной функцией, то вызывается наиболее дочерний метод между A и C. И это A::getName(). Поскольку B::getName() и С::getName() не являются const, то они не считаются переопределениями!

f)

Ответ f)

Результат:

А

Ещё одно хитрое задание. При создании объекта c, сначала выполняется построение родительской части A. Для этого вызывается конструктор A, а он, в свою очередь, вызывает виртуальную функцию getName(). Поскольку части классов B и C ещё не созданы, то выполняется A::getName().


Оценить статью:

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (51 оценок, среднее: 4,90 из 5)
Загрузка...

Комментариев: 3

  1. Аватар Stikkerrr:

    А зачем для char возвращать значение с помощью указателя?

  2. Аватар Игорь:

    Что это по вашему?

    Что вот это по вашему?

  3. Аватар Константин:

    Логика примера D какая-то иезуитская. Программер явно отказался объявлять методы как virtual, а компилятор так не считает. И что делать программеру с собственным мнением компилятора?

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

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

telegram канал
НОВОСТИ RAVESLI