Что значит виртуальный метод

Виртуальный метод

Виртуальный метод (виртуальная функция) — в объектно-ориентированном программировании метод (функция) класса, который может быть переопределён в классах-наследниках так, что конкретная реализация метода для вызова будет определяться во время исполнения. Таким образом, программисту необязательно знать точный тип объекта для работы с ним через виртуальные методы: достаточно лишь знать, что объект принадлежит классу или наследнику класса, в котором метод объявлен.

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

Базовый класс может и не предоставлять реализации виртуального метода, а только декларировать его существование. Такие методы без реализации называются «чистыми виртуальными» (перевод англ. pure virtual ) или абстрактными. Класс, содержащий хотя бы один такой метод, тоже будет абстрактным. Объект такого класса создать нельзя (в некоторых языках допускается, но вызов абстрактного метода приведёт к ошибке). Наследники абстрактного класса должны предоставить реализацию для всех его абстрактных методов, иначе они, в свою очередь, будут абстрактными классами.

Читайте также:  Что значит жить по правилам обществознание 7 класс боголюбова гдз

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

Содержание

Пример виртуальной функции на C++

Пример на C++, иллюстрирующий отличие виртуальных функций от невиртуальных:

В этом примере класс Ancestor определяет две функции, одну из них виртуальную, другую — нет. Класс Descendant переопределяет обе функции. Однако, казалось бы одинаковое обращение к функциям даёт разные результаты. На выводе программа даст следующее:

То есть, в случае виртуальной функции, для определения реализации функции используется информация о типе объекта и вызывается «правильная» реализация, независимо от типа указателя. При вызове невиртуальной функции, компилятор руководствуется типом указателя или ссылки, поэтому вызываются две разные реализации function2() , несмотря на то, что используется один и тот же объект.

Следует отметить, что в С++ можно, при необходимости, указать конкретную реализацию виртуальной функции, фактически вызывая её невиртуально:

для нашего примера выведет Ancestor::function1(), игнорируя тип объекта.

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

Язык Object Pascal, использующийся в Delphi, тоже поддерживает полиморфизм. Рассмотрим пример:

Объявим два класса. Предка (Ancestor):

и его потомка (Descendant):

Как видно в классе предке объявлена виртуальная функция — VirtualProcedure . Чтобы воспользоваться достоинствами полиморфизма, её нужно перекрыть в потомке.

Реализация выглядит следующим образом:

Посмотрим как это работает:

Заметьте, что в разделе var мы объявили два объекта MyObject1 и MyObject2 типа TAncestor . А при создании MyObject1 создали как TAncestor , а MyObject2 как TDescendant . Вот что мы увидим при нажатии на кнопку BitBtn1 :

  1. Ancestor static procedure.
  2. Ancestor virtual procedure.
  3. Ancestor static procedure.
  4. Descendant override procedure.

Для MyObject1 все понятно, просто вызвались указанные процедуры. А вот для MyObject2 это не так.

Вызов MyObject2.StaticProcedure; привел к появлению «Ancestor static procedure.». Ведь мы объявили MyObject2: TAncestor , поэтому и была вызвана процедура StaticProcedure; класса TAncestor .

А вот вызов MyObject2.VirtualProcedure; привел к вызову VirtualProcedure; реализованной в потомке( TDescendant ). Это произошло потому, что MyObject2 был создан не как TAncestor , а как TDescendant : MyObject2 := TDescendant.Create; . И виртуальный метод VirtualProcdure был перекрыт.

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

Достаточно часто виртуальные методы забывают перекрыть с помощью ключевого слова override . Это приводит к закрытию метода. В этом случае замещения методов в VMT не произойдет и требуемая функциональность не будет получена.

Эта ошибка отслеживается компилятором, который выдаёт соответствующее предупреждение.

Вызов метода предка из перекрытого метода

Бывает необходимо вызвать метод предка в перекрытом методе.

Объявим два класса. Предка(Ancestor):

и его потомка (Descendant):

Обращение к методу предка реализуется с помощью ключевого слова «inherited»

Стоит помнить, что в Delphi деструктор должен быть обязательно перекрытым — «override» — и содержать вызов деструктора предка

В языке C++ не нужно вызывать конструктор и деструктор предка, деструктор должен быть виртуальным. Деструкторы предков вызовутся автоматически. Чтобы вызвать метод предка, нужно явно вызвать метод:

Для вызова конструктора предка нужно указать конструктор:

Источник

Виртуальные методы, свойства и индексаторы

C# — Руководство по C# — Виртуальные методы, свойства и индексаторы

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

Виртуальным называется такой метод, который объявляется как virtual в базовом классе. Виртуальный метод отличается тем, что он может быть переопределен в одном или нескольких производных классах. Следовательно, у каждого производного класса может быть свой вариант виртуального метода. Кроме того, виртуальные методы интересны тем, что именно происходит при их вызове по ссылке на базовый класс. В этом случае средствами языка C# определяется именно тот вариант виртуального метода, который следует вызывать, исходя из типа объекта, к которому происходит обращение по ссылке, причем это делается во время выполнения. Поэтому при ссылке на разные типы объектов выполняются разные варианты виртуального метода. Иными словами, вариант выполняемого виртуального метода выбирается по типу объекта, а не по типу ссылки на этот объект.

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

Метод объявляется как виртуальный в базовом классе с помощью ключевого слова virtual, указываемого перед его именем. Когда же виртуальный метод переопределяется в производном классе, то для этого используется модификатор override. А сам процесс повторного определения виртуального метода в производном классе называется переопределением метода. При переопределении метода — имя, возвращаемый тип и сигнатура переопределяющего метода должны быть точно такими же, как и у того виртуального метода, который переопределяется. Кроме того, виртуальный метод не может быть объявлен как static или abstract.

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

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

И еще одно замечание: свойства также подлежат модификации ключевым словом virtual и переопределению ключевым словом override. Это же относится и к индексаторам.

Давайте рассмотрим пример использования виртуальных методов, свойств и индексаторов:

Давайте рассмотрим данный пример более подробно. В базовом классе Font инкапсулируется виртуальный метод FontInfo (), возвращающий информацию о шрифте. В производном классе FontColor данный метод переопределяется с помощью ключевого слова override, поэтому при создании экземпляра данного класса и вызова метода FontInfo() в исходную информацию возвращается помимо первоначальных данных еще и цвет шрифта. Затем данный метод вновь переопределяется в классе GradientColorFont, унаследованном от класса FontColor. Обратите внимание, что здесь переопределяется не исходный метод базового класса Font, а уже переопределенный метод класса FontColor. В этом и заключается принцип динамического полиморфизма!

Так же обратите внимание, что в данном примере используется виртуальное свойство color, принцип использования которого аналогичен использованию виртуального метода.

Источник

Классы и объекты C#: виртуальные методы и свойства

При использовании механизма наследовании в C# достаточно часто появляется необходимость изменить в классе-наследнике функционал метода, который был унаследован от предка (базового класса). Для этого, в C# класс-наследник может переопределять методы и свойства базового класса. Те методы или свойства, которые мы хотим сделать доступными для переопределения, в базовом классе помечается модификатором virtual . Такие методы и свойства обычно называют виртуальными.

Переопределение методов в C#

Чтобы переопределить метод в классе-наследнике, этот метод определяется с в классе модификатором override . В отличие от перегрузки, переопределенный метод в классе-наследнике должен иметь тот же набор параметров, что и виртуальный метод в базовом классе. Например, рассмотрим следующие классы:

Класс Person представляет человека. В свою очередь, класс Employee наследуется от Person и представляет сотрудника организации и, кроме унаследованного свойства Name , имеет еще одно свойство — Company . Чтобы сделать метод Display доступным для переопределения, этот метод определен с модификатором virtual . Поэтому мы можем переопределить этот метод (а можем и не переопределять — всё зависит от наших потребностей).

Допустим, что нас устраивает реализация метода из базового класса. В таком случае, объекты Employee будут использовать реализацию метода Display из класса Person :

В консоли мы увидим следующее:

Если же нас не устраивает функционал метода Display в родителе, то мы можем переопределить виртуальный метод. Для этого в классе-наследнике определяется метод с модификатором override , который имеет то же самое имя и набор параметров:

В консоли будут следующие строки:

Tom работает в Microsoft

Виртуальные методы базового класса определяют интерфейс всей иерархии классов. Это значит, что в любом производном классе, который не является прямым наследником от базового класса, можно переопределить виртуальные методы. Например, мы можем определить класс Manager , который будет производным от Employee , и в нем также переопределить метод Display .

При переопределении виртуальных методов в C# необходимо учитывать следующие ограничения:

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

Переопределение свойств в C#

В C# можно переопределять как методы, так и свойства базовых классов:

Ключевое слово base

С использованием ключевого слова base мы можем обращаться к членам базового класса. В нашем случае вызов base.Display(); позволяет обратиться к методу Display() в классе Person :

Запрет переопределения методов

Бывает необходимо также запретить переопределение методов и свойств в классах-наследниках. В этом случае методы и свойства необходимо объявлять с модификатором sealed :

При создании методов с модификатором sealed следует учитывать, что ключевое слово sealed применяется в паре с override , то есть только в переопределяемых методах.

Итого

Сегодня мы познакомились с такой замечательной возможностью языка C# как переопределение методов и свойств. Узнали как определять в классах виртуальные методы, переопределять методы и свойства в наследниках, а также обращаться к членам и методам базовых классов, используя ключевое слово base и запрещать переопределять методы с использованием ключевого слова sealed .

Источник

Таблица виртуальных методов и техника безопасности

В качестве небольшой разминки перед статьёй хотелось бы, чтобы читатель задал себе следующий вопрос: нужно ли фотографу для получения качественных снимков знать, как работает фотоаппарат? Ну, по крайней мере, должен ли он знать понятие «диафрагма»? «Отношение сигнал-шум»? «Глубина резкости»? Практика подсказывает, что даже со знанием таких сложных слов снимки могут получиться у наиболее «рукастых» не особо лучше снятых на мобильник через 0.3-МПикс-дупло. И наоборот, по-настоящему хорошие снимки могут получаться благодаря исключительно опыту и наитию при полном незнании матчасти (хотя это, скорее, исключения из правил, но всё же). Однако вряд ли со мной кто-то будет спорить, что профессионалам, которые хотят выжать из своей техники всё (а не только количество мегапикселей на квадратный миллиметр матрицы), эти знания нужны в обязательном порядке, поскольку в противном случае ему и называться профессионалом-то нельзя. И верно это не только для отрасли цифровой фотографии, но и для практически любой другой.

Верно это и для программирования, а для программирования на языке С++ – вдвойне. В этой статье будет описано важное понятие языка, известное как «Виртуальный табличный указатель», что присутствует почти во всех сложных классах, и то, каким образом его можно случайно повредить. Это может, в свою очередь, вести к едва поддающимся отладке ошибкам. Сначала напомню, что это вообще такое, а затем и поделюсь своими соображениями по поводу того, как и что может там сломаться.

К нашему огромному сожалению, в этой статье будет много рассуждений, связанных с низким уровнем. Но больше никак проблему, увы, не проиллюстрировать. Заодно оговорюсь, что статья написана по большей части для компилятора Visual C++ Compiler в режиме сборки 64-битной программы – результаты работы программы в других компиляторах и под другую архитектуру могут отличаться.

Виртуальный табличный указатель

В теории сказано, что указатель vptr – указатель на таблицу виртуальных методов, или виртуальный табличный указатель – присутствует в каждом классе, в котором есть хотя бы один виртуальный метод. Разберёмся поподробнее, что же это за зверь-то такой. Для этого напишем простенькую демонстрационную программу на языке С++.

Несмотря на относительно большой объём кода, логика его работы должна быть достаточно очевидна: на стеке выделяется 32 байта, которые заполняются значениями 0x11 (считаем, что это как бы «мусор» в памяти). Затем поверх этих 32 байт при помощи оператора placement new создаётся достаточно тривиальный объект класса A. Наконец, производится печать содержимого памяти, после чего программа разрушает объект и завершает своё выполнение. Ниже представлен вывод данной программы (Microsoft Visual Studio 2012, x64).

Нетрудно заметить, что размер класса в памяти составляет 8 байт и равен размеру единственного его члена unsigned long long content_A.

Немного усложним программу, добавив к объявлению функции void function(void) ключевое слово virtual:

Вывод программы (здесь и далее будет показываться лишь часть вывода за исключением Memory before placement new и Press any key. ):

Опять же, нетрудно заметить, что размер класса в памяти теперь составляет 16 байт. Первые восемь байт теперь занимает указатель на таблицу виртуальных методов. Указатель при этом запуске программы оказался равен 0x000000013FC4D1F8 (указатель и content_A «развёрнуты» в памяти, так как Intel64 использует little-endian порядок байт; правда, в случае с content_A так сразу и не скажешь об этом).

Таблица виртуальных методов – специальная структура в памяти, генерируемая автоматически, в которой перечислены указатели на виртуальные методы. В случае, если где-то в коде вызывается метод function() применительно к указателю на класс A, вместо вызова непосредственно функции A::function() будет произведён вызов функции, находящейся в таблице виртуальных методов по нужному смещению – это поведение реализует полиморфизм. Сама по себе таблица виртуальных функций представлена ниже (получена путём компиляции с ключом /FAs; дополнительно обратите внимание на несколько странное имя функции в ассемблерном коде – оно прошло через «манглинг имён»):

__declspec(novtable)

Иногда бывают такие ситуации, когда таблица виртуальных классов, в принципе, не нужна. Предположим, что мы никогда не будем инстанцировать класс A, а если и будем, то только по выходным и в праздники, но при этом тщательно следя, чтобы не вызывалась ни одна виртуальная функция. Это достаточно частая ситуация в случаях абстрактных классов – известно, что если класс абстрактный, то он не может быть инстанцирован. Вообще никак. Действительно, если бы функция function(void) была бы объявлена в классе A как абстрактная, то таблица виртуальных методов выглядела бы следующим образом:

Очевидно, что попытка вызова такой функции приведёт к прострелу собственной ноги.

Встаёт вопрос: если класс никогда не инстанцируется, то зачем устанавливать виртуальный табличный указатель? Для того, чтобы компилятор не генерировал лишний код, ему можно дать указание в виде __declspec(novtable) (осторожно: Microsoft-specific!). Перепишем наш пример класса с виртуальной функции с использованием атрибута __declspec(novtable):

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

В первую очередь обратим внимание на то, что размер объекта не изменился: он по-прежнему занимает 16 байт. Итого после внесения атрибута __declspec(novtable) появилось всего два отличия: во-первых, теперь на том месте, где раньше располагался адрес таблицы виртуальных методов, находится неинициализированная область памяти; во-вторых – в ассемблерном коде таблицы виртуальных методов класса A теперь нет вообще. Но виртуальный табличный указатель по-прежнему есть и по-прежнему «весит» восемь байт! Это нужно помнить, потому что…

Наследование

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

Также сделаем так, чтобы вместо класса A в основной программе создавался (и уничтожался) класс B:

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

Попробуем разобраться, что произошло. Был вызван конструктор B::B(). Этот конструктор, прежде чем выполняться, вызывает конструктор базового класса, конструктор A::A(). В первую очередь тот должен был бы инициализировать виртуальный табличный указатель, однако из-за атрибута __declspec(novtable) он не был инициализирован. Затем конструктор устанавливает значение поля content_A в 0xAAAAAAAAAAAAAAAAull (второе поле в памяти) и возвращает управление конструктору B::B().

Поскольку объект B не имеет атрибута __declspec(novtable), конструктор устанавливает виртуальный табличный указатель (первое поле в памяти) на таблицу виртуальных методов класса B, а затем устанавливает content_B в 0xBBBBBBBBBBBBBBBBull (третье поле в памяти) и возвращает управление основной программе. По содержимому памяти можно без труда понять, что объект класса B был сконструирован правильно, а из логики ясно, что ненужная в данном контексте операция была пропущена. Если запутались: под ненужной операцией имеется в виду инициализация указателя на виртуальную таблицу в конструкторе базового класса.

Казалось бы, пропущена всего одна операция – смысл избавляться от неё? Но если в программе тысячи и тысячи классов, унаследованные от одного и того же абстрактного класса, избавление от одной автогенерируемой команды может серьёзно повлиять на производительность. И повлияет. Не верите?

Функция memset

Основная идея функции memset() – заполнение области памяти некоторым константным значением (чаще всего нулями). В языке Си её можно было использовать для быстрой инициализации всех полей структуры. А чем отличается класс С++ от структуры Си по расположению в памяти, если в нём нет виртуального табличного указателя? В принципе, ничем, данные – они и есть данные. Для инициализации действительно простых классов (в терминологии С++11 – типов со стандартным устройством) вполне возможно применять функцию memset(). Но, по идее, функцию memset() можно применять для инициализации вообще всех классов, вот только каковы будут последствия? Неправильный memset() может одним махом привести виртуальный табличный указатель в негодность. Но тут же встаёт вопрос: а, может, всё-таки можно, если класс объявлен как __declspec(novtable)?

Ответ: можно, но только осторожно.

Перепишем классы следующим образом: добавим метод wipe, который будет устанавливать всё содержимое класса A в 0xAA:

Вывод программы в этом случае получится достаточно ожидаемым:

Пока всё работает хорошо.

Однако стоит слегка изменить место вызова функции wipe(), закомментировав строки конструкторов и раскомментировав идущие за ними, и сразу станет ясно, что что-то пошло не так. Первый же вызов виртуальной функции function() обернётся ошибкой времени выполнения из-за повреждённого виртуального табличного указателя:

Почему так произошло? Функция wipe() была вызвана уже после того, как конструктор класса B инициализировал указатель на таблицу виртуальных методов. В итоге этот указатель испортился. Иными словами – не стоит обнулять класс с виртуальным табличным указателем, даже если он объявлен с __declspec(novtable). Полное обнуление будет уместно только в конструкторе того класса, который никогда не будет инстанцирован, да и то делать это нужно с большой осторожностью.

Функция memcpy

С функцией memcpy() картина точно такая же. Опять же, по идее, ей можно пользоваться для того, чтобы копировать типы со стандартным устройством в памяти. Однако, если судить по практике, некоторые программисты любят пользоваться ей где надо и где не надо. В случае с типами, не обладающими стандартным устройством в памяти, использование функции memcpy() – это как хождение по канату над Ниагарским водопадом: одна ошибка может привести к фатальным последствиям, а совершить её до смешного просто. В качестве примера:

Конструктор копирования может писать всё, что его цифровой душе угодно, в указатель на виртуальную таблицу абстрактного класса: туда всё равно в классах-наследниках будет помещено правильное значение. А вот в реализации оператора присваивания использовать функцию memcpy() уже нельзя:

А теперь вспомните, насколько мы привыкли, что оператор присваивания и конструктор копирования – это фактически одно и то же. Нет, не всё так плохо: на практике код оператора присваивания даже может исправно работать, но вовсе не потому, что он корректен, а потому, что так сложились звёзды. В коде копируется указатель на таблицу виртуальных методов из другого объекта, и во что это выльется – неизвестно.

PVS-Studio

Эта статья появилась как результат детального исследования касательно загадочного __declspec(novtable), а также когда можно, а когда нельзя использовать функции memset() и memcpy() в высокоуровневом коде. Нам время от времени пишут разработчики, что анализатор PVS-Studio слишком часто выдаёт предупреждения касательно виртуального табличного указателя. Программисты считают, что если есть __declspec(novtable), то нет ни таблицы виртуальных методов, ни виртуального табличного указателя. Мы начали внимательно разбираться с этим вопросом и поняли, что не всё так просто.

Это надо запомнить. Если при объявлении класса используется __declspec(novtable), это не значит, что класс не содержит указателя на таблицу виртуальных методов! А вот инициализируется этот указатель или нет – это уже совсем другой вопрос.

Мы сделаем так, чтобы анализатор не ругался на функции memset()/memcpy(), но только если они используются в конструкторах базового класса, объявленного с __declspec(novtable).

Источник

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