Зачем нам нужны виртуальные функции в C++?
Я изучаю C++, и я только вхожу в виртуальные функции.
Из того, что я прочитал (в книге и онлайн), виртуальные функции - это функции в базовом классе, которые вы можете переопределить в производных классах.
Но ранее в книге, изучая базовое наследование, я смог переопределить базовые функции в производных классах без использования virtual
,
Так чего мне здесь не хватает? Я знаю, что есть еще что-то для виртуальных функций, и это кажется важным, поэтому я хочу уточнить, что это такое. Я просто не могу найти прямой ответ в Интернете.
28 ответов
Вот как я понял не только то, что virtual
функции есть, но зачем они нужны:
Допустим, у вас есть эти два класса:
class Animal
{
public:
void eat() { std::cout << "I'm eating generic food."; }
};
class Cat : public Animal
{
public:
void eat() { std::cout << "I'm eating a rat."; }
};
В вашей основной функции:
Animal *animal = new Animal;
Cat *cat = new Cat;
animal->eat(); // Outputs: "I'm eating generic food."
cat->eat(); // Outputs: "I'm eating a rat."
Пока все хорошо, правда? Животные едят дженерики, кошки едят крыс, все без virtual
,
Давайте немного изменим это сейчас, чтобы eat()
вызывается через промежуточную функцию (тривиальная функция только для этого примера):
// This can go at the top of the main.cpp file
void func(Animal *xyz) { xyz->eat(); }
Теперь наша основная функция:
Animal *animal = new Animal;
Cat *cat = new Cat;
func(animal); // Outputs: "I'm eating generic food."
func(cat); // Outputs: "I'm eating generic food."
Э-э-э... мы передали Кота в func()
, но он не будет есть крыс. Если вы перегружены func()
так что требуется Cat*
? Если вам нужно извлечь больше животных из животных, им всем понадобится свой func()
,
Решение состоит в том, чтобы сделать eat()
от Animal
Класс виртуальной функции:
class Animal
{
public:
virtual void eat() { std::cout << "I'm eating generic food."; }
};
class Cat : public Animal
{
public:
void eat() { std::cout << "I'm eating a rat."; }
};
Главный:
func(animal); // Outputs: "I'm eating generic food."
func(cat); // Outputs: "I'm eating a rat."
Готово.
Без "виртуального" вы получаете "раннее связывание". Какая реализация метода используется, определяется во время компиляции на основе типа указателя, через который вы вызываете.
С "виртуальным" вы получаете "позднее связывание". Какая реализация метода используется, определяется во время выполнения в зависимости от типа указываемого объекта - как он был изначально создан. Это не обязательно то, что вы думаете, основываясь на типе указателя, который указывает на этот объект.
class Base
{
public:
void Method1 () { std::cout << "Base::Method1" << std::endl; }
virtual void Method2 () { std::cout << "Base::Method2" << std::endl; }
};
class Derived : public Base
{
public:
void Method1 () { std::cout << "Derived::Method1" << std::endl; }
void Method2 () { std::cout << "Derived::Method2" << std::endl; }
};
Base* obj = new Derived ();
// Note - constructed as Derived, but pointer stored as Base*
obj->Method1 (); // Prints "Base::Method1"
obj->Method2 (); // Prints "Derived::Method2"
РЕДАКТИРОВАТЬ - см. Этот вопрос.
Также - этот урок охватывает раннее и позднее связывание в C++.
Вам нужен как минимум 1 уровень наследования и принижение, чтобы продемонстрировать это. Вот очень простой пример:
class Animal
{
public:
// turn the following virtual modifier on/off to see what happens
//virtual
std::string Says() { return "?"; }
};
class Dog: public Animal
{
public: std::string Says() { return "Woof"; }
};
void test()
{
Dog* d = new Dog();
Animal* a = d; // refer to Dog instance with Animal pointer
cout << d->Says(); // always Woof
cout << a->Says(); // Woof or ?, depends on virtual
}
Виртуальные функции используются для поддержки полиморфизма времени выполнения.
То есть виртуальное ключевое слово говорит компилятору не принимать решение (о привязке функции) во время компиляции, а скорее отложить его на время выполнения ".
Вы можете сделать функцию виртуальной, предшествуя ключевому слову
virtual
в объявлении базового класса. Например,class Base { virtual void func(); }
Когда базовый класс имеет виртуальную функцию-член, любой класс, который наследуется от базового класса, может переопределить функцию с точно таким же прототипом, т. Е. Может быть переопределена только функциональность, а не интерфейс функции.
class Derive : public Base { void func(); }
Указатель базового класса может использоваться для указания на объект базового класса, а также на объект производного класса.
- Когда виртуальная функция вызывается с использованием указателя базового класса, компилятор решает во время выполнения, какая версия функции - то есть версия базового класса или переопределенная версия производного класса - должна быть вызвана. Это называется полиморфизмом времени выполнения.
Вам нужны виртуальные методы для безопасного даункинга, простоты и лаконичности.
Это то, что делают виртуальные методы: они снижают безопасность с помощью, по-видимому, простого и лаконичного кода, избегая небезопасного ручного преобразования в более сложный и подробный код, который вы могли бы иметь в противном случае.
Не виртуальный метод ⇒ статическое связывание
Следующий код намеренно "неверен". Это не объявляет value
метод как virtual
и, следовательно, выдает непреднамеренный "неправильный" результат, а именно 0:
#include <iostream>
using namespace std;
class Expression
{
public:
auto value() const
-> double
{ return 0.0; } // This should never be invoked, really.
};
class Number
: public Expression
{
private:
double number_;
public:
auto value() const
-> double
{ return number_; } // This is OK.
Number( double const number )
: Expression()
, number_( number )
{}
};
class Sum
: public Expression
{
private:
Expression const* a_;
Expression const* b_;
public:
auto value() const
-> double
{ return a_->value() + b_->value(); } // Uhm, bad! Very bad!
Sum( Expression const* const a, Expression const* const b )
: Expression()
, a_( a )
, b_( b )
{}
};
auto main() -> int
{
Number const a( 3.14 );
Number const b( 2.72 );
Number const c( 1.0 );
Sum const sum_ab( &a, &b );
Sum const sum( &sum_ab, &c );
cout << sum.value() << endl;
}
В строке прокомментировал как "плохой" Expression::value
метод вызывается, потому что статически известный тип (тип, известный во время компиляции) Expression
и value
метод не виртуальный.
Виртуальный метод ⇒ динамическое связывание.
декларирование value
как virtual
в статически известном виде Expression
гарантирует, что каждый вызов будет проверять, что это за фактический тип объекта, и вызывает соответствующую реализацию value
для этого динамического типа:
#include <iostream>
using namespace std;
class Expression
{
public:
virtual
auto value() const -> double
= 0;
};
class Number
: public Expression
{
private:
double number_;
public:
auto value() const -> double
override
{ return number_; }
Number( double const number )
: Expression()
, number_( number )
{}
};
class Sum
: public Expression
{
private:
Expression const* a_;
Expression const* b_;
public:
auto value() const -> double
override
{ return a_->value() + b_->value(); } // Dynamic binding, OK!
Sum( Expression const* const a, Expression const* const b )
: Expression()
, a_( a )
, b_( b )
{}
};
auto main() -> int
{
Number const a( 3.14 );
Number const b( 2.72 );
Number const c( 1.0 );
Sum const sum_ab( &a, &b );
Sum const sum( &sum_ab, &c );
cout << sum.value() << endl;
}
Здесь вывод 6.86
как и должно быть, поскольку виртуальный метод вызывается виртуально. Это также называется динамическим связыванием вызовов. Выполняется небольшая проверка, находим фактический динамический тип объекта и вызывается соответствующая реализация метода для этого динамического типа.
Соответствующая реализация относится к наиболее конкретному (наиболее производному) классу.
Обратите внимание, что реализации методов в производных классах здесь не отмечены virtual
, но вместо этого отмечены override
, Они могут быть отмечены virtual
но они автоматически виртуальные. override
Ключевое слово гарантирует, что если в некотором базовом классе нет такого виртуального метода, вы получите ошибку (что желательно).
Безобразие делать это без виртуальных методов
Без virtual
нужно было бы реализовать некоторую версию динамического связывания Do It Yourself. Именно это, как правило, связано с небезопасным ручным понижением, сложностью и многословностью.
Для случая отдельной функции, как здесь, достаточно сохранить указатель на функцию в объекте и вызвать ее через этот указатель на функцию, но даже в этом случае он требует небезопасных понижений, сложности и многословия, а именно:
#include <iostream>
using namespace std;
class Expression
{
protected:
typedef auto Value_func( Expression const* ) -> double;
Value_func* value_func_;
public:
auto value() const
-> double
{ return value_func_( this ); }
Expression(): value_func_( nullptr ) {} // Like a pure virtual.
};
class Number
: public Expression
{
private:
double number_;
static
auto specific_value_func( Expression const* expr )
-> double
{ return static_cast<Number const*>( expr )->number_; }
public:
Number( double const number )
: Expression()
, number_( number )
{ value_func_ = &Number::specific_value_func; }
};
class Sum
: public Expression
{
private:
Expression const* a_;
Expression const* b_;
static
auto specific_value_func( Expression const* expr )
-> double
{
auto const p_self = static_cast<Sum const*>( expr );
return p_self->a_->value() + p_self->b_->value();
}
public:
Sum( Expression const* const a, Expression const* const b )
: Expression()
, a_( a )
, b_( b )
{ value_func_ = &Sum::specific_value_func; }
};
auto main() -> int
{
Number const a( 3.14 );
Number const b( 2.72 );
Number const c( 1.0 );
Sum const sum_ab( &a, &b );
Sum const sum( &sum_ab, &c );
cout << sum.value() << endl;
}
Один из положительных способов взглянуть на это так: если вы сталкиваетесь с небезопасным снижением рейтинга, сложностью и многословностью, как описано выше, то зачастую виртуальный метод или методы действительно могут помочь.
Если базовый класс Base
и производный класс Der
Вы можете иметь Base *p
указатель, который на самом деле указывает на экземпляр Der
, Когда вы звоните p->foo();
, если foo
не является виртуальным, то Base
его версия выполняется, игнорируя тот факт, что p
на самом деле указывает на Der
, Если Foo является виртуальным, p->foo()
выполняет "крайнюю" переопределение foo
, полностью учитывая фактический класс намеченного пункта. Таким образом, различие между виртуальным и не виртуальным на самом деле очень важно: первое допускает полиморфизм во время выполнения, основную концепцию ОО-программирования, а второе - нет.
Я хотел бы добавить еще одно использование виртуальной функции, хотя она использует ту же концепцию, что и приведенные выше ответы, но, думаю, стоит упомянуть.
ВИРТУАЛЬНЫЙ РАЗРУШИТЕЛЬ
Рассмотрим эту программу ниже, не объявляя деструктор Базового класса как виртуальный; память для кошки не может быть очищена.
class Animal {
public:
~Animal() {
cout << "Deleting an Animal" << endl;
}
};
class Cat:public Animal {
public:
~Cat() {
cout << "Deleting an Animal name Cat" << endl;
}
};
int main() {
Animal *a = new Cat();
delete a;
return 0;
}
Выход:
Deleting an Animal
class Animal {
public:
virtual ~Animal() {
cout << "Deleting an Animal" << endl;
}
};
class Cat:public Animal {
public:
~Cat(){
cout << "Deleting an Animal name Cat" << endl;
}
};
int main() {
Animal *a = new Cat();
delete a;
return 0;
}
Выход:
Deleting an Animal name Cat Deleting an Animal
Потребность в виртуальной функции объяснена [легко понять]
#include<iostream>
using namespace std;
class A{
public:
void show(){
cout << " Hello from Class A";
}
};
class B :public A{
public:
void show(){
cout << " Hello from Class B";
}
};
int main(){
A *a1 = new B; // Create a base class pointer and assign address of derived object.
a1->show();
}
Выход будет:
Hello from Class A.
Но с виртуальной функцией:
#include<iostream>
using namespace std;
class A{
public:
virtual void show(){
cout << " Hello from Class A";
}
};
class B :public A{
public:
virtual void show(){
cout << " Hello from Class B";
}
};
int main(){
A *a1 = new B;
a1->show();
}
Выход будет:
Hello from Class B.
Следовательно, с виртуальной функцией вы можете достичь полиморфизма во время выполнения.
Вы должны различать перегрузку и перегрузку. Без virtual
Ключевое слово вы только перегружаете метод базового класса. Это ничего не значит, кроме сокрытия. Допустим, у вас есть базовый класс Base
и производный класс Specialized
которые оба реализуют void foo()
, Теперь у вас есть указатель на Base
указывая на случай Specialized
, Когда вы звоните foo()
на нем вы можете увидеть разницу, что virtual
делает: если метод является виртуальным, реализация Specialized
будет использоваться, если она отсутствует, версия из Base
будет выбран. Лучше никогда не перегружать методы из базового класса. Создание метода, не являющегося виртуальным, позволяет его автору сказать, что его расширение в подклассах не предназначено.
У меня есть мой ответ в форме беседы, чтобы быть лучше прочитанным:
Зачем нам нужны виртуальные функции?
Из-за полиморфизма.
Что такое полиморфизм?
Тот факт, что базовый указатель также может указывать на объекты производного типа.
Как это определение полиморфизма приводит к необходимости виртуальных функций?
Ну, через раннее связывание.
Что такое раннее связывание?
Раннее связывание (связывание во время компиляции) в C++ означает, что вызов функции фиксируется до выполнения программы.
Так...?
Таким образом, если вы используете базовый тип в качестве параметра функции, компилятор распознает только базовый интерфейс, и если вы вызываете эту функцию с любыми аргументами из производных классов, она отсекается, что не должно происходить.
Если это не то, что мы хотим, почему это разрешено?
Потому что нам нужен полиморфизм!
Какая польза от полиморфизма?
Вы можете использовать указатель базового типа в качестве параметра отдельной функции, а затем во время выполнения вашей программы вы можете получить доступ к каждому из интерфейсов производного типа (например, к их функциям-членам) без каких-либо проблем, используя разыменование этого единственного объекта. базовый указатель.
Я до сих пор не знаю, для чего нужны виртуальные функции...! И это был мой первый вопрос!
ну, это потому что ты задал свой вопрос слишком рано!
Зачем нам нужны виртуальные функции?
Предположим, что вы вызвали функцию с базовым указателем, у которого был адрес объекта из одного из его производных классов. Как мы говорили об этом выше, во время выполнения этот указатель разыменовывается, но пока все хорошо, однако мы ожидаем, что метод (== функция-член) "из нашего производного класса" будет выполнен! Тем не менее, в базовом классе уже определен тот же метод (с тем же заголовком), так почему ваша программа должна выбрать другой метод? Другими словами, я имею в виду, как вы можете отличить этот сценарий от того, что мы обычно видели раньше?
Краткий ответ "Виртуальная функция-член в базе", и более длинный ответ: "На этом этапе, если программа видит виртуальную функцию в базовом классе, она знает (понимает), что вы пытаетесь использовать "полиморфизм" и т. д. переходит к производным классам (используя v-таблицу, форму позднего связывания), чтобы найти этот другой метод с тем же заголовком, но, как ожидается, с другой реализацией
Почему другая реализация?
Ты с головой! Иди почитай хорошую книгу!
Хорошо, подожди, подожди, подожди, зачем использовать базовые указатели, если он / она может просто использовать указатели производного типа? Будь судьей, стоит ли вся эта головная боль? Посмотрите на эти два фрагмента:
// 1:
Parent* p1 = &boy;
p1 -> task();
Parent* p2 = &girl;
p2 -> task();
// 2:
Boy* p1 = &boy;
p1 -> task();
Girl* p2 = &girl;
p2 -> task();
Хорошо, хотя я думаю, что 1 все же лучше, чем 2, вы можете написать 1 так же:
// 1:
Parent* p1 = &boy;
p1 -> task();
p1 = &girl;
p1 -> task();
и более того, вы должны знать, что это всего лишь надуманное использование всего того, что я вам объяснил до сих пор. Вместо этого, предположим, например, ситуацию, в которой у вас была функция в вашей программе, которая использовала методы из каждого из производных классов соответственно (getMonthBenefit()):
double totalMonthBenefit = 0;
std::vector<CentralShop*> mainShop = { &shop1, &shop2, &shop3, &shop4, &shop5, &shop6};
for(CentralShop* x : mainShop){
totalMonthBenefit += x -> getMonthBenefit();
}
Теперь попробуйте переписать это без головной боли!
double totalMonthBenefit=0;
Shop1* branch1 = &shop1;
Shop2* branch2 = &shop2;
Shop3* branch3 = &shop3;
Shop4* branch4 = &shop4;
Shop5* branch5 = &shop5;
Shop6* branch6 = &shop6;
totalMonthBenefit += branch1 -> getMonthBenefit();
totalMonthBenefit += branch2 -> getMonthBenefit();
totalMonthBenefit += branch3 -> getMonthBenefit();
totalMonthBenefit += branch4 -> getMonthBenefit();
totalMonthBenefit += branch5 -> getMonthBenefit();
totalMonthBenefit += branch6 -> getMonthBenefit();
И на самом деле, это может быть еще и надуманным примером!
Зачем нам нужны виртуальные методы в C++?
Быстрый ответ:
- Он предоставляет нам один из необходимых "ингредиентов" 1 для объектно-ориентированного программирования.
В Bjarne Stroustrup C++ Программирование: принципы и практика, (14.3):
Виртуальная функция предоставляет возможность определить функцию в базовом классе и иметь функцию с тем же именем и типом в производном классе, вызываемую, когда пользователь вызывает функцию базового класса. Это часто называют полиморфизмом во время выполнения, динамической диспетчеризацией или диспетчеризацией во время выполнения, потому что вызываемая функция определяется во время выполнения на основе типа используемого объекта.
- Это самая быстрая и эффективная реализация, если вам нужен вызов виртуальной функции 2.
Для обработки виртуального вызова требуется один или несколько фрагментов данных, связанных с производным объектом 3. Обычно это делается путем добавления адреса таблицы функций. Эту таблицу обычно называют виртуальной таблицей или таблицей виртуальных функций, а ее адрес часто называют виртуальным указателем. Каждая виртуальная функция получает слот в виртуальной таблице. В зависимости от типа объекта (производного) вызывающего, виртуальная функция, в свою очередь, вызывает соответствующее переопределение.
1. Использование наследования, полиморфизма во время выполнения и инкапсуляции является наиболее распространенным определением объектно-ориентированного программирования.
2. Вы не можете кодировать функциональность, чтобы быть быстрее или использовать меньше памяти, используя другие языковые функции для выбора среди альтернатив во время выполнения. Бьярн Страуструп C++ Программирование: принципы и практика. (14.3.1).
3. Что-то, чтобы сказать, какая функция действительно вызывается, когда мы вызываем базовый класс, содержащий виртуальную функцию.
Когда у вас есть функция в базовом классе, вы можете Redefine
или же Override
это в производном классе.
Переопределение метода. В производном классе дается новая реализация метода базового класса. Не облегчает Dynamic binding
,
Переопределение метода:Redefining
virtual method
базового класса в производном классе. Виртуальный метод облегчает динамическое связывание.
Итак, когда вы сказали:
Но ранее в книге, изучая базовое наследование, я смог переопределить базовые методы в производных классах без использования "виртуального".
вы не переопределяли его, так как метод в базовом классе не был виртуальным, скорее вы переопределяли его
Это помогает, если вы знаете основные механизмы. C++ формализует некоторые методы кодирования, используемые программистами C, "классы" заменяются на "оверлеи" - структуры с общими разделами заголовка будут использоваться для обработки объектов различных типов, но с некоторыми общими данными или операциями. Обычно базовая структура оверлея (общая часть) имеет указатель на таблицу функций, которая указывает на различный набор процедур для каждого типа объекта. C++ делает то же самое, но скрывает механизмы, то есть C++ ptr->func(...)
где func виртуален как C (*ptr->func_table[func_num])(ptr,...)
где то, что изменяется между производными классами, это содержимое func_table. [Не виртуальный метод ptr->func() просто переводится как mangled_func(ptr,..).]
В результате вам нужно только понять базовый класс, чтобы вызывать методы производного класса, т.е. если подпрограмма понимает класс A, вы можете передать ей указатель производного класса B, тогда будут вызваны виртуальные методы. B, а не A, так как вы проходите через таблицу функций B указывает на.
Ключевое слово virtual сообщает компилятору, что он не должен выполнять раннее связывание. Вместо этого он должен автоматически установить все механизмы, необходимые для выполнения позднего связывания. Для этого типичный компилятор1 создает одну таблицу (называемую VTABLE) для каждого класса, содержащего виртуальные функции. Компилятор помещает адреса виртуальных функций для этого конкретного класса в VTABLE. В каждом классе с виртуальными функциями он тайно размещает указатель, называемый vpointer (сокращенно VPTR), который указывает на VTABLE для этого объекта. Когда вы делаете виртуальный вызов функции через указатель базового класса, компилятор незаметно вставляет код для извлечения VPTR и поиска адреса функции в VTABLE, вызывая, таким образом, правильную функцию и вызывая позднее связывание.
Более подробная информация в этой ссылке http://cplusplusinterviews.blogspot.sg/2015/04/virtual-mechanism.html
ООП Ответ: Подтип Полиморфизм
В C++ виртуальные методы необходимы для реализации полиморфизма, точнее подтипирования или полиморфизма подтипов, если вы применяете определение из википедии.
Wikipedia, Subtyping, 2019-01-09: В теории языка программирования подтип (также полиморфизм подтипов или полиморфизм включения) является формой полиморфизма типов, в которой подтип является типом данных, который по некоторому понятию связан с другим типом данных (супертипом). заменяемости, означая, что программные элементы, обычно подпрограммы или функции, написанные для работы с элементами супертипа, также могут работать с элементами подтипа.
ПРИМЕЧАНИЕ. Подтип означает базовый класс, а подтип - унаследованный класс.
Дальнейшее чтение о полиморфизме подтипа
- https://en.wikipedia.org/wiki/Subtyping
- https://en.wikipedia.org/wiki/Polymorphism_(computer_science)
Технический ответ: динамическая отправка
Если у вас есть указатель на базовый класс, то вызов метода (который объявлен как виртуальный) будет отправлен методу фактического класса создаваемого объекта. Вот как реализуется полиморфизм подтипа C++.
Дальнейшее чтение Полиморфизм в C++ и Dynamic Dispatch
- http://www.cplusplus.com/doc/tutorial/polymorphism/
- https://en.cppreference.com/w/cpp/language/virtual
Реализация Ответ: создает запись vtable
Для каждого модификатора, "виртуального" в методах, компиляторы C++ обычно создают запись в vtable класса, в котором объявлен метод. Вот как обычный компилятор C++ реализует Dynamic Dispatch.
Дальнейшее чтение vtables
Пример кода
#include <iostream>
using namespace std;
class Animal {
public:
virtual void MakeTypicalNoise() = 0; // no implementation needed, for abstract classes
virtual ~Animal(){};
};
class Cat : public Animal {
public:
virtual void MakeTypicalNoise()
{
cout << "Meow!" << endl;
}
};
class Dog : public Animal {
public:
virtual void MakeTypicalNoise() { // needs to be virtual, if subtype polymorphism is also needed for Dogs
cout << "Woof!" << endl;
}
};
class Doberman : public Dog {
public:
virtual void MakeTypicalNoise() {
cout << "Woo, woo, woow!";
cout << " ... ";
Dog::MakeTypicalNoise();
}
};
int main() {
Animal* apObject[] = { new Cat(), new Dog(), new Doberman() };
const int cnAnimals = sizeof(apObject)/sizeof(Animal*);
for ( int i = 0; i < cnAnimals; i++ ) {
apObject[i]->MakeTypicalNoise();
}
for ( int i = 0; i < cnAnimals; i++ ) {
delete apObject[i];
}
return 0;
}
Вывод примера кода
Meow!
Woof!
Woo, woo, woow! ... Woof!
Диаграмма классов UML примера кода
Ключевое слово virtual заставляет компилятор выбирать реализацию метода, определенную в классе объекта, а не в классе указателя.
Shape *shape = new Triangle();
cout << shape->getName();
В приведенном выше примере Shape::getName будет вызываться по умолчанию, если только getName() не определен как виртуальный в базовом классе Shape. Это заставляет компилятор искать реализацию getName() в классе Triangle, а не в классе Shape.
Виртуальная таблица - это механизм, в котором компилятор отслеживает различные реализации подклассов виртуальных методов. Это также называется динамической диспетчеризацией, и с ней связаны некоторые накладные расходы.
Наконец, почему виртуальная среда даже необходима в C++, почему бы не сделать ее поведение по умолчанию, как в Java?
- C++ основан на принципах "Нулевые накладные расходы" и "Платите за то, что вы используете". Так что он не пытается выполнить динамическую диспетчеризацию для вас, если вам это не нужно.
- Чтобы обеспечить больше контроля над интерфейсом. Делая функцию не виртуальной, интерфейс / абстрактный класс может управлять поведением во всех его реализациях.
Проблема с объяснениями виртуальных функций заключается в том, что они не объясняют, как они используются на практике и как они помогают с ремонтопригодностью. Я создал учебник по виртуальным функциям, который люди уже сочли очень полезным. Кроме того, он основан на предпосылке поля боя, что делает его немного более захватывающим: https://nrecursions.blogspot.com/2015/06/so-why-do-we-need-virtual-functions.html.
Рассмотрим это приложение на поле боя:
#include "iostream"
//This class is created by Gun1's company
class Gun1 {public: void fire() {std::cout<<"gun1 firing now\n";}};
//This class is created by Gun2's company
class Gun2 {public: void shoot() {std::cout<<"gun2 shooting now\n";}};
//We create an abstract class to interface with WeaponController
class WeaponsInterface {
public:
virtual void shootTarget() = 0;
};
//A wrapper class to encapsulate Gun1's shooting function
class WeaponGun1 : public WeaponsInterface {
private:
Gun1* g;
public:
WeaponGun1(): g(new Gun1()) {}
~WeaponGun1() { delete g;}
virtual void shootTarget() { g->fire(); }
};
//A wrapper class to encapsulate Gun2's shooting function
class WeaponGun2 : public WeaponsInterface {
private:
Gun2* g;
public:
WeaponGun2(): g(new Gun2()) {}
~WeaponGun2() { delete g;}
virtual void shootTarget() { g->shoot(); }
};
class WeaponController {
private:
WeaponsInterface* w;
WeaponGun1* g1;
WeaponGun2* g2;
public:
WeaponController() {g1 = new WeaponGun1(); g2 = new WeaponGun2(); w = g1;}
~WeaponController() {delete g1; delete g2;}
void shootTarget() { w->shootTarget();}
void changeGunTo(int gunNumber) {//Virtual functions makes it easy to change guns dynamically
switch(gunNumber) {
case 1: w = g1; break;
case 2: w = g2; break;
}
}
};
class BattlefieldSoftware {
private:
WeaponController* wc;
public:
BattlefieldSoftware() : wc(new WeaponController()) {}
~BattlefieldSoftware() { delete wc; }
void shootTarget() { wc->shootTarget(); }
void changeGunTo(int gunNumber) {wc->changeGunTo(gunNumber); }
};
int main() {
BattlefieldSoftware* bf = new BattlefieldSoftware();
bf->shootTarget();
for(int i = 2; i > 0; i--) {
bf->changeGunTo(i);
bf->shootTarget();
}
delete bf;
}
Я рекомендую вам сначала прочитать сообщение в блоге, чтобы понять, почему были созданы классы-оболочки.
Как видно на изображении, существуют различные пушки / ракеты, которые могут быть подключены к программному обеспечению поля боя, и этому оружию могут быть даны команды для стрельбы или повторной калибровки и т. Д. Проблема здесь заключается в том, чтобы иметь возможность изменить / заменить оружие / ракеты без необходимости вносить изменения в программное обеспечение синего поля боя и иметь возможность переключаться между оружием во время выполнения без необходимости вносить изменения в код и повторно компилировать.
В приведенном выше коде показано, как решается проблема и как виртуальные функции с хорошо спроектированными классами-оболочками могут инкапсулировать функции и помогать в назначении указателей производных классов во время выполнения. Создание классаWeaponGun1
гарантирует, что вы полностью разделили обработку Gun1
в класс. Что бы вы ни делали сGun1
, вам нужно будет только внести изменения в WeaponGun1
и будьте уверены, что никакие другие классы не затронуты.
Из-за WeaponsInterface
class, теперь вы можете назначить любой производный класс указателю базового класса WeaponsInterface
и поскольку его функции виртуальны, когда вы вызываете WeaponsInterface
с shootTarget
, производный класс shootTarget
вызывается.
Самое приятное то, что вы можете менять оружие во время выполнения (w=g1
а также w=g2
). Это главное преимущество виртуальных функций, и поэтому нам нужны виртуальные функции.
Так что больше нет необходимости комментировать код в разных местах при смене оружия. Теперь это простая и понятная процедура, и добавить больше классов оружия также проще, потому что нам просто нужно создать новыйWeaponGun3
или WeaponGun4
класс, и мы можем быть уверены, что это не испортит BattlefieldSoftware
код или WeaponGun1
/WeaponGun2
код.
Зачем нам нужны виртуальные функции?
Виртуальные функции избегают ненужных проблем с типизацией, и некоторые из нас могут спорить, зачем нам нужны виртуальные функции, когда мы можем использовать указатель производного класса для вызова функции, специфичной для производного класса! Ответ - это сводит на нет всю идею наследования в большой системе развитие, где наличие одного объекта указателя базового класса является очень желательным.
Давайте сравним ниже две простые программы, чтобы понять важность виртуальных функций:
Программа без виртуальных функций:
#include <iostream>
using namespace std;
class father
{
public: void get_age() {cout << "Fathers age is 50 years" << endl;}
};
class son: public father
{
public : void get_age() { cout << "son`s age is 26 years" << endl;}
};
int main(){
father *p_father = new father;
son *p_son = new son;
p_father->get_age();
p_father = p_son;
p_father->get_age();
p_son->get_age();
return 0;
}
ВЫХОД:
Fathers age is 50 years
Fathers age is 50 years
son`s age is 26 years
Программа с виртуальной функцией:
#include <iostream>
using namespace std;
class father
{
public:
virtual void get_age() {cout << "Fathers age is 50 years" << endl;}
};
class son: public father
{
public : void get_age() { cout << "son`s age is 26 years" << endl;}
};
int main(){
father *p_father = new father;
son *p_son = new son;
p_father->get_age();
p_father = p_son;
p_father->get_age();
p_son->get_age();
return 0;
}
ВЫХОД:
Fathers age is 50 years
son`s age is 26 years
son`s age is 26 years
Внимательно проанализировав оба выхода, можно понять важность виртуальных функций.
Я думаю, что вы имеете в виду тот факт, что после объявления метода виртуальным вам не нужно использовать ключевое слово "virtual" в переопределениях.
class Base { virtual void foo(); };
class Derived : Base
{
void foo(); // this is overriding Base::foo
};
Если вы не используете 'virtual' в объявлении foo для Base, тогда Derived foo просто будет его скрывать.
Вот полный пример, который иллюстрирует, почему используется виртуальный метод.
#include <iostream>
using namespace std;
class Basic
{
public:
virtual void Test1()
{
cout << "Test1 from Basic." << endl;
}
virtual ~Basic(){};
};
class VariantA : public Basic
{
public:
void Test1()
{
cout << "Test1 from VariantA." << endl;
}
};
class VariantB : public Basic
{
public:
void Test1()
{
cout << "Test1 from VariantB." << endl;
}
};
int main()
{
Basic *object;
VariantA *vobjectA = new VariantA();
VariantB *vobjectB = new VariantB();
object=(Basic *) vobjectA;
object->Test1();
object=(Basic *) vobjectB;
object->Test1();
delete vobjectA;
delete vobjectB;
return 0;
}
Виртуальные методы используются в дизайне интерфейса. Например, в Windows есть интерфейс под названием IUnknown, как показано ниже:
interface IUnknown {
virtual HRESULT QueryInterface (REFIID riid, void **ppvObject) = 0;
virtual ULONG AddRef () = 0;
virtual ULONG Release () = 0;
};
Эти методы оставлены пользователю интерфейса для реализации. Они необходимы для создания и уничтожения определенных объектов, которые должны наследовать IUnknown. В этом случае среда выполнения знает о трех методах и ожидает, что они будут реализованы при вызове. Таким образом, в некотором смысле они действуют как договор между самим объектом и тем, что использует этот объект.
Что касается эффективности, виртуальные функции несколько менее эффективны, чем функции раннего связывания.
"Этот механизм виртуального вызова можно сделать почти таким же эффективным, как механизм" нормального вызова функции "(в пределах 25%). Его служебная память занимает один указатель в каждом объекте класса с виртуальными функциями плюс один vtbl для каждого такого класса" [A тур по С ++ Бьярном Страуструпом]
Вы знакомы с указателями функций? Виртуальные функции представляют собой аналогичную идею, за исключением того, что вы можете легко привязать данные к виртуальным функциям (в качестве членов класса). Привязать данные к указателям на функцию не так просто. Для меня это главное концептуальное отличие. Многие другие ответы здесь просто говорят "потому что... полиморфизм!"
Вот объединенная версия кода C++ для первых двух ответов.
#include <iostream>
#include <string>
using namespace std;
class Animal
{
public:
#ifdef VIRTUAL
virtual string says() { return "??"; }
#else
string says() { return "??"; }
#endif
};
class Dog: public Animal
{
public:
string says() { return "woof"; }
};
string func(Animal *a)
{
return a->says();
}
int main()
{
Animal *a = new Animal();
Dog *d = new Dog();
Animal *ad = d;
cout << "Animal a says\t\t" << a->says() << endl;
cout << "Dog d says\t\t" << d->says() << endl;
cout << "Animal dog ad says\t" << ad->says() << endl;
cout << "func(a) :\t\t" << func(a) << endl;
cout << "func(d) :\t\t" << func(d) << endl;
cout << "func(ad):\t\t" << func(ad)<< endl;
}
Два разных результата:
Без виртуального #define он связывается во время компиляции. Animal *ad и func(Animal *) - все указывает на метод говорит () Animal.
$ g++ virtual.cpp -o virtual
$ ./virtual
Animal a says ??
Dog d says woof
Animal dog ad says ??
func(a) : ??
func(d) : ??
func(ad): ??
С виртуальным #define он связывается во время выполнения. Dog *d, Animal *ad и func(Animal *) указывают / указывают на метод say () собаки, поскольку Dog является их типом объекта. Если метод [Dog's say () "woof"] не определен, он будет первым, который ищется в дереве классов, то есть производные классы могут переопределять методы своих базовых классов [Animal's say ()].
$ g++ virtual.cpp -D VIRTUAL -o virtual
$ ./virtual
Animal a says ??
Dog d says woof
Animal dog ad says woof
func(a) : ??
func(d) : woof
func(ad): woof
Интересно отметить, что все атрибуты класса (данные и методы) в Python являются фактически виртуальными. Поскольку все объекты создаются динамически во время выполнения, нет объявления типа или необходимости в ключевом слове virtual. Ниже приведена версия кода Python:
class Animal:
def says(self):
return "??"
class Dog(Animal):
def says(self):
return "woof"
def func(a):
return a.says()
if __name__ == "__main__":
a = Animal()
d = Dog()
ad = d # dynamic typing by assignment
print("Animal a says\t\t{}".format(a.says()))
print("Dog d says\t\t{}".format(d.says()))
print("Animal dog ad says\t{}".format(ad.says()))
print("func(a) :\t\t{}".format(func(a)))
print("func(d) :\t\t{}".format(func(d)))
print("func(ad):\t\t{}".format(func(ad)))
Выход:
Animal a says ??
Dog d says woof
Animal dog ad says woof
func(a) : ??
func(d) : woof
func(ad): woof
который идентичен виртуальному определению C++. Обратите внимание, что d и ad - две разные переменные-указатели, ссылающиеся на один и тот же экземпляр Dog. Выражение (ad is d) возвращает True, и их значения совпадают с < main.Dog object at 0xb79f72cc>.
Суть в том, что виртуальные функции облегчают жизнь. Давайте использовать некоторые идеи М. Перри и опишем, что произойдет, если у нас не будет виртуальных функций, а вместо этого мы сможем использовать только указатели на функции-члены. В обычной оценке без виртуальных функций имеем:
class base {
public:
void helloWorld() { std::cout << "Hello World!"; }
};
class derived: public base {
public:
void helloWorld() { std::cout << "Greetings World!"; }
};
int main () {
base hwOne;
derived hwTwo = new derived();
base->helloWorld(); //prints "Hello World!"
derived->helloWorld(); //prints "Hello World!"
Итак, это то, что мы знаем. Теперь давайте попробуем сделать это с помощью указателей на функции-члены:
#include <iostream>
using namespace std;
class base {
public:
void helloWorld() { std::cout << "Hello World!"; }
};
class derived : public base {
public:
void displayHWDerived(void(derived::*hwbase)()) { (this->*hwbase)(); }
void(derived::*hwBase)();
void helloWorld() { std::cout << "Greetings World!"; }
};
int main()
{
base* b = new base(); //Create base object
b->helloWorld(); // Hello World!
void(derived::*hwBase)() = &derived::helloWorld; //create derived member
function pointer to base function
derived* d = new derived(); //Create derived object.
d->displayHWDerived(hwBase); //Greetings World!
char ch;
cin >> ch;
}
Хотя мы можем делать некоторые вещи с помощью указателей на функции-члены, они не так гибки, как виртуальные функции. Сложно использовать указатель на функцию-член в классе; указатель на функцию-член почти, по крайней мере в моей практике, всегда должен вызываться в основной функции или из функции-члена, как в приведенном выше примере.
С другой стороны, виртуальные функции, хотя они могут иметь некоторые накладные расходы на указатель на функцию, значительно упрощают ситуацию.
РЕДАКТИРОВАТЬ: есть еще один метод, который похож на eddietree: виртуальная функция C++ против указателя функции-члена (сравнение производительности).
Нам нужны виртуальные методы для поддержки "Полиморфизма времени выполнения". Когда вы ссылаетесь на объект производного класса, используя указатель или ссылку на базовый класс, вы можете вызвать виртуальную функцию для этого объекта и выполнить версию функции производного класса.
Насколько я понимаю этот вопрос, он спрашивает, почему C++ требует ключевого слова virtual.
Потому что компилятор может не определить, какой метод экземпляра вызывать на этапе компиляции.
Следующий код представляет собой пример:
#include <iostream>
using namespace std;
class Animal {
public:
virtual void Say() { cout << "Im animal"; }
};
class Cat : public Animal {
public:
void Say() { cout << "Im cat"; }
};
class Dog : public Animal {
public:
void Say() { cout << "Im dog"; }
};
Animal* NewAnimal() {
int v = 1;
// The input is totally unpredictable.
cin >> v;
switch (v) {
case 1:
return new Cat();
case 2:
return new Dog();
default:
return new Animal();
}
}
int main(void) {
auto x = NewAnimal();
// Compiler can't determine what x is (a dog or a cat, or some animal else)
// in compiling stage. So, to call which Say function, is runtime related.
// That's to say, the Say function requires dynamically binding.
// What the keyword virtual does, is to tell the compiler the Say function
// should be determined at runtime stage, but not compiling stage.
x->Say();
return 0;
}
Какого животного позвать, совершенно непредсказуемо. Мы должны динамически решать, какой из них, во время выполнения.
Следуя ответу @user6359267, иерархия области видимости С++
global -> namespace -> class -> local -> statement
Следовательно, каждый класс определяет область действия. Если бы это было не так, переопределенные функции в подклассе фактически переопределяли бы функцию в той же области видимости, что не позволяет компоновщик:
- Функция должна быть объявлена перед использованием в каждой единице перевода, и
- Функция может быть определена только один раз в заданной области действия во всей программе (во всех единицах перевода).
Поскольку каждый класс определяет свою собственную область видимости, вызывается функция, определенная в классе объекта, вызывающего эту функцию. Так,
#include <iostream>
#include <string>
class Parent
{
public:
std::string GetName() { return "Parent"; }
};
class Child : public Parent
{
public:
std:::string GetName() { return "Child"; }
};
int main()
{
Parent* parent = new Parent();
std::cout << parent->GetName() << std::endl;
Child* child = new Child();
std::cout << child->GetName() << std::endl;
*parent = child;
std::cout << child->GetName() << std::endl;
return 0;
}
выходы
Parent
Child
Parent
Следовательно, нам нужен способ сообщить компилятору, что вызываемая функция должна быть определена во время выполнения, а не во время компиляции. Это то, что делает виртуальное ключевое слово.
Вот почему перегрузка функций называется полиморфизмом времени компиляции (или ранним связыванием), а переопределение виртуальных функций называется полиморфизмом времени выполнения (или поздним связыванием).
Подробности:
Внутренне, когда компилятор видит виртуальную функцию, он создает указатель члена класса, который в общем указывает на член класса (а не на конкретный экземпляр этого члена в объекте) с помощью