В объектно-ориентированном программировании , которое часто используется в C++ и Object Pascal , виртуальная функция или виртуальный метод — это наследуемая и переопределяемая функция или метод , которые отправляются динамически . Виртуальные функции являются важной частью полиморфизма (времени выполнения) в объектно-ориентированном программировании (ООП). Они позволяют выполнять целевые функции, которые не были точно идентифицированы во время компиляции.
Большинство языков программирования, таких как JavaScript , PHP и Python , по умолчанию рассматривают все методы как виртуальные [1] [2] и не предоставляют модификатор для изменения этого поведения. Однако некоторые языки предоставляют модификаторы для предотвращения переопределения методов производными классами (например, ключевые слова final и private в Java [3] и PHP [4] ).
Концепция виртуальной функции решает следующую задачу:
В объектно-ориентированном программировании , когда производный класс наследует от базового класса, объект производного класса может быть указан через указатель или ссылку типа базового класса вместо типа производного класса. Если есть методы базового класса, переопределенные производным классом, метод, фактически вызываемый такой ссылкой или указателем, может быть связан (связан) либо «рано» (компилятором), в соответствии с объявленным типом указателя или ссылки, либо «поздно» (т. е. системой времени выполнения языка), в соответствии с фактическим типом объекта, на который ссылаются.
Виртуальные функции разрешаются "поздно". Если рассматриваемая функция является "виртуальной" в базовом классе, вызывается реализация функции в наиболее производном классе в соответствии с фактическим типом объекта, на который она ссылается, независимо от объявленного типа указателя или ссылки. Если она не является "виртуальной", метод разрешается "рано" и выбирается в соответствии с объявленным типом указателя или ссылки.
Виртуальные функции позволяют программе вызывать методы, которые не обязательно существуют на момент компиляции кода. [ необходима цитата ]
В C++ виртуальные методы объявляются путем добавления virtual
ключевого слова к объявлению функции в базовом классе. Этот модификатор наследуется всеми реализациями этого метода в производных классах, что означает, что они могут продолжать переопределять друг друга и иметь позднее связывание. И даже если методы, принадлежащие базовому классу, вызывают виртуальный метод, они вместо этого будут вызывать производный метод. Перегрузка происходит , когда два или более методов в одном классе имеют одинаковое имя метода, но разные параметры. Переопределение означает наличие двух методов с одинаковым именем метода и параметрами. Перегрузка также называется сопоставлением функций, а переопределение — динамическим отображением функций.
Например, базовый класс Animal
может иметь виртуальную функцию Eat
. Подкласс Llama
будет реализовываться Eat
иначе, чем подкласс Wolf
, но можно вызвать Eat
любой экземпляр класса, называемый Animal, и получить Eat
поведение конкретного подкласса.
class Animal { public : // Намеренно не виртуальный: void Move () { std :: cout << "Это животное движется каким-то образом" << std :: endl ; } virtual void Eat () = 0 ; }; // Класс "Животное" может иметь определение для Eat, если это необходимо. class Llama : public Animal { public : // Невиртуальная функция Move наследуется, но не переопределяется. void Eat () override { std :: cout << "Ламы едят траву!" << std :: endl ; } };
Это позволяет программисту обрабатывать список объектов класса Animal
, по очереди сообщая каждому из них о необходимости поесть (вызывая Eat
), без необходимости знать, какой вид животного может быть в списке, как каждое животное питается или каков может быть полный набор возможных типов животных.
В языке C механизм виртуальных функций может быть реализован следующим образом:
#include <stdio.h> /* объект указывает на свой класс... */ struct Animal { const struct AnimalVTable * vtable ; }; /* которая содержит виртуальную функцию Animal.Eat */ struct AnimalVTable { void ( * Eat )( struct Animal * self ); // 'виртуальная' функция }; /* Поскольку Animal.Move не является виртуальной функцией, она отсутствует в структуре выше. */ void Move ( const struct Animal * self ) { printf ( "<Animal at %p> перемещено некоторым образом \n " , ( void * )( self )); } /* в отличие от Move, который напрямую выполняет Animal.Move, Eat не может знать, какую функцию (если таковая имеется) вызывать во время компиляции. Animal.Eat может быть разрешен только во время выполнения, когда вызывается Eat. */ void Eat ( struct Animal * self ) { const struct AnimalVTable * vtable = * ( const void ** )( self ); if ( vtable -> Eat != NULL ) { ( * vtable -> Eat )( self ); // выполнить Animal.Eat } else { fprintf ( stderr , "виртуальный метод 'Eat' не реализован \n " ); } } /* реализация Llama.Eat это целевая функция , которая будет вызвана 'void Eat(struct Animal *self).' */ static void _Llama_eat ( struct Animal * self ) { printf ( "<Llama at %p> Лама ест траву! \n " , ( void * )( self )); } /* инициализация класса */ const struct AnimalVTable Animal = { NULL }; // базовый класс не реализует Animal.Eat const struct AnimalVTable Llama = { _Llama_eat }; // но производный класс реализует int main ( void ) { /* инициализирует объекты как экземпляры своего класса */ struct Animal animal = { & Animal }; struct Animal llama = { & Llama }; Move ( & animal ); // Animal.Move Move ( & llama ); // Llama.Move Eat ( & animal ); // не удается разрешить Animal.Eat, поэтому печатает "Not Implemented" в stderr Eat ( & llama ); // разрешает Llama.Eat и выполняет }
Чисто виртуальная функция или чисто виртуальный метод — это виртуальная функция, которая должна быть реализована производным классом, если производный класс не является абстрактным . Классы, содержащие чисто виртуальные методы, называются «абстрактными», и их нельзя инстанцировать напрямую. Подкласс абстрактного класса может быть инстанцирован напрямую, только если все унаследованные чисто виртуальные методы были реализованы этим классом или родительским классом. Чисто виртуальные методы обычно имеют объявление ( сигнатуру ) и не имеют определения ( реализации ).
Например, абстрактный базовый класс MathSymbol
может предоставлять чисто виртуальную функцию doOperation()
, а производные классы Plus
и Minus
реализовать doOperation()
для предоставления конкретных реализаций. Реализация doOperation()
не имела бы смысла в MathSymbol
классе, как и MathSymbol
абстрактная концепция, поведение которой определено исключительно для каждого данного вида (подкласса) MathSymbol
. Аналогично, данный подкласс MathSymbol
не был бы полным без реализации doOperation()
.
Хотя чисто виртуальные методы обычно не имеют реализации в классе, который их объявляет, чисто виртуальным методам в некоторых языках (например, C++ и Python) разрешено содержать реализацию в объявляющем их классе, обеспечивая резервное или стандартное поведение, которому производный класс может делегировать полномочия, если это уместно. [5] [6]
Чистые виртуальные функции также могут использоваться там, где объявления методов используются для определения интерфейса — аналогично тому, что явно указывает ключевое слово interface в Java. При таком использовании производные классы будут предоставлять все реализации. В таком шаблоне проектирования абстрактный класс, который служит интерфейсом, будет содержать только чистые виртуальные функции, но не будет содержать членов данных или обычных методов. В C++ использование таких чисто абстрактных классов в качестве интерфейсов работает, поскольку C++ поддерживает множественное наследование . Однако, поскольку многие языки ООП не поддерживают множественное наследование, они часто предоставляют отдельный механизм интерфейса. Примером является язык программирования Java .
Языки различаются по своему поведению во время работы конструктора или деструктора объекта. По этой причине вызов виртуальных функций в конструкторах обычно не рекомендуется.
В C++ вызывается «базовая» функция. В частности, вызывается самая производная функция, которая не является более производной, чем класс текущего конструктора или деструктора. [7] : §15.7.3 [8] [9] Если эта функция является чисто виртуальной функцией, то возникает неопределенное поведение . [7] : §13.4.6 [8] Это верно, даже если класс содержит реализацию для этой чисто виртуальной функции, поскольку вызов чисто виртуальной функции должен быть явно квалифицирован. [10] Соответствующая реализация C++ не требуется (и, как правило, не способна) обнаруживать косвенные вызовы чисто виртуальных функций во время компиляции или компоновки . Некоторые системы времени выполнения выдают ошибку вызова чисто виртуальной функции при обнаружении вызова чисто виртуальной функции во время выполнения .
В Java и C# вызывается производная реализация, но некоторые поля еще не инициализируются производным конструктором (хотя они инициализируются нулевыми значениями по умолчанию). [11] Некоторые шаблоны проектирования , такие как шаблон Abstract Factory , активно продвигают такое использование в языках, поддерживающих эту возможность.
Объектно-ориентированные языки обычно управляют выделением и освобождением памяти автоматически при создании и уничтожении объектов. Однако некоторые объектно-ориентированные языки позволяют реализовать пользовательский метод деструктора, если это необходимо. Если рассматриваемый язык использует автоматическое управление памятью, вызываемый пользовательский деструктор (обычно называемый финализатором в этом контексте) наверняка будет подходящим для рассматриваемого объекта. Например, если создается объект типа Wolf, который наследует Animal, и оба имеют пользовательские деструкторы, вызываемым будет тот, который объявлен в Wolf.
В контексте ручного управления памятью ситуация может быть более сложной, особенно в отношении статической диспетчеризации . Если объект типа Wolf создан, но на него указывает указатель Animal, и именно этот тип указателя Animal удаляется, вызываемый деструктор может быть фактически определенным для Animal, а не для Wolf, если только деструктор не является виртуальным. Это особенно касается C++, где поведение является распространенным источником ошибок программирования, если деструкторы не являются виртуальными.