Зачем нам нужны виртуальные функции на С++?
Я изучаю С++, и я просто вхожу в виртуальные функции.
Из того, что я прочитал (в книге и в Интернете), виртуальные функции - это функции в базовом классе, которые вы можете переопределить в производных классах.
Но ранее в книге, узнав о базовом наследовании, я смог переопределить базовые функции в производных классах без использования virtual
.
Так что мне здесь не хватает? Я знаю, что для виртуальных функций больше, и, похоже, это важно, поэтому я хочу четко понять, что именно. Я просто не могу найти прямой ответ в Интернете.
Ответы
Ответ 1
Вот как я понял не только, что такое 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."
Ох... мы передали Cat в func()
, но он не будет есть крыс. Если вы перегружаете func()
значит, требуется Cat*
? Если вам нужно вывести больше животных из Animal, им все равно понадобится их 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."
Готово.
Ответ 2
Без "виртуального" вы получаете "раннее связывание". Какая реализация метода используется, определяется во время компиляции в зависимости от типа указателя, который вы вызываете.
С "virtual" вы получаете "позднюю привязку". Какая реализация метода используется, определяется во время выполнения, основываясь на типе объекта с указателем на объект, из которого он был первоначально построен. Это не обязательно то, что вы думаете, основываясь на типе указателя, указывающего на этот объект.
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"
EDIT - см. этот вопрос.
Также - этот учебник охватывает раннее и позднее связывание в С++.
Ответ 3
Вам нужно как минимум 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
}
Ответ 4
Вам нужны виртуальные методы для безопасного downcasting, простоты и лаконичности.
Это то, что делают виртуальные методы: они сбрасываются безопасно, с явно простым и сжатым кодом, избегая небезопасных ручных отбросов в более сложном и подробном коде, который вы в противном случае имели бы.
Не виртуальный метод → статическая привязка
Следующий код намеренно "неверен". Он не объявляет метод 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;
}
Один положительный способ взглянуть на это - если вы столкнулись с небезопасным понижением, сложностью и многословием, как описано выше, то часто может реально помочь виртуальный метод или методы.
Ответ 5
Виртуальные функции используются для поддержки полиморфизма времени выполнения.
То есть, виртуальное ключевое слово сообщает компилятору не принимать решение (привязки функции) во время компиляции, а скорее откладывать его на время выполнения ".
-
Вы можете сделать функцию виртуальной, предшествуя ключевому слову virtual
в своем объявлении базового класса. Например,
class Base
{
virtual void func();
}
-
Когда базовый класс имеет виртуальную функцию-член, любой класс, который наследует базовый класс, может переопределить функцию точно таким же прототипом, т.е. Можно переопределить только функциональность, а не интерфейс функции.
class Derive : public Base
{
void func();
}
-
Указатель базового класса можно использовать для указания на объект класса Base, а также на объект класса Derived.
- Когда виртуальная функция вызывается с использованием указателя базового класса, компилятор решает во время выполнения, какую версию функции, то есть версию базового класса или переопределенную версию класса Derived, вызывается. Это называется полиморфизмом Runtime.
Ответ 6
Если базовый класс - Base
, а производный класс - Der
, вы можете иметь указатель Base *p
который фактически указывает на экземпляр Der
. Когда вы звоните p->foo();
Если foo
не является виртуальным, то выполняется его Base
версия, игнорируя тот факт, что p
фактически указывает на Der
. Если foo является виртуальным, p->foo()
выполняет "самую крайнюю" переопределение foo
, полностью учитывая фактический класс указанного элемента. Таким образом, разница между виртуальным и не виртуальным на самом деле очень важна: первый допускает полиморфизм во время выполнения, основную концепцию ОО-программирования, а второй - нет.
Ответ 7
Необходимость объяснения виртуальной функции [Легко понять]
#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.
Следовательно, с виртуальной функцией вы можете добиться полиморфизма во время выполнения.
Ответ 8
Вы должны различать переопределение и перегрузку. Без ключевого слова virtual
вы перегружаете только метод базового класса. Это означает только скрытие.
Скажем, у вас есть базовый класс Base
и производный класс Specialized
, которые оба реализуют void foo()
. Теперь у вас есть указатель на Base
, указывающий на экземпляр Specialized
. Когда вы вызываете foo()
на нем, вы можете наблюдать разницу, которую делает virtual
: Если метод является виртуальным, будет использоваться реализация Specialized
, если он отсутствует, будет выбрана версия из Base
.
Лучше всего никогда не перегружать методы из базового класса. Создание не виртуального метода - это способ его автора сказать вам, что его расширение в подклассах не предназначено.
Ответ 9
Я хотел бы добавить еще одно использование виртуальной функции, хотя она использует ту же концепцию, что и вышеприведенные ответы, но, я думаю, ее стоит упомянуть.
ВИРТУАЛЬНЫЙ ДЕСТРУКТОР
Рассмотрим эту программу ниже, не объявляя деструктор класса Base виртуальным; память для Cat не может быть очищена.
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
Ответ 10
Зачем нам нужны виртуальные методы на С++?
Быстрый ответ:
- Он предоставляет нам один из необходимых "ингредиентов" 1 для объектно-ориентированного программирования.
В Bjarne Stroustrup С++ Programming: Принципы и практика, (14.3):
Виртуальная функция предоставляет возможность определять функцию в базовом классе и иметь функцию с тем же именем и тип в производном классе, вызываемом при вызове пользователем функции базового класса. Это часто называют полиморфизмом во время выполнения, динамической диспетчеризацией или диспетчером времени выполнения, поскольку вызванная функция определяется во время выполнения в зависимости от типа используемого объекта.
- Это самая быстрая более эффективная реализация, если вам нужен вызов виртуальной функции 2.
Для обработки виртуального вызова требуется одна или несколько частей данных, связанных с производным объектом 3. Обычно делается так, чтобы добавить адрес таблицы функций. Эту таблицу обычно называют виртуальной таблицей или таблицей виртуальных функций, а ее адрес часто называют виртуальным указателем. Каждая виртуальная функция получает слот в виртуальной таблице. В зависимости от типа объекта вызывающего (производного) виртуальная функция, в свою очередь, вызывает соответствующее переопределение.
1. Использование наследования, полиморфизма во времени выполнения и инкапсуляции является наиболее распространенным определением объектно-ориентированного программирования.
2. Вы не можете использовать функциональность кода быстрее или использовать меньшую память, используя другие языковые функции, чтобы выбирать среди альтернатив во время выполнения. Bjarne Stroustrup С++ Programming: Принципы и практика. (14.3.1).
3. Что-то сказать, какая функция действительно вызывается, когда мы вызываем базовый класс, содержащий виртуальную функцию.
Ответ 11
Когда у вас есть функция в базовом классе, вы можете Redefine
или Override
в производном классе.
Переопределение метода:
В производном классе дается новая реализация метода базового класса. Не помогает Dynamic binding
.
Переопределение метода:
Redefining
a virtual method
базового класса в производном классе. Виртуальный метод облегчает динамическое связывание.
Итак, когда вы сказали:
Но ранее в книге, узнав об основном наследовании, я был возможность переопределять базовые методы в производных классах без использования 'Виртуальное'.
вы не переопределяли его, поскольку метод в базовом классе не был виртуальным, скорее вы переопределяли его
Ответ 12
У меня есть мой ответ в форме беседы, чтобы быть лучше прочитанным:
Зачем нам нужны виртуальные функции?
Из-за полиморфизма.
Что такое полиморфизм?
Тот факт, что базовый указатель также может указывать на объекты производного типа.
Как это определение полиморфизма приводит к необходимости виртуальных функций?
Ну, через раннее связывание.
Что такое раннее связывание?
Раннее связывание (связывание во время компиляции) в 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();
И на самом деле, это может быть еще и надуманным примером!
Ответ 13
Это помогает, если вы знаете основные механизмы. С++ формализует некоторые методы кодирования, используемые программистами C, заменяемые "классы" с использованием "overlays" - структуры с общими разделами заголовков будут использоваться для обработки объектов разных типов, но с некоторыми общими данными или операциями. Обычно базовая структура наложения (общая часть) имеет указатель на таблицу функций, которая указывает на другой набор подпрограмм для каждого типа объекта. С++ делает то же самое, но скрывает механизмы, то есть С++ ptr->func(...)
, где func является виртуальным, поскольку C будет (*ptr->func_table[func_num])(ptr,...)
, где изменения между производными классами являются содержимым func_table. [Не виртуальный метод ptr- > func() просто переводит на mangled_func (ptr,..).]
Это означает, что вам нужно только понять базовый класс, чтобы вызвать методы производного класса, то есть, если подпрограмма понимает класс A, вы можете передать ему указатель производного класса B, а затем виртуальные методы, называемые будут те из B, а не A, поскольку вы проходите через таблицу функций B.
Ответ 14
Ключевое слово virtual говорит компилятору, что он не должен выполнять раннее связывание. Вместо этого он должен автоматически установить все механизмы, необходимые для выполнения позднего связывания. Для этого типичный compiler1 создает единую таблицу (называемую VTABLE) для каждого класса, который содержит виртуальные функции. Компилятор помещает адреса виртуальных функций для этого конкретного класса в VTABLE. В каждом классе с виртуальными функциями он тайно помещает указатель, называемый vpointer (сокращенно VPTR), который указывает на VTABLE для этого объекта. Когда вы вызываете виртуальный вызов функции с помощью указателя базового класса, компилятор спокойно вставляет код для извлечения VPTR и ищет адрес функции в VTABLE, тем самым вызывая правильную функцию и вызывая позднюю привязку.
Подробнее в этой ссылке http://cplusplusinterviews.blogspot.sg/2015/04/virtual-mechanism.html
Ответ 15
Ключевое слово virtual заставляет компилятор выбрать реализацию метода, определенную в классе объекта, а не в классе указателя.
Shape *shape = new Triangle();
cout << shape->getName();
В приведенном выше примере Shape:: getName будет вызываться по умолчанию, если getName() не определен как виртуальный в Shape Base. Это заставляет компилятор искать реализацию getName() в классе Triangle, а не в классе Shape.
Виртуальная таблица - это механизм, в котором компилятор отслеживает различные реализации подклассов виртуального метода. Это также называется динамической диспетчеризацией, и с ней связаны некоторые накладные расходы.
Наконец, почему виртуальный даже необходим в С++, почему бы не сделать его поведением по умолчанию, как на Java?
- С++ основан на принципах "Zero Overhead" и "Платите за то, что вы используете". Поэтому он не пытается выполнять динамическую отправку для вас, если вам это не нужно.
- Чтобы обеспечить больший контроль над интерфейсом. Делая функцию не виртуальной, интерфейс/абстрактный класс может управлять поведением во всех его реализациях.
Ответ 16
Зачем нам нужны виртуальные функции?
Виртуальные функции избегают ненужной проблемы с приведением типов, и некоторые из нас могут обсуждать, почему нам нужны виртуальные функции, когда мы можем использовать указатель производного класса для вызова функции, специфичной в производном классе! Ответ - это сводит на нет всю идею наследования в большой системной разработке, где желательно иметь объект базового класса с одним указателем.
Давайте сравним ниже две простые программы, чтобы понять важность виртуальных функций:
Программа без виртуальных функций:
#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
Тщательно анализируя оба выхода, можно понять важность виртуальных функций.
Ответ 17
Об эффективности виртуальные функции немного менее эффективны, чем функции раннего связывания.
"Этот механизм виртуальных вызовов может быть почти таким же эффективным, как механизм" нормальной функции "(в пределах 25%). Его служебные данные пространства - это один указатель в каждом объекте класса с виртуальными функциями плюс один vtbl для каждого такого класса" [Тур по С++ by Bjarne Stroustrup]
Ответ 18
В дизайне интерфейса используются виртуальные методы. Например, в Windows есть интерфейс IUnknown, как показано ниже:
interface IUnknown {
virtual HRESULT QueryInterface (REFIID riid, void **ppvObject) = 0;
virtual ULONG AddRef () = 0;
virtual ULONG Release () = 0;
};
Эти методы предоставляются пользователю интерфейса для реализации. Они необходимы для создания и уничтожения определенных объектов, которые должны наследовать IUnknown. В этом случае время выполнения известно о трех методах и ожидает, что они будут реализованы при их вызове. Поэтому в некотором смысле они действуют как договор между самим объектом и тем, что использует этот объект.
Ответ 19
Вот полный пример, иллюстрирующий, почему используется виртуальный метод.
#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;
}
Ответ 20
ООП Ответ: Подтип Полиморфизм
В C++, виртуальные методы необходимы для реализации полиморфизма, более точно подтипов или подтип полиморфизм, если применять определение из википедии.
Wikipedia, Subtyping, 2019-01-09: В теории языка программирования подтип (также полиморфизм подтипов или полиморфизм включения) представляет собой форму полиморфизма типов, в которой подтип является типом данных, который по некоторому понятию связан с другим типом данных (супертипом). заменяемости, означая, что программные элементы, обычно подпрограммы или функции, написанные для работы с элементами супертипа, также могут работать с элементами подтипа.
ПРИМЕЧАНИЕ. Подтип означает базовый класс, а подтип - унаследованный класс.
Дальнейшее чтение относительно полиморфизма подтипа
Технический ответ: динамическая отправка
Если у вас есть указатель на базовый класс, то вызов метода (который объявлен как виртуальный) будет отправлен методу фактического класса создаваемого объекта. Вот как реализуется полиморфизм подтипа C++.
Дальнейшее чтение Полиморфизм в C++ и Dynamic Dispatch
Реализация Ответ: создает запись vtable
Для каждого модификатора, "виртуального" в методах, компиляторы C++ обычно создают запись в виртуальной таблице класса, в котором объявлен метод. Вот как обычный компилятор 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 примера кода
Ответ 21
я думаю, вы ссылаетесь на факт, как только метод объявлен виртуальным, вам не нужно использовать ключевое слово "virtual" в переопределениях.
class Base { virtual void foo(); };
class Derived : Base
{
void foo(); // this is overriding Base::foo
};
Если вы не используете "virtual" в Base foo, то Derived foo просто затеняет его.
Ответ 22
Ниже приведена объединенная версия кода 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 virtual он связывается во время компиляции. Animal * ad и func (Animal *) указывают на метод Animal says().
$ g++ virtual.cpp -o virtual
$ ./virtual
Animal a says ??
Dog d says woof
Animal dog ad says ??
func(a) : ??
func(d) : ??
func(ad): ??
С#define virtual он связывается во время выполнения. Dog * d, Animal * ad и func (Animal *)/ссылаются на метод Dog says(), поскольку Dog является их типом объекта. Если метод [Dog says() "woof"] не определен, он будет первым, который был первым в дереве классов, т.е. Производные классы могут переопределять методы их базовых классов [Animal says()].
$ 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 - d) возвращает True, а их значения - один и тот же < основной.Dog-объект в 0xb79f72cc>.
Ответ 23
Нам нужны виртуальные методы для поддержки "Полиморфизма времени выполнения".
Когда вы ссылаетесь на объект производного класса, используя указатель или ссылку на базовый класс, вы можете вызвать виртуальную функцию для этого объекта и выполнить версию функции производного класса.
Ответ 24
Вы знакомы с указателями функций? Виртуальные функции представляют собой аналогичную идею, за исключением того, что вы можете легко привязать данные к виртуальным функциям (в качестве членов класса). Привязать данные к указателям на функцию не так просто. Для меня это главное концептуальное отличие. Многие другие ответы здесь просто говорят "потому что... полиморфизм!"
Ответ 25
Суть в том, что виртуальные функции облегчают жизнь. Давайте рассмотрим некоторые идеи М. Перри и опишем, что произойдет, если у нас не будет виртуальных функций, а вместо этого мы сможем использовать только указатели на функции-члены. В обычной оценке без виртуальных функций имеем:
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++ виртуальная функция против указателя функции-члена (сравнение производительности).