что такое виртуальные методы c
Виртуальные функции
Виртуальные функции — специальный вид функций-членов класса. Виртуальная функция отличается об обычной функции тем, что для обычной функции связывание вызова функции с ее определением осуществляется на этапе компиляции. Для виртуальных функций это происходит во время выполнения программы.
Виртуальная функция — это функция, которая определяется в базовом классе, а любой порожденный класс может ее переопределить. Виртуальная функция вызывается только через указатель или ссылку на базовый класс.
Указатель на базовый класс может указывать либо на объект базового класса, либо на объект порожденного класса. Выбор функции-члена зависит от того, на объект какого класса при выполнении программы указывает указатель, но не от типа указателя. При отсутствии члена порожденного класса по умолчанию используется виртуальная функция базового класса.
Результат выполнения
В терминологии ООП «объект посылает сообщение print и выбирает свою собственную версию соответствующего метода». Виртуальной может быть только нестатическая функция-член класса. Для порожденного класса функция автоматически становится виртуальной, поэтому ключевое слово virtual можно опустить.
Пример : выбор виртуальной функции
Результат выполнения
Чистая виртуальная функция
Чистая виртуальная функция — это метод класса, тело которого не определено.
В базовом классе такая функция записывается следующим образом:
Для рассмотренного выше примера (класс Фигура) функцию вычисления площади целесообразно задать чистой виртуальной функцией, которую переопределяет каждый наследуемый класс.
Строка 9 при этом будет иметь вид:
virtual (Справочник по C#)
Ключевое слово virtual используется для изменения объявлений методов, свойств, индексаторов и событий и разрешения их переопределения в производном классе. Например, этот метод может быть переопределен любым наследующим его классом:
Реализацию виртуального члена можно изменить путем переопределения члена в производном классе. Дополнительные сведения об использовании ключевого слова virtual см. в разделах Управление версиями с помощью ключевых слов Override и New и Использование ключевых слов Override и New.
Remarks
При вызове виртуального метода тип времени выполнения объекта проверяется на переопределение члена. Вызывается переопределение члена в самом дальнем классе. Это может быть исходный член, если никакой производный класс не выполнял переопределение этого члена.
По умолчанию методы не являются виртуальными. Такой метод переопределить невозможно.
Действие виртуальных свойств аналогично виртуальным методам, за исключением отличий в синтаксисе объявлений и вызовов.
Использование модификатора virtual в статическом свойстве является недопустимым.
Пример
Следующая программа вычисляет и отображает соответствующую область для каждой фигуры путем вызова нужной реализации метода Area() в соответствии с объектом, связанным с методом.
Спецификация языка C#
Дополнительные сведения см. в спецификации языка C#. Спецификация языка является предписывающим источником информации о синтаксисе и использовании языка C#.
NEWOBJ.ru → Введение в ООП с примерами на C# →
3.3. Виртуальные и абстрактные методы
3.3. Виртуальные и абстрактные методы
§ 37. Абстрактные методы. Рассмотрим следующий пример. Мы проектируем приложение, отображающее схему производственной установки, на которой размещены датчики температуры и давления. На схеме отображаются текущие показания датчиков. Для каждого типа датчика мы создаем отдельный класс, который «умеет» получать показания по этому типу. При этом мы знаем, что все типы датчиков, с которыми мы планируем работать, измеряют текущее значение только одной величины (температуры или давления), соответственно, все классы будут иметь общее состояние – текущее значение – и общий метод – получение этого текущего значения. Проанализируем следующий код:
Какой возможности языка программирования нам не хватает? Мы бы хотели при вызове некоторого метода, объявленного в базовом классе, вызывать реализацию этого метода из производного класса, к типу которого реально относится объект. Конечно, мы можем переадресовать вызов метода базового класса к производному классу, однако, это создает зависимость базового класса от производных, что всегда приводит к серьезным проблемам 47 :
Тем не менее, сама обозначенная задача объявления метода в базовом классе, а реализации – в производном, вполне логична и типична. Поэтому объектно-ориентированные языки предоставляют механизм, позволяющий объявить в базовом классе метод без тела, а реализовать его в производном классе. Такой метод называется абстрактным методом, а класс, его содержащий – абстрактным классом 49 :
При вызове метода для переменной базового класса на этапе выполнения определяется реальный тип объекта и вызывается соответствующая реализация. То есть задействуется тот же механизм, который мы использовали при ручной «переадресации» в листинге с антишаблоном, однако решение с абстрактными методами не приводит к появлению зависимости базового класса от производных и реализуется на уровне языка программирования.
Переопределение метода (overriding) – определение (с телом или без) в производном классе метода с сигнатурой, совпадающей с сигнатурой метода, определенного в базовом классе.
Продемонстрируем это следующим примером:
В заключение разберем еще один важный аспект использования абстрактных методов: вызов абстрактных методов из неабстрактных методов того же класса. Положим, класс датчика получает данные об измеренных значениях в виде массива объектов Data и сохраняет самое последнее полученное измерение как текущее значение curVal :
Так как метод базового класса, в том числе, конструктор базового класса может вызывать код метода производного класса, то должно гарантироваться, что все поля производного класса уже инициализированы. Этим объясняется рассмотренная нами в главе 3.1 последовательность конструирования объекта производного класса. Проанализируйте следующий код:
Объясните, почему при создании объекта при вызове метода N выводится значение 1, а при явном вызове метода N – 2.
Абстрактный методы – крайне важный механизм объектно-ориентированного программирования; убедитесь, что вы вполне разобрались с рассмотренными в параграфе примерами, прежде чем продолжать изучение.
§ 38. Виртуальные методы. В примере с датчиками из предыдущего параграфа метод GetCurVal имеет общую для всех производных классов сигнатуру и реализацию (это обычный, «конкретный» метод), а метод Readout имеет общую для всех производных классов сигнатуру, но реализация определяется в каждом из классов отдельно (абстрактный метод). Однако вполне можно представить, что семантика некоторых методов будет таковой, что у них или (первая ситуация) будет общая реализация, но она будет общей только для некоторых производных классов, или (вторая ситуация) для некоторых производных классов потребуется дополнить общую реализацию некоторым кодом. Рассмотрим такие ситуации.
Как мы можем решить эти две задачи?
Для второй ситуации возможно добавить вызов абстрактного метода с дополнительными действиями, однако это будет плохим решением. Рассмотрим код:
Сложность восприятия кода повысилась, также мы были вынуждены создать в классах TemperatureSensor и PressureSensor методы, не выполняющие никаких действий, а только возвращающие значение, вычисленное в базовом классе. Однако основная проблема этого решения в другом: чтобы расширить поведение базового класса для целей нового производного класса мы вынуждены вносить изменения и в базовый класс, и во все другие уже реализованные и стабильные производные классы, что в значительной мере обесценивает механизм наследования.
Какие возможности языка программирования могли бы решить эти две проблемы? Первая – возможность вызвать из переопределенного метода в производном классе тот же переопределенный метод в базовом классе. Вторая – возможность переопределять не только абстрактные, но и обычные, «конкретные» методы. Проанализируйте следующий код, демонстрирующий обе возможности:
Виртуальный метод (virtual) – метод, который может быть переопределен в производных классах.
Обратим внимание, что аналогично абстрактному методу, виртуальный метод может, но не должен быть переопределен.
В случае многоуровневого наследования логика вызовов методов такая же как для абстрактных методов: всегда вызывается самая последняя реализация в цепочке наследования для реального объекта. Продемонстрируем это сначала на приведенном выше коде. Положим, мы вызываем метод GetCurValString для объекта реального типа VelocityHDSensor через переменную типа Sensor :
Следующий рисунок демонстрирует поведение в аналогичной ситуации при многоуровневом наследовании:
В заключение, рассмотрим еще один пример:
Отметим, что абстрактные методы являются частным случаем виртуальных методов. Можно сказать, что абстрактный метод – это виртуальный метод без реализации. Поэтому, например, в С++, абстрактные методы также называются чистыми виртуальными методами (pure virtual).
Большинство современных языков программирования для обозначения рассмотренных понятий виртуальных и абстрактных методов используют именно термины «виртуальный» и «абстрактный». Также обратим внимание на то, что в C# мы должны явно помечать метод как виртуальный ключевым словом virtual, однако это не является общим правилом. Например, в Java все методы по умолчанию являются виртуальными.
Мы уже отмечали крайнюю важность механизма абстрактных методов. То же самое верно и для виртуальных методов. Можно сказать, что механизм абстрактных и виртуальных методов – ядро инструментария объектно-ориентированного программирования.
Первая возможность, связанная с типом object – использование виртуальных методов, определённых в этом типе. Мы ограничимся рассмотрением только одного такого метода:
Реализация по умолчанию возвращает полное имя класса. Но мы можем переопределить его в своем классе:
Такое преобразование принципиально отличается от того механизма преобразования между базовым и производными типами, который мы рассматривали ранее:
В случае преобразования переменной значимого типа в object и обратно (на рисунке – слева) выполняется создание новой ссылочной переменной и копирование в нее значения исходной. В случае ссылочных переменных (на рисунке – справа), исходные данные остаются без изменений, меняются только ячейки для хранения адреса.
Преобразование переменной значимого типа в ссылочную переменную типа object называется упаковкой (boxing). Обратное преобразование называется распаковкой (unboxing).
Этот механизм используется, например, когда нам нужно обработать массив значений, которые могут быть как значимого, так и ссылочного типа:
Мы вернемся к вопросу упаковки и распаковки в главе 3.6 «Обобщенное программирование».
Вопросы и задания
Дайте определения следующим терминам, а также сопоставьте русские и английские термины: абстрактный метод, абстрактный класс, виртуальный метод, чистый абстрактный метод, переопределенный метод, упаковка, распаковка; abstract, virtual, pure virtual, override, boxing, unboxing.
Зачем в абстрактном классе нужен конструктор, если экземпляр все равно никогда не создается?
Можно ли при переопределении метода изменить его видимость?
* В объектно-ориентированных языках существует возможность запретить наследование от заданного класса. Так в C#, мы можем пометить класс ключевым словом sealed (запечатанный). Чем можно объяснить, что в практике программирования, это ключевое слово значительно чаще используется в Java чем в C#?
47. Мы вернемся к вопросу о недопустимости зависимости базовых классов от производных в § 43.
48. Отметим, что мы не можем объявить метод с одинаковой сигнатурой и базовом, и в производном классе (будет ошибка компиляции), не используя специальные механизмы переопределения, которые мы будем рассматривать далее. Поэтому в этом примере мы используем разные имена методов в производных классах (ReadoutTemperature, ReadoutPressure вместо Readout).
49. Не следует путать с абстрактными типами данных. Это не связанные термины. Как мы уже говорили в главе 2.1, термин «абстрактные типы данных» сегодня употребляется не так широко и обозначает типы данных, моделирующие в программе некоторые абстракции. Слово «абстрактные» в терминах «абстрактные методы» и «абстрактные классы» используется в несколько ином значения, обозначая, что методы или классы не вполне реализованы, в отличие от обычных, конкретных, методов и классов.
50. Заметим, что ключевым словом abstract может быть помечен и класс без абстрактных методов. В этом случае также запрещается создание экземпляров этого класса (но допускается создание экземпляров неабстрактных производных классов).
51. Термин «переопределение» используется, даже если в базовом классе метод только объявлен (приведена сигнатура без тела). Строго говоря, исходный английский термин override переводится как «замещение», то есть мы замещаем метод из базового класса (только объявленный или определенный с реализацией), новым методом (также только объявленным или определённым с реализацией). А термины «объявленный» и «определенный» используются непоследовательно.
52. Строго говоря, в С++ нет ссылочных типов, вместо этого мы создаем переменную указатель и присваиваем ей адрес объекта. Но эта схема в первом приближении полностью аналогична ссылочным типам C#. C++: Point* p = new Point(); C#: Point p = new Point(); Однако в C++ мы можем объявить переменную класса и как значимую: Point p; (аналогия: int x;). В этом случае память будет выделена в стеке метода и p будет обозначать не ячейку с адресом объекта, а сам объект.
Урок №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 в предыдущем примере подавляют механизм вызова виртуальных функций.