экономия кода с использованием виртуальных функций

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

Недавно мне задали вопрос: как бы я реализовал механизм виртуальных функций на языке C?

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

Краткое пояснение для тех, кто не знает, что такое виртуальные функции:
Виртуальная функция — это функция, которая может быть переопределена классом-наследником, для того чтобы тот имел свою, отличающуюся, реализацию. В языке C++ используется такой механизм, как таблица виртуальных функций
(кратко vtable) для того, чтобы поддерживать связывание на этапе выполнения программы. Виртуальная таблица — статический массив, который хранит для каждой виртуальной функции указатель на ближайшую в иерархии наследования реализацию этой функции. Ближайшая в иерархии реализация определяется во время выполнения посредством извлечения адреса функции из таблицы методов объекта.

Давайте теперь посмотрим на простой пример использования виртуальных функций в C++

Теперь давайте подумаем, как реализовать концепцию виртуальных функций на C. Зная, что виртуальные функции представлены в виде указателей и хранятся в vtable, а vtable — статический массив, мы должны создать структуру, имитирующую сам класс ClassA, таблицу виртуальных функций для ClassA, а также реализацию методов ClassA.

Как мы видим из кода, приведенного выше, реализация ClassA_get() вызывает функцию set() через указатель из vtable. Теперь посмотрим на реализацию класса-наследника:

Вот так выглядит функция main() :

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

Источник

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

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

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

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

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

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

Вызов невиртуальной функции разрешается в соответствии с типом указателя или ссылки.

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

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

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

Оба вызова PrintBalance в предыдущем примере подавляют механизм вызова виртуальных функций.

Источник

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

экономия кода с использованием виртуальных функций. ajax loader. экономия кода с использованием виртуальных функций фото. экономия кода с использованием виртуальных функций-ajax loader. картинка экономия кода с использованием виртуальных функций. картинка ajax loader. Недавно мне задали вопрос: как бы я реализовал механизм виртуальных функций на языке C?

В С++ виртуальные функции (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 мы можем поместить в базовый для всех монстров класс:

экономия кода с использованием виртуальных функций. monsters. экономия кода с использованием виртуальных функций фото. экономия кода с использованием виртуальных функций-monsters. картинка экономия кода с использованием виртуальных функций. картинка monsters. Недавно мне задали вопрос: как бы я реализовал механизм виртуальных функций на языке C?

Неплохо было бы иметь возможность хранить объекты всех этих классов вместе и использовать одинаковый синтаксис для вызова методов этих классов. Это и есть полиморфизм (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++ для поддержки полиморфизма. Полиморфизм позволяет использовать одинаковый синтаксис для разных классов:

Copyright 2021. All rights reserved.

Источник

C++ MythBusters. Миф о виртуальных функциях

В прошлой статье я рассказывал, с какой не всем известной особенностью можно столкнуться при работе с подставляемыми функциями. Статья породила как несколько существенных замечаний, так и многостраничные споры (и даже холивары), начавшиеся с того, что inline-функции вообще лучше не использовать, и перешедшие в стандартную тему C vs. C++ vs. Java vs. C# vs. PHP vs. Haskell vs. …

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

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

Если в материале про inline-методы миф был не совсем очевиден, то в этом — напротив. Собственно, перейдем к «мифу».

Виртуальные функции и ключевое слово virtual

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

pA is B:
B::foo() // потому что в родительском классе A метод foo() помечен как virtual
B::bar() // потому что в родительском классе A метод bar() помечен как virtual
A::baz() // потому что в родительском классе A метод baz() не помечен как virtual

pA is C:
С::foo() // потому что в родительском классе B метод foo() помечен как virtual
B::bar() // потому что в родительском классе B метод bar() не помечен как virtual,
// но он помечен как virtual в классе A, указатель на который мы используем
A::baz() // потому что в классе A метод baz() не помечен как virtual

С невиртуальной функцией baz() всё и так ясно. А вот с логикой вызова виртуальных функций есть неувязочка. Думаю, не стоит говорить, что на самом деле вывод будет следующим:

Вывод: виртуальная функция становится виртуальной до конца иерархии, а ключевое слово virtual является «ключевым» только в первый раз, а в последующие разы оно несет в себе чисто информативную функцию для удобства программистов.

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

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

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

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

Вывод может отличаться в зависимости от платформы, но в моем случае (Win32, msvc2008) он был следующим:

Что можно понять из этого примера. Во-первых, размер «пустого» класса всегда больше нуля, потому что компилятор специально вставляет в него фиктивный член. Как пишет Эккель, «представьте процесс индексирования в массиве объектов нулевого размера, и все станет ясно» 😉 Во-вторых, мы видим, что размер «непустого» класса NotEmptyVirt при добавлении в него виртуальной функции увеличился на стандартный размер указателя на void; а в «пустом» классе EmptyVirt фиктивный член, который компилятор ранее добавлял для приведения класса к ненулевому размеру, был заменен на указатель. В то же время добавление невиртуальной функции в класс на размер не влияет (спасибо nullbie за совет). Имя указателя на таблицу отличается в зависимости от компилятора. К примеру, компилятор называет его __vfptr, а саму таблицу ‘vftable’ (кто не верит, может посмотреть в отладчике 🙂 В литературе указатель на таблицу виртуальных функций принято называть VPTR, а саму таблицу VTABLE, поэтому я буду придерживаться таких же обозначений.

Что представляет собой таблица виртуальных функций и для чего она нужна? Таблица виртуальных функций хранит в себе адреса всех виртуальных методов класса (по сути, это массив указателей), а также всех виртуальных методов базовых классов этого класса.

Таблиц виртуальных функций у нас будет столько, сколько есть классов, содержащих виртуальные функции — по одной таблице на класс. Объекты каждого из классов содержат именно указатель на таблицу, а не саму таблицу! Вопросы на эту тему любят задавать преподаватели, а также те, кто проводит собеседования. (Примеры каверзных вопросов, на которых можно подловить новичков: «если класс содержит таблицу виртуальных функций, то размер объекта класса будет зависеть от количества виртуальных функций, содержащихся в нем, верно?»; «имеем массив указателей на базовый класс, каждый из которых указывает на объект одного из производных классов — сколько у нас будет таблиц виртуальных функций?» и т.д.).

Итак, для каждого класса у нас будет создана таблица виртуальных функций. Каждой виртуальной функции базового класса присваивается подряд идущий индекс (в порядке объявления функций), по которому в последствие и будет определяться адрес тела функции в таблице VTABLE. При наследовании базового класса, производный класс «получает» и таблицу адресов виртуальных функций базового класса. Если какой-либо виртуальный метод в производном классе переопределяется, то в таблице виртуальных функций этого класса адрес тела соответствующего метода просто будет заменен на новый. При добавлении в производный класс новых виртуальных методов VTABLE производного класса расширяется, а таблица базового класса естественно остается такой же, как и была. Поэтому через указатель на базовый класс нельзя виртуально вызвать методы производного класса, которых не было в базовом — ведь базовый класс о них ничего «не знает» (дальше мы все это посмотрим на примере).

Конструктор класса теперь должен делать дополнительную операцию: инициализировать указатель VPTR адресом соответствующей таблицы виртуальных функций. То есть, когда мы создаем объект производного класса, сначала вызывается конструктор базового класса, инициализирующий VPTR адресом «своей» таблицы виртуальных функций, затем вызывается конструктор производного класса, который перезаписывает это значение.

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

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

Думаю, на примере все станет понятнее. Рассмотрим следующую иерархию:

экономия кода с использованием виртуальных функций. image loader. экономия кода с использованием виртуальных функций фото. экономия кода с использованием виртуальных функций-image loader. картинка экономия кода с использованием виртуальных функций. картинка image loader. Недавно мне задали вопрос: как бы я реализовал механизм виртуальных функций на языке C?

В данном случае получим две таблицы виртуальных функций:

Base
0Base::foo()
1Base::bar()
2Base::baz()

и

Inherited
0Base::foo()
1Inherited::bar()
2Base::baz()
3Inherited::qux()

Как видим, в таблице производного класса адрес второго метода был заменен на соответствующий переопределенный. Пруфкод:

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

Также становится понятно, почему виртуальные функции работают только при обращении по адресу объекта (через указатели либо через ссылки). Как я уже сказал, в этой строке
Base * pBase = new Inherited;
происходит повышающее приведение типа: Inherited* приводится к Base*, но в любом случае указатель всего лишь хранит адрес «начала» объекта в памяти. Если же повышающее приведение производить непосредственно для объекта, то он фактически «обрезается» до размера объекта базового класса. Поэтому логично, что для вызова функций «через объект» используется раннее связывание — компилятор и так «знает» фактический тип объекта.

Собственно, это всё. Жду комментариев. Спасибо за внимание.

Источник

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

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