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

  Юрий Ворон  | 

    | 

  Обновлено 6 Окт 2018  | 

 1109

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

Например:

Результат:

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 (10 оценок, среднее: 5,00 из 5)
Загрузка...

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

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

telegram канал
RAVESLI