以下是我如何理解不仅仅是 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*
吗?如果你必须从Animal派生更多的动物,它们都需要自己的func()
。
解决方案是将来自Animal
类的eat()
函数变为虚函数:
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."
完成。
__Animal *animal = new Animal;__
__//Cat *cat = new Cat;__
__Animal *cat = new Cat;__
__animal->eat();__ // 输出: "我正在吃着通用的食物."
__cat->eat();__ // 输出: "我正在吃着通用的食物."
即使你在给子类对象(Cat)赋值,调用的方法是基于指针类型(Animal)而不是它所指向的对象类型。这就是为什么需要“虚拟”的原因。 - rexbeliavirtual
关键字引入了动态绑定的概念,与静态绑定相对应。如果你之前主要使用的是像 Java 这样的语言,这可能会感觉有些奇怪。 - Peter Chaulavirtual
关键字时,会出现"早期绑定"。方法的具体实现在编译时根据调用指针的类型来决定。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 () override { std::cout << "Derived::Method2" << std::endl; }
// Note - override is optional; adding it to Method1 would result in an error
};
Base* basePtr = new Derived ();
// Note - constructed as Derived, but pointer stored as Base*
basePtr->Method1 (); // Prints "Base::Method1"
basePtr->Method2 (); // Prints "Derived::Method2"
main
函数中等等。指向派生类的指针隐式地转换为指向基类的指针(更具体的隐式转换为更一般的)。相反,您需要显式转换,通常使用 dynamic_cast
。其他任何操作都很容易产生未定义行为,因此请确保您知道自己在做什么。据我所知,这种情况甚至在 C++98 之前也没有改变。 - user180247你需要至少一层继承和一个向上转型来演示它。这里是一个非常简单的例子:
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
std::cout << d->Says(); // always Woof
std::cout << a->Says(); // Woof or ?, depends on virtual
}
虚函数用于支持运行时多态性。
也就是说,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;
}
在被注释为“bad”的代码行中,调用了Expression::value
方法,因为在编译时已知的类型是Expression
,value
方法不是虚函数。
Expression
中声明value
为virtual
可以确保每次调用都会检查对象的实际类型,并调用适合该动态类型的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
,就必须实现一些自己的版本的动态绑定。这通常涉及不安全的手动向下转换、复杂性和冗长性。
对于仅有一个函数的情况,如此处,将函数指针存储在对象中并通过该函数指针调用即可满足要求,但即便如此,仍然涉及一些不安全的向下转换、复杂性和冗长性,即:
#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
版本的foo
,忽略p
实际上指向Der
的事实。如果foo
是虚函数,则p->foo()
将执行“最底层”覆盖的foo
,完全考虑指向的项目的实际类。因此,虚函数和非虚函数之间的区别实际上非常重要:前者允许运行时多态,这是面向对象编程的核心概念,而后者则不允许。虽然与上述答案使用相同的概念,但是我希望添加另一种使用虚函数的方法。以下是一个示例程序:
虚析构函数
考虑下面的程序,在不将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
解释虚函数的必要性 [易于理解]
#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
,你并不是在重载函数而是在进行隐藏。如果基类 B
有一个或多个名为 foo
的函数,而派生类 D
定义了名为 foo
的函数,则所有这些在 B
中的 foo
都被隐藏了。它们可以通过作用域解析访问为 B::foo
。如果要将 B::foo
函数提升到 D
中进行重载,必须使用 using B::foo
。 - Kaz我已经把答案以对话的形式整理好了:
为什么我们需要虚函数?
因为多态。
什么是多态?
基类指针可以指向派生类对象的事实。
这个多态的定义如何导致我们需要虚函数?
通过早绑定。
什么是早绑定?
C++中的早绑定(编译时绑定)意味着在程序执行之前函数调用已经固定。
那么呢...?
如果你使用一个基类型作为函数的参数,编译器将只识别基接口。如果你使用任何来自派生类的参数来调用该函数,它会被切割掉,这不是你想发生的。
如果这不是我们想要发生的,为什么还允许发生?
因为我们需要多态!
那什么是多态的好处呢?
你可以使用一个基类型指针作为单个函数的参数,在程序运行时,你可以使用那个单一的基指针进行解引用,从而访问每个派生类型接口(例如它们的成员函数),没有任何问题。
我还是不知道虚函数有什么用...!这是我的第一个问题!
好吧,这是因为你问的太早了!
为什么我们需要虚函数?
假设您使用一个基指针调用函数,该指针具有其派生类之一的对象的地址。正如我们之前谈到的那样,在运行时,这个指针被解引用,目前为止没问题,然而,我们期望执行“来自我们的派生类”的方法(即成员函数)!但是,在基类中已经定义了相同的方法(具有相同的头文件),所以为什么您的程序还要费心选择另一个方法呢?换句话说,我是说,你怎么能把这种情况和以前通常发生的事情区分开来呢?
简短的答案是“基类中的虚成员函数”,稍微长一点的答案是,“在这一步中,如果程序在基类中看到虚函数,它会意识到你正在尝试使用多态”,因此会转向派生类(使用v-table,一种晚绑定形式)来查找具有相同头文件但具有-预期的不同实现的另一个方法。
为什么是不同的实现?
你这个笨蛋!去读一本好书吧!
好的,等等等等,为什么有人要用基指针,而不是直接使用派生类型指针?你来判断,这一切麻烦值得吗?看看下面这两个代码片段:
//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();
而且实际上,这可能仍然是一个人为的例子!