Перейти к содержимому

Зачем нужны виртуальные функции в c

  • автор:

Зачем нужны виртуальные функции C++

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

Отслеживать
31k 13 13 золотых знаков 96 96 серебряных знаков 157 157 бронзовых знаков
задан 26 авг 2011 в 12:43
Егор Соколов Егор Соколов
797 15 15 серебряных знаков 29 29 бронзовых знаков

1 ответ 1

Сортировка: Сброс на вариант по умолчанию

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

Зачем нужны виртуальные функции? Для обеспечения полиморфизма — одного из базовых китов ООП, чтобы по ссылке/указателю на объект не определять его тип самостоятельно с помощью if’ов, а дать это делать компилятору и/или в runtime. Можно ещё в википедии почитать.

Отслеживать
ответ дан 26 авг 2011 в 12:52
112k 6 6 золотых знаков 93 93 серебряных знака 159 159 бронзовых знаков
>> иначе будут вызваны все деструкторы. Правильно: иначе НЕ будут вызваны все деструкторы
26 авг 2011 в 13:13
исправился
26 авг 2011 в 13:15

    Важное на Мете
Похожие

Подписаться на ленту

Лента вопроса

Для подписки на ленту скопируйте и вставьте эту ссылку в вашу программу для чтения RSS.

Дизайн сайта / логотип © 2024 Stack Exchange Inc; пользовательские материалы лицензированы в соответствии с CC BY-SA . rev 2024.1.3.2953

Нажимая «Принять все файлы cookie» вы соглашаетесь, что Stack Exchange может хранить файлы cookie на вашем устройстве и раскрывать информацию в соответствии с нашей Политикой в отношении файлов cookie.

Для чего нужны виртуальные функции?

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

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

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

Чтобы понять всю мощь идеи «один интерфейс — множество методов», рассмотрим следую­щую короткую программу. Она создает базовый класс figure. Этот класс используется для хране­ния размеров различных двумерных объектов и для вычисления их площадей. Функция set_dim() является стандартной функцией-членом, поскольку ее действия являются общими для всех произ­водных классов. Однако функция show_area() объявляется как виртуальная функция, поскольку способ вычисления площади каждого объекта является специфическим. Программа использует класс figure для вывода двух специфических классов square и triangle.

#include
class figure protected:
double x, y;
public:
void set_dim( double i, double j) x = i;
у = j;
>
virtual void show_area() cout cout >
>;
class triangle: public figure public:
void show_area() cout cout cout cout >
>;
class square: public figure public:
void show_area() cout cout cout cout >
>;
int main ( )
figure *p; /* создание указателя базового типа */
triangle t; /* создание объектов порожденных типов */
square s;
р = &t;
p->set_dim(10.0, 5.0);
p->show_area();
p = &s;
p->set_dim(10.0, 5.0);
p->show_area ();
return 0;
>

Как можно видеть на основе анализа этой программы, интерфейс классов square и triangle явля­ется одинаковым, хотя оба обеспечивают свои собственные методы для вычисления площади каж­дой из фигур. На основе объявления класса figure можно вывести класс circle, вычисляющий пло­щадь, ограниченную окружностью заданного радиуса. Для этого необходимо создать новый производный класс, в котором реализовано вычисление площади круга. Вся сила виртуальной функции основана на том факте, что можно легко вывести новый класс, разделяющий один и тот же общий интерфейс с другими подобными объектами. В качестве примера здесь показан один из способов реализации:

Прежде чем использовать класс circle, посмотрим внимательно на определение функции show_area(). Обратим внимание, что она использует только величину х, которая выражает ради­ус. Как известно, площадь круга вычисляется по формуле π R 2 . Однако функция set_dim(), опре­деленная в классе figure, требует не одного, а двух аргументов. Поскольку класс circle не нужда­ется во второй величине, то как же нам быть в данной ситуации?

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

Лучшее решение данной проблемы связано с использованием параметра у в set_dim() со значе­нием по умолчанию. В таком случае при вызове set_dim() для круга необходимо указать только радиус. При вызове set_dim() для треугольника или прямоугольника укажем обе величины. Ниже показана программа, реализующая этот подход:

#include
class figure protected:
double x, y;
public:
void set_dim (double i, double j=0) x = i;
y = j;
>
virtual void show_area() cout cout >
>;
class triangle: public figure public:
void show_area() cout cout cout cout >
>;
class square: public figure public:
void show_area() cout cout cout cout >
>;
class circle: public figure public:
void show_area() cout cout cout cout >
>;
int main ( )
figure *p; /* создание указателя базового типа */
triangle t; /* создание объектов порожденных типов */
square s;
circle с;
р = &t;
p->set_dim(10.0, 5.0);
p->show_area ();
p = &s;
p->set_dim(10.0, 5.0);
p->show_area ();
p = &c;
p->set_dim(9. 0) ;
p->show_area ();
return 0;
>

Этот пример также показывает, что при определении базового класса важно проявлять максималь­но возможную гибкость. Не следует налагать на программу какие-то ненужные ограничения.

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

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

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

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

// deriv_VirtualFunctions.cpp // compile with: /EHsc #include using namespace std; class Account < public: Account( double d ) < _balance = d; >virtual ~Account() <> virtual double GetBalance() < return _balance; >virtual void PrintBalance() < cerr private: double _balance; >; class CheckingAccount : public Account < public: CheckingAccount(double d) : Account(d) <>void PrintBalance() < cout >; class SavingsAccount : public Account < public: SavingsAccount(double d) : Account(d) <>void PrintBalance() < cout >; int main() < // Create objects of type CheckingAccount and SavingsAccount. CheckingAccount checking( 100.00 ); SavingsAccount savings( 1000.00 ); // Call PrintBalance using a pointer to Account. Account *pAccount = &checking; pAccount->PrintBalance(); // Call PrintBalance using a pointer to Account. pAccount = &savings; pAccount->PrintBalance(); > 

В предыдущем примере вызовы PrintBalance идентичны за исключением тех, на которые указывает объект pAccount . Поскольку функция PrintBalance виртуальная, вызывается версия функции, определенная для каждого объекта. Функция PrintBalance в производных классах CheckingAccount и SavingsAccount переопределяет функцию в базовом классе Account .

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

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

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

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

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

// deriv_VirtualFunctions2.cpp // compile with: /EHsc #include using namespace std; class Base < public: virtual void NameOf(); // Virtual function. void InvokingClass(); // Nonvirtual function. >; // Implement the two functions. void Base::NameOf() < cout void Base::InvokingClass() < cout class Derived : public Base < public: void NameOf(); // Virtual function. void InvokingClass(); // Nonvirtual function. >; // Implement the two functions. void Derived::NameOf() < cout void Derived::InvokingClass() < cout int main() < // Declare an object of type Derived. Derived aDerived; // Declare two pointers, one of type Derived * and the other // of type Base *, and initialize them to point to aDerived. Derived *pDerived = &aDerived; Base *pBase = &aDerived; // Call the functions. pBase->NameOf(); // Call virtual function. pBase->InvokingClass(); // Call nonvirtual function. pDerived->NameOf(); // Call virtual function. pDerived->InvokingClass(); // Call nonvirtual function. > 
Derived::NameOf Invoked by Base Derived::NameOf Invoked by Derived 

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

Так как виртуальные функции вызываются только для объектов типов классов, нельзя объявлять глобальные или статические функции как virtual .

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

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

Механизм вызова виртуальных функций можно подавить, явно указав имя функции с помощью оператора разрешения области действия ( :: ). Рассмотрим предыдущий пример с классом Account . Для вызова PrintBalance в базовом классе используйте следующий код.

CheckingAccount *pChecking = new CheckingAccount( 100.00 ); pChecking->Account::PrintBalance(); // Explicit qualification. Account *pAccount = pChecking; // Call Account::PrintBalance pAccount->Account::PrintBalance(); // Explicit qualification. 

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

Зачем нужны виртуальные функции в c

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

Когда вызовы функций фиксируются до выполнения программы на этапе компиляции, это называется статическим связыванием (static binding), либо ранним связыванием (early binding). При этом вызов функции через указатель определяется исключительно типом указателя, а не объектом, на который он указывает. Например:

#include class Person < public: Person(std::string name): name < >void print() const < std::cout private: std::string name; // имя >; class Employee: public Person < public: Employee(std::string name, std::string company): Person, company  < >void print() const < Person::print(); std::cout private: std::string company; // компания >; int main() < Person tom ; Person* person ; person->print(); // Name: Tom Employee bob ; person = &bob; person->print(); // Name: Bob >

В данном случае класс Employee наследуется от класса Person, но оба этих класса определяют функцию print() , которая выводит данные об объекте. В функции main создаем два объекта и поочередно присваиваем их указателю на тип Person и вызываем через этот указатель функцию print. Однако даже если этому указателю присваивается адрес объекта Employee, то все равно вызывает реализация функции из класса Person:

Employee bob ; person = &bob; person->print(); // Name: Bob

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

Name: Tom Name: Bob

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

Другой тип связывания представляет динамическое связывание (dynamic binding), еще называют поздним связыванием (late binding), которое позволяет на этапе выполнения решать, функцию какого типа вызвать. Для этого в языке С++ применяют виртуальные функции . Для определения виртуальной функции в базовом классе функция определяется с ключевым словом virtual . Причем данное ключевое слово можно применить к функции, если она определена внутри класса. А производный класс может переопределить ее поведение.

Итак, сделаем функцию print в базовом классе Person виртуальной:

#include class Person < public: Person(std::string name): name < >virtual void print() const // виртуальная функция < std::cout private: std::string name; >; class Employee: public Person < public: Employee(std::string name, std::string company): Person, company  < >void print() const < Person::print(); std::cout private: std::string company; >; int main() < Person tom ; Person* person ; person->print(); // Name: Tom Employee bob ; person = &bob; person->print(); // Name: Bob // Works in Microsoft >

Таким образом, базовый класс Person определяет виртуальную функцию print, а производный класс Employee переопределяет ее. В первом же примере, где функция print не была виртуальной, класс Employee не переопределял, а скрывал ее. Теперь при вызове функции print для объекта Employee через указатель Person* будет вызываться реализация функции именно класса Employee. Соответственно тепепрь мы получим другой консольный вывод:

Name: Tom Name: Bob Works in Microsoft

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

Класс, который определяет или наследует виртуальную функцию, еще назвается полиморфным (polymorphic class). То есть в данном случае Person и Employee являются полиморфными классами.

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

Employee bob ; Person p = bob; p.print(); // Name: Bob - статическое связывание

Динамическое связывание возможно только через указатель или ссылку.

Employee bob ; Person &p ; // присвоение ссылке p.print(); // динамическое связывание Person *ptr ; // присвоение адреса указателю ptr->print(); // динамическое связывание

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

Также статические функции не могут быть виртуальными.

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

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

#include class Person < public: Person(std::string name): name < >virtual void print() const // виртуальная функция < std::cout private: std::string name; >; class Employee: public Person < public: Employee(std::string name, std::string company): Person, company  < >void print() const override // явным образом указываем, что функция переопределена < Person::print(); std::cout private: std::string company; >; int main() < Person tom ; Person* person ; person->print(); // Name: Tom Employee bob ; person = &bob; person->print(); // Name: Bob // Works in Microsoft >

То есть здесь выражение

void print() const override

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

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

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

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

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

vtable, полиформизм и виртуальные функции в C++

  1. Указатель на vtable в объекте используется для поиска адреса vtable для класса.
  2. Затем в таблице идет поиск указателя на вызываемую виртуальную функцию.
  3. Через найденный указатель функции в vtable вызывается сама функция. В итоге вызов виртуальной функции происходит немного медленнее, чем прямой вызов невиртуальной функции, поэтому каждое объявление и вызов виртуальной функции несет некоторые накладные расходы.

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

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

class Person < public: virtual void print() const final < >>; class Employee : public Person < public: void print() const override // Ошибка. < >>;

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

class Person < public: virtual void print() const // переопределение разрешено < >>; class Employee : public Person < public: void print() const override final // в классах, производных от Employee переопределение запрещено < >>;

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

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