что такое виртуальный метод
Виртуальные методы, свойства и индексаторы
Виртуальным называется такой метод, который объявляется как virtual в базовом классе. Виртуальный метод отличается тем, что он может быть переопределен в одном или нескольких производных классах. Следовательно, у каждого производного класса может быть свой вариант виртуального метода. Кроме того, виртуальные методы интересны тем, что именно происходит при их вызове по ссылке на базовый класс. В этом случае средствами языка C# определяется именно тот вариант виртуального метода, который следует вызывать, исходя из типа объекта, к которому происходит обращение по ссылке, причем это делается во время выполнения. Поэтому при ссылке на разные типы объектов выполняются разные варианты виртуального метода. Иными словами, вариант выполняемого виртуального метода выбирается по типу объекта, а не по типу ссылки на этот объект.
Так, если базовый класс содержит виртуальный метод и от него получены производные классы, то при обращении к разным типам объектов по ссылке на базовый класс выполняются разные варианты этого виртуального метода.
Переопределение метода служит основанием для воплощения одного из самых эффективных в C# принципов: динамической диспетчеризации методов, которая представляет собой механизм разрешения вызова во время выполнения, а не компиляции. Значение динамической диспетчеризации методов состоит в том, что именно благодаря ей в C# реализуется динамический полиморфизм.
Если при наличии многоуровневой иерархии виртуальный метод не переопределяется в производном классе, то выполняется ближайший его вариант, обнаруживаемый вверх по иерархии.
И еще одно замечание: свойства также подлежат модификации ключевым словом virtual и переопределению ключевым словом override. Это же относится и к индексаторам.
Давайте рассмотрим пример использования виртуальных методов, свойств и индексаторов:
Давайте рассмотрим данный пример более подробно. В базовом классе Font инкапсулируется виртуальный метод FontInfo (), возвращающий информацию о шрифте. В производном классе FontColor данный метод переопределяется с помощью ключевого слова override, поэтому при создании экземпляра данного класса и вызова метода FontInfo() в исходную информацию возвращается помимо первоначальных данных еще и цвет шрифта. Затем данный метод вновь переопределяется в классе GradientColorFont, унаследованном от класса FontColor. Обратите внимание, что здесь переопределяется не исходный метод базового класса Font, а уже переопределенный метод класса FontColor. В этом и заключается принцип динамического полиморфизма!
Так же обратите внимание, что в данном примере используется виртуальное свойство color, принцип использования которого аналогичен использованию виртуального метода.
Урок №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.
Виртуальный метод
Виртуальный метод (виртуальная функция) — в объектно-ориентированном программировании метод (функция) класса, который может быть переопределён в классах-наследниках так, что конкретная реализация метода для вызова будет определяться во время исполнения. Таким образом, программисту необязательно знать точный тип объекта для работы с ним через виртуальные методы: достаточно лишь знать, что объект принадлежит классу или наследнику класса, в котором метод объявлен.
Виртуальные методы — один из важнейших приёмов реализации полиморфизма. Они позволяют создавать общий код, который может работать как с объектами базового класса, так и с объектами любого его класса-наследника. При этом базовый класс определяет способ работы с объектами и любые его наследники могут предоставлять конкретную реализацию этого способа. В некоторых языках программирования, например в Java, нет понятия виртуального метода, данное понятие следует применять лишь для языков, в которых методы родительского класса не могут быть переопределены по умолчанию, а только с помощью некоторых вспомогательных ключевых слов. В некоторых же (как, например, в Python), все методы — виртуальные.
Базовый класс может и не предоставлять реализации виртуального метода, а только декларировать его существование. Такие методы без реализации называются «чистыми виртуальными» (перевод англ. pure virtual ) или абстрактными. Класс, содержащий хотя бы один такой метод, тоже будет абстрактным. Объект такого класса создать нельзя (в некоторых языках допускается, но вызов абстрактного метода приведёт к ошибке). Наследники абстрактного класса должны предоставить реализацию для всех его абстрактных методов, иначе они, в свою очередь, будут абстрактными классами.
Для каждого класса, имеющего хотя бы один виртуальный метод, создаётся таблица виртуальных методов. Каждый объект хранит указатель на таблицу своего класса. Для вызова виртуального метода используется такой механизм: из объекта берётся указатель на соответствующую таблицу виртуальных методов, а из неё, по фиксированному смещению, — указатель на реализацию метода, используемого для данного класса. При использовании множественного наследования или интерфейсов ситуация несколько усложняется за счёт того, что таблица виртуальных методов становится нелинейной.
Содержание
Пример виртуальной функции на C++
Пример на C++, иллюстрирующий отличие виртуальных функций от невиртуальных:
В этом примере класс Ancestor определяет две функции, одну из них виртуальную, другую — нет. Класс Descendant переопределяет обе функции. Однако, казалось бы одинаковое обращение к функциям даёт разные результаты. На выводе программа даст следующее:
Следует отметить, что в С++ можно, при необходимости, указать конкретную реализацию виртуальной функции, фактически вызывая её невиртуально:
для нашего примера выведет Ancestor::function1(), игнорируя тип объекта.
Пример виртуальной функции в Delphi
Язык Object Pascal, использующийся в Delphi, тоже поддерживает полиморфизм. Рассмотрим пример:
Объявим два класса. Предка (Ancestor):
и его потомка (Descendant):
Реализация выглядит следующим образом:
Посмотрим как это работает:
Для MyObject1 все понятно, просто вызвались указанные процедуры. А вот для MyObject2 это не так.
В Delphi полиморфизм реализован с помощью так называемой виртуальной таблицы методов (или VMT).
Эта ошибка отслеживается компилятором, который выдаёт соответствующее предупреждение.
Вызов метода предка из перекрытого метода
Бывает необходимо вызвать метод предка в перекрытом методе.
Объявим два класса. Предка(Ancestor):
и его потомка (Descendant):
Обращение к методу предка реализуется с помощью ключевого слова «inherited»
Стоит помнить, что в Delphi деструктор должен быть обязательно перекрытым — «override» — и содержать вызов деструктора предка
В языке C++ не нужно вызывать конструктор и деструктор предка, деструктор должен быть виртуальным. Деструкторы предков вызовутся автоматически. Чтобы вызвать метод предка, нужно явно вызвать метод:
Для вызова конструктора предка нужно указать конструктор:
Виртуальные функции
Виртуальные функции — специальный вид функций-членов класса. Виртуальная функция отличается об обычной функции тем, что для обычной функции связывание вызова функции с ее определением осуществляется на этапе компиляции. Для виртуальных функций это происходит во время выполнения программы.
Виртуальная функция — это функция, которая определяется в базовом классе, а любой порожденный класс может ее переопределить. Виртуальная функция вызывается только через указатель или ссылку на базовый класс.
Указатель на базовый класс может указывать либо на объект базового класса, либо на объект порожденного класса. Выбор функции-члена зависит от того, на объект какого класса при выполнении программы указывает указатель, но не от типа указателя. При отсутствии члена порожденного класса по умолчанию используется виртуальная функция базового класса.
Результат выполнения
В терминологии ООП «объект посылает сообщение print и выбирает свою собственную версию соответствующего метода». Виртуальной может быть только нестатическая функция-член класса. Для порожденного класса функция автоматически становится виртуальной, поэтому ключевое слово virtual можно опустить.
Пример : выбор виртуальной функции
Результат выполнения
Чистая виртуальная функция
Чистая виртуальная функция — это метод класса, тело которого не определено.
В базовом классе такая функция записывается следующим образом:
Для рассмотренного выше примера (класс Фигура) функцию вычисления площади целесообразно задать чистой виртуальной функцией, которую переопределяет каждый наследуемый класс.
Строка 9 при этом будет иметь вид:
Виртуальные функции в C
Недавно мне задали вопрос: как бы я реализовал механизм виртуальных функций на языке C?
Поначалу я понятия не имел, как это можно сделать: ведь C не является языком объектно-ориентированного программирования, и здесь нет такого понятия, как наследование. Но поскольку у меня уже было немного опыта с C, и я знал, как работают виртуальные функции, я подумал, что должен быть способ сымитировать поведение виртуальных функций, используя структуры (struct).
Краткое пояснение для тех, кто не знает, что такое виртуальные функции:
Виртуальная функция — это функция, которая может быть переопределена классом-наследником, для того чтобы тот имел свою, отличающуюся, реализацию. В языке C++ используется такой механизм, как таблица виртуальных функций
(кратко vtable) для того, чтобы поддерживать связывание на этапе выполнения программы. Виртуальная таблица — статический массив, который хранит для каждой виртуальной функции указатель на ближайшую в иерархии наследования реализацию этой функции. Ближайшая в иерархии реализация определяется во время выполнения посредством извлечения адреса функции из таблицы методов объекта.
Давайте теперь посмотрим на простой пример использования виртуальных функций в C++
Теперь давайте подумаем, как реализовать концепцию виртуальных функций на C. Зная, что виртуальные функции представлены в виде указателей и хранятся в vtable, а vtable — статический массив, мы должны создать структуру, имитирующую сам класс ClassA, таблицу виртуальных функций для ClassA, а также реализацию методов ClassA.
Как мы видим из кода, приведенного выше, реализация ClassA_get() вызывает функцию set() через указатель из vtable. Теперь посмотрим на реализацию класса-наследника:
Вот так выглядит функция main() :
Конечно же, этот трюк не выглядит настолько же естественно, как в C++ или в другом объектно-ориентированном языке программирования, и мне никогда не приходилось реализовывать нечто подобное, когда я писал программы на C, но тем не менее это может помочь лучше понять внутреннее устройство виртуальных функций.