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

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

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

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

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

Результат выполнения
Что такое виртуальная функция. Смотреть фото Что такое виртуальная функция. Смотреть картинку Что такое виртуальная функция. Картинка про Что такое виртуальная функция. Фото Что такое виртуальная функция

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

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

Результат выполнения
Что такое виртуальная функция. Смотреть фото Что такое виртуальная функция. Смотреть картинку Что такое виртуальная функция. Картинка про Что такое виртуальная функция. Фото Что такое виртуальная функция

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

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

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

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

Источник

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

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

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

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

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

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

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

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

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

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

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

Источник

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

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

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

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

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

Содержание

Пример

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Источник

Урок №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.

Источник

Вызов виртуальных функций в конструкторах и деструкторах (C++)

Что такое виртуальная функция. Смотреть фото Что такое виртуальная функция. Смотреть картинку Что такое виртуальная функция. Картинка про Что такое виртуальная функция. Фото Что такое виртуальная функция
В разных языках программирования поведение виртуальных функций отличается, когда речь заходит о конструкторах и деструкторах. Неправильное использование виртуальных функций – это классическая ошибка при разработке на языке С++, которую мы разберём в этой статье.

Теория

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

Для начала поясню это рисунком.

Что такое виртуальная функция. Смотреть фото Что такое виртуальная функция. Смотреть картинку Что такое виртуальная функция. Картинка про Что такое виртуальная функция. Фото Что такое виртуальная функция

Создадим объект класса C и рассмотрим, что произойдёт, если мы вызовем эти две функции в конструкторе класса B.

Теперь продемонстрирую то же самое кодом.

Если скомпилировать и запустить этот код, то он распечатает:

При вызове виртуальных методов в деструкторах всё работает точно так же.

Казалось бы, в чём проблема? Всё это описано в книжках по программированию на языке С++.

Проблема в том, что про это легко забыть! И считать, что функции foo и bar будут вызваны из крайнего наследника, т.е. из класса C.

Вопрос «Почему код работает неожиданным образом?» вновь и вновь поднимается на форумах. Пример: Calling virtual functions inside constructors.

Думаю, теперь понятно, почему в подобном коде легко допустить ошибку. Особенно легко запутаться, если довелось программировать на других языках, где поведение отличается. Рассмотрим следующую программу на языке C#:

Если её запустить, то будет распечатано:

Соответствующая визуальная схема:

Что такое виртуальная функция. Смотреть фото Что такое виртуальная функция. Смотреть картинку Что такое виртуальная функция. Картинка про Что такое виртуальная функция. Фото Что такое виртуальная функция

Вызывается функция в наследнике из конструктора базового класса!

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

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

При попытке создания экземпляра типа Derived возникнет исключение типа NullReferenceException, даже если в качестве аргумента в конструктор передаётся значение, отличное от null: new Derived(«Hello there»).

При исполнении тела конструктора типа Base будет вызвана реализация метода Test из типа Derived. Этот метод обращается к свойству MyStr, которое в текущий момент проинициализировано значением по умолчанию (null), а не параметром, переданным в конструктор (myStr).

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

Как появилась статья

Всё началось с вопроса «Scan-Build for clang-13 not showing errors» на сайте StackOverflow. Хотя вернее будет сказать, что всё началось с обсуждения статьи «О том, как мы с сочувствием смотрим на вопрос на StackOverflow, но молчим».

Можете не переходить по ссылкам. Я сейчас кратко перескажу суть истории.

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

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

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

Что такое виртуальная функция. Смотреть фото Что такое виртуальная функция. Смотреть картинку Что такое виртуальная функция. Картинка про Что такое виртуальная функция. Фото Что такое виртуальная функция

К публикации на сайте habr появились комментарии (RU) следующего вида:

Сокращенный комментарий N1. Так что компилятор прав, ошибки нет. Ошибка только в логике программиста, его пример кода всегда будет возвращать единицу в первом случае. И он мог бы даже использовать inline для того, чтобы ускорить работу и кода конструктора, и деструктора. Но компилятору это все равно не имеет значение, либо результат функции нигде не используется, функция не задействует никакие внешние аргументы — компилятор просто выкинет пример в качестве оптимизации. И это логичный правильный поступок. Как итог, ошибки просто нет.

Сокращенный комментарий N2. Про виртуальные функции вообще вашего юмора не понял. [цитата из книги про виртуальные функции]. Автор подчеркивает, что ключевое слово virtual используется только один раз. Далее в книге разъясняется, что оно наследуется. А теперь студенты ответьте мне на вопрос: «Где вы увидели проблему вызова виртуальной функции в конструкторе и деструкторе класса? Ответ дать по отдельности для каждого случая». Подразумевая, что вы оба, как неприлежные студенты, не разбираетесь в вопросе, когда вызываются конструктор и деструктор класса. И в добавок совершенно упустили тему «В каком порядке определяются объекты родительских классов при определение предка, и в каком порядке они уничтожаются».

Возможно, прочитав эти комментарии вы недоумеваете, как всё это относится к рассмотренной ранее теме. Правильно, что недоумеваете. Ответ: никак не относится.

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

Да, стоит признать, что вопрос можно было бы сформулировать лучше. Собственно, как таковой проблемы в приведённом коде действительно нет. Пока нет. Она появится в дальнейшем, когда у классов появятся новые наследники, реализующие функцию GetAge, которые что-то делают. Если бы в примере присутствовал ещё один класс, наследующий P, то вопрос стал бы более полным.

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

Запрет на вызов виртуальных функций в конструкторах/деструкторах нашёл своё отражение и в стандартах кодирования. Например в SEI CERT C++ Coding Standard есть правило: OOP50-CPP. Do not invoke virtual functions from constructors or destructors. Это диагностическое правило реализуют многие анализаторы кода, такие как Parasoft C/C++test, Polyspace Bug Finder, PRQA QA-C++, SonarQube C/C++ Plugin. В их число входит и разрабатываемый нашей командой PVS-Studio (диагностика V1053).

А если ошибки нет?

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

Такой код будет однозначно правильно понят вашими коллегами. Статические анализаторы в свою очередь тоже всё поймут и промолчат.

Заключение

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

Работа виртуальных функций, конечно, не такое тайное знание, как примеры по ссылкам :). Однако, как показывают комментарии и вопросы на StackOverflow, эта тема заслуживает внимания и контроля. Было бы всё очевидно – не было бы этой статьи. Хорошо, что анализаторы кода способны подстраховать программиста в его работе.

Спасибо за внимание, и приходите попробовать анализатор PVS-Studio.

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Andrey Karpov. Virtual function calls in constructors and destructors (C++).

Источник

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

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