Как работают виртуальные функции

от admin

Виртуальные функции

Виртуальные функции — специальный вид функций-членов класса. Виртуальная функция отличается об обычной функции тем, что для обычной функции связывание вызова функции с ее определением осуществляется на этапе компиляции. Для виртуальных функций это происходит во время выполнения программы.

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

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

Виртуальная функция — это функция, которая определяется в базовом классе, а любой порожденный класс может ее переопределить. Виртуальная функция вызывается только через указатель или ссылку на базовый класс.

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

Указатель на базовый класс может указывать либо на объект базового класса, либо на объект порожденного класса. Выбор функции-члена зависит от того, на объект какого класса при выполнении программы указывает указатель, но не от типа указателя. При отсутствии члена порожденного класса по умолчанию используется виртуальная функция базового класса.

Результат выполнения

В каждом случае выполняется различная версия функции print() . Выбор динамически зависит от объекта, на который ссылается указатель.

Если в строке 9 (см. код выше) убрать ключевое слово virtual , то результат выполнения будет уже другим, т.к. связывание функций будет происходить на этапе компиляции:
Без виртуальной функции

В терминологии ООП «объект посылает сообщение print и выбирает свою собственную версию соответствующего метода». Виртуальной может быть только нестатическая функция-член класса. Для порожденного класса функция автоматически становится виртуальной, поэтому ключевое слово virtual можно опустить.

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

Результат выполнения
Переопределение виртуальной функции

Чистая виртуальная функция

Базовый класс иерархии типа обычно содержит ряд виртуальных функций, которые обеспечивают динамическую типизацию. Часто в самом базовом классе сами виртуальные функции фиктивны и имеют пустое тело. Определенное значение им придается лишь в порожденных классах. Такие функции называются чистыми виртуальными функциями .

Чистая виртуальная функция — это метод класса, тело которого не определено.

В базовом классе такая функция записывается следующим образом:

Чистая виртуальные функции используются для того, чтобы отложить решение задачи о реализации функции на более поздний срок. В терминологии ООП это называется отсроченным методом . Класс, имеющий по крайней мере одну чистую виртуальную функцию, называется абстрактным базовым классом . Для иерархии типа полезно иметь абстрактный базовый класс. Он содержит общие свойства иерархии типа, но каждый порожденный класс реализует эти свойства по-своему.

Для рассмотренного выше примера (класс Фигура) функцию вычисления площади целесообразно задать чистой виртуальной функцией, которую переопределяет каждый наследуемый класс.
Строка 9 при этом будет иметь вид:

Виртуальные функции в C++ (virtual functions)

В С++ виртуальные функции (virtual functions) позволяют использовать полиморфизм (polymorhpism) классов. Так как виртуальные функции могут использоваться только внутри классов, то иногда их называют виртуальными методами (virtual methods). Прежде чем воспользоваться виртуальными методами, мы рассмотрим работу обычных методов класса.

Статическое или раннее связывание (static/early binding)

Давайте разберёмся, как происходит вызов обычных функций и методов классов. Вызов обычных функций и методов происходит через механизм, называемый статическим (статичным) связыванием (static binding) или ранним связыванием (early binding).

Раннее связывание использовалось во всех функциях и методах наших программ за исключением тех случаев, где мы использовали указатели на функции.

Когда мы запускаем сборку (building) программы, компилятор просматривает исходный код и превращает все операторы в команды процессора. Допустим, в коде встречается вызов какой-нибудь функции:

Если это обычная функция (не указатель на функцию), то при вызове используется механизм раннего связывания.

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

Теперь взглянем на небольшой пример:

На экран будет выведено две строки Базовый класс. На этапе компиляции память выделяется для двух копий Method – для базового класса и для производного. Оба адреса привязываются к именам методов: Base::Method, Derived::Method. Т.е. когда в коде мы вызываем Method, то вызывается метод, соответствующий типу объекта. Чтобы увидеть, что для каждого объекта вызывается свой метод, давайте переопределим метод Derived::Method:

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

Самое важное здесь то, что компилятор спокойно “проглатывает” тот факт, что указатель на Base указывает на производный класс. Дело в том, что базовый и производный классы являются совместимыми по типу.

Во время выполнения программы процессор видит, что b – это указатель на Base. Процессор не обращает внимание, что на самом деле этот указатель указывает на объект Derived. При вызове метода объекта b процессор переходит к адресу Base::Method.

Чтобы объект b вызвал метод Derived::Method, нужно привести тип. Например, так:

Это примеры раннего связывания (статического).

Обратите внимание, что в этом примере мы помещали в указатель на Base объект Derived, а не наоборот:

Этот случай нам не интересен. К тому же при компиляции возникнет ошибка – здесь нужно использовать static_cast или dynamic_cast. Практическое применение имеет только случай, когда Base* указывает на Derived. В этом случае появляется возможность использовать полиморфизм (polymorphism).

Полиморфизм (polymorphism) и полиморфные типы (polymorphic types)

Рассмотрим гипотетическую ситуацию: в игре есть несколько типов монстров. Все монстры могут атаковать (attack) и перемещаться (move). При этом каждый вид монстров делает это по своему: у кого-то есть только когти, а у кого-то за пазухой припрятан гранатомёт. move и attack мы можем поместить в базовый для всех монстров класс:

Полиморфизм (polymorphism) в C++ и UML

Неплохо было бы иметь возможность хранить объекты всех этих классов вместе и использовать одинаковый синтаксис для вызова методов этих классов. Это и есть полиморфизм (polymorphism) – много (от греческого поли) форм (от греческого морф). Т.е. объекты этих классов должны храниться в одном массиве.

Понятно, что для этого не годится массив объектов, так как в таком массиве для элементов выделяется фиксированное количество памяти. Соответственно, для поддержки полиморфизма нужно использовать массив указателей. Какой тип указателей выбрать? Как мы выяснили раньше, в указателе на базовый тип можно хранить объект любого производного типа – базовый и производный классы являются совместимыми по типу. Почему используется указатели именно на базовый тип? Потому что это более общий класс и от него наследуют все производные классы.

Классы, используемые для получения эффекта полиморфизма, называют полиморфными типами (polymorphic types).

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

Позднее/динамическое связывание (late/dynamic binding)

Поздним связыванием в C++ обладают указатели на функции (function pointers). Мы их уже разбирали, поэтому сложностей возникнуть не должно. Сразу пример:

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

functionPointer обладает динамическим (dynamic) или поздним связыванием (late binding). На какую функцию указывает этот указатель, становится известно только во время выполнения программы. При этом functionPointer может указывать на любую функцию, т.е. значение указателя functionPointer может меняться во время выполнения программы. Это и есть позднее связывание.

Ещё одним примером позднего связывания в C++ являются виртуальные функции (virtual functions). На самом деле виртуальные методы – это обычные указатели на функции. Но об этом чуть позже.

Виртуальные функции/методы (virtual functions/methods)

Чтобы объявить функцию как виртуальную, необходимо добавить ключевое слово virutal перед именем возвращаемого типа:

Вносить изменения в производные классы не нужно. Хотя можно и там добавить ключевое слово virtual (это не обязательно). Теперь посмотрим на наш код:

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

    Несколько замечаний по виртуальным функциям:

Как видите, пользоваться виртуальными методами очень просто. Теперь давайте разберёмся, какие механизмы стоят за виртуальными функциями.

Таблица виртуальных функций (virtual function table)

Рассмотрим простой код:

Функции Base::vf и Derived::vf являются виртуальными. Об этом говорит ключевое слово virtual в базовом классе. А производный класс наследует это свойство для своего метода.

Для виртуальных методов память выделяется точно так же, как и для обычных: на этапе компиляции под эти методы выделяются участки памяти, первые адреса которых являются адресами методов. Но так как методы виртуальные, то фактические адреса метода не привязывается к именам: Base::vf и Derived::vf. Адрес метода, который назначается на этапе компиляции при выделении памяти, будем называть настоящим (или фактическим) адресом.

Когда в базовом классе объявляется хотя бы одна виртуальная функция, то для всех полиморфных классов создаётся таблица виртуальных функций (virtual function table).

Встречаются разные названия этой таблицы: virtual function table, virtual method table, vtable, vftable.

Таблица виртуальных функций – это одномерный массив указателей на функции. Количество элементов в массиве равно количеству виртуальных функций в классе.

Для каждого полиморфного класса (базового и всех производных) создаётся своя таблица виртуальных методов. Количество элементов во всех этих таблицах одинаковое.

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

Помимо создания виртуальной таблицы функций, в базовом классе объявляется поле __vfptr – указатель на vtable. Конечно же, этот указатель наследуется всеми производными классами. __vfptr можно увидеть при отладке.

Читать:
Где находится код главной страницы сайта

__vfptr объекта указывает на vtable класса, которому принадлежит объект.

Рассмотрим пример. Допустим, в базовом классе определено две функции: f – не виртуальная и vf – виртуальная:

В данном случае компилятор не обращает внимания, объект какого типа на самом деле хранится в object. Компилятор смотрит на тип укзаталя и вызывает соответствующий метод – Base::f().

В данном случае процессор видит, что vf – виртуальный метод. Поэтому он ищет в таблице виртуальных функций нужную запись. Но адрес таблицы виртуальных функций он узнаёт через __vfptr, а этот указатель указывает на таблицу своего класса. Соответственно, будет вызван метод того класса, чей объект вызывает метод vf.

Обратите внимание, что в обоих случаях компилятор отдыхает – он даже не пытается проверить тип объекта, на который указывает указатель. Просто при раннем и позднем связывании методы классов вызываются по-разному.

Виртуальный деструктор (virtual destructor)

Как мы видели в примере выше, при использовании обычных функций вызывается функция базового класса. Это же относится и к деструктору. Если в коде вы используете полиморфизм, то всегда объявляйте деструктор базового класса виртуальным. Иначе, при уничтожении всех объектов будет вызываться деструктор базового класса. Вот так правильно:

Хотя, в данном примере это не имеет значения – в данном деструкторе (и в деструкторах производных классов) ничего не происходит.

Абстрактные классы (abstract classes) и чистые виртуальные функции (pure virtual functions)

Очень часто в программах не требуется создавать объекты базовых классов. Т.е. базовые классы нужны только для того, чтобы построить иерархию классов и определить общие свойства для производных классов. Такие классы можно сделать абстрактными (abstract class). При попытке создания объекта абстрактного класса, компилятор выдаст ошибку.

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

Чистая виртуальная функция (pure virtual function) как бы намекает, что она будет реализована в производных классах.

Чтобы сделать виртуальную функцию чистой (pure), нужно добавить после заголовка функции символы =0 (знак равенства и ноль):

В данном случае уже не нужно писать определение такой функции. Помимо этого теперь нельзя создавать объекты класса Base, так как он стал абстрактным.

Символы =0 необязательно добавлять ко всем виртуальным функциям, достаточно добавить к одной.

Заключение

Работу функций трудно объяснить, используя язык программирования высокого уровня. Наиболее просто это сделать с помощью ассемблера, которого мы пока ещё не знаем.

В заключении я попытаюсь кратко резюмировать материал урока.

При вызове обычной функции во время выполнения программы, подставляется её адрес, который был присвоен на этапе компиляции. Это раннее или статическое связывание (early/static binding).

При использовании указателя на функцию, в нём хранится адрес фактического местоположения реальной функции. Этот адрес был назначен на этапе компиляции (абзац выше), но указатель может менять своё значение во время выполнении программы. Это позволяет вызывать с помощью указателя разные функции. Это пример позднего/динамического связывания (late/dynamic binding). Ещё одним примером позднего связывания являются виртуальные функции.

Виртуальные функции объявляются с помощью ключевого слова virtual в базовом классе. При этом для базового класса и для всех производных создаётся таблица указателей на функции – виртуальная таблица методов/функций (virtual function table или vtable). Для каждого класса создаётся своя таблица. Количество элементво в таблице равно количеству виртуальных методов. В таблице хранятся фактические адреса методов, определённых в классах. Также в базовом классе объявляется дополнительное поле __vfptr (наследуется всеми производными классами) – указатель на таблицу виртуальных функций класса. Т.е. когда создаётся объект самого класса или любого производного, в нём __vfptr присваивается адрес таблицы виртуальных функций этого класса (или производных).

Виртуальные функции нужны в C++ для поддержки полиморфизма. Полиморфизм позволяет использовать одинаковый синтаксис для разных классов:

Виртуальные функции в C++

В этом руководстве мы узнаем о виртуальной функции C++ и ее использовании с помощью примеров.

Что такое виртуальная функция в C++?

Виртуальная функция в C++ – это функция-член в базовом классе, которую мы ожидаем переопределить в производных классах.

Виртуальную функцию в C++ используют в базовом классе, чтобы гарантировать, что функция переопределена. Это относится к случаям, когда указатель базового класса указывает на объект производного класса.

Например, рассмотрим код ниже:

Позже, если мы создадим указатель базового типа для указания на объект производного класса и вызовем функцию print(), он вызовет функцию print() базового класса.

Другими словами, функция-член Base не переопределяется.

Чтобы этого избежать, мы объявляем функцию print() базового класса виртуальной с помощью ключевого слова virtual.

Виртуальные функции являются неотъемлемой частью полиморфизма в С++.

Пример 1

Здесь мы объявили функцию print() в Base, как виртуальную.

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

Работа виртуальных функций

Идентификатор переопределения

C++ 11 дал нам новое переопределение идентификатора, которое очень полезно, во избежание ошибок при использовании виртуальных функций.

Этот идентификатор определяет функции-члены производных классов, которые переопределяют функцию-член базового класса.

Если мы используем прототип функции в классе Derived и определяем эту функцию вне класса, то мы используем следующий код:

Использование переопределения

При использовании виртуальных функций в си++ при объявлении функций-членов производных классов можно сделать ошибки.

Использование идентификатора переопределения заставляет компилятор отображать сообщения об ошибках, когда эти ошибки сделаны.

В противном случае программа просто скомпилируется, но виртуальная функция не будет отменена.

Вот некоторые из этих возможных ошибок:

  • Функции с неправильными именами: например, если виртуальная функция в базовом классе называется print(), но мы случайно назвали замещающую функцию в производном классе, как pint().
  • Функции с разными типами возврата: если виртуальная функция, скажем, имеет тип void, но функция в производном классе имеет тип int.
  • Функции с разными параметрами: если параметры виртуальной функции и функций в производных классах не совпадают.
  • В базовом классе не объявлена виртуальная функция.

Использование функции

У нас есть базовый класс Animal и производные классы Dog и Cat.

У каждого класса есть член данных с именем type . Предположим, что эти переменные инициализируются соответствующими конструкторами.

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

  1. getType(), чтобы вернуть значение type;
  2. print() для печати значения type.

Мы могли бы создать обе эти функции в каждом классе отдельно и переопределить их, что будет долгим и утомительным процессом.

Или мы могли бы сделать getType() виртуальным в классе Animal, а затем создать одну отдельную функцию print(), которая принимает указатель типа Animal в качестве аргумента. Затем мы можем использовать эту единственную функцию, чтобы переопределить виртуальную.

Это сделает код короче, чище и менее повторяющимся.

Пример 2: демонстрация

Здесь мы использовали виртуальную функцию getType() и указатель Animal ani , чтобы избежать повторения функции print() в каждом классе.

В main() мы создали 3 указателя Animal для динамического создания объектов классов Animal, Dog и Cat.

Как работают виртуальные функции

A virtual function is a member function which is declared within a base class and is re-defined (overridden) by a derived class. When you refer to a derived class object using a pointer or a reference to the base class, you can call a virtual function for that object and execute the derived class’s version of the function.

  • Virtual functions ensure that the correct function is called for an object, regardless of the type of reference (or pointer) used for function call.
  • They are mainly used to achieve Runtime polymorphism
  • Functions are declared with a virtual keyword in base class.
  • The resolving of function call is done at runtime.

Rules for Virtual Functions

  1. Virtual functions cannot be static.
  2. A virtual function can be a friend function of another class.
  3. Virtual functions should be accessed using pointer or reference of base class type to achieve runtime polymorphism.
  4. The prototype of virtual functions should be the same in the base as well as derived class.
  5. They are always defined in the base class and overridden in a derived class. It is not mandatory for the derived class to override (or re-define the virtual function), in that case, the base class version of the function is used.
  6. A class may have virtual destructor but it cannot have a virtual constructor.

Compile time (early binding) VS runtime (late binding) behavior of Virtual Functions

Consider the following simple program showing runtime behavior of virtual functions.

Output:

Explanation: Runtime polymorphism is achieved only through a pointer (or reference) of base class type. Also, a base class pointer can point to the objects of base class as well as to the objects of derived class. In above code, base class pointer ‘bptr’ contains the address of object ‘d’ of derived class.
Late binding (Runtime) is done in accordance with the content of pointer (i.e. location pointed to by pointer) and Early binding (Compile time) is done according to the type of pointer, since print() function is declared with virtual keyword so it will be bound at runtime (output is print derived class as pointer is pointing to object of derived class) and show() is non-virtual so it will be bound during compile time (output is show base class as pointer is of base type).
NOTE: If we have created a virtual function in the base class and it is being overridden in the derived class then we don’t need virtual keyword in the derived class, functions are automatically considered as virtual functions in the derived class.

Working of virtual functions (concept of VTABLE and VPTR)
As discussed here, if a class contains a virtual function then compiler itself does two things.

Похожие статьи