我正在学习C++,我刚刚开始学习虚拟函数。
从我(在书中和网上)读到的内容来看,虚拟函数是基类中的函数,可以在派生类中重写。
但在本书的早些时候,当我学习基本继承时,我能够在派生类中重写基函数,而不使用虚函数。
那么我在这里错过了什么?我知道虚拟函数还有很多,它似乎很重要,所以我想清楚它到底是什么。我只是在网上找不到一个直截了当的答案。
我正在学习C++,我刚刚开始学习虚拟函数。
从我(在书中和网上)读到的内容来看,虚拟函数是基类中的函数,可以在派生类中重写。
但在本书的早些时候,当我学习基本继承时,我能够在派生类中重写基函数,而不使用虚函数。
那么我在这里错过了什么?我知道虚拟函数还有很多,它似乎很重要,所以我想清楚它到底是什么。我只是在网上找不到一个直截了当的答案。
当前回答
你熟悉函数指针吗?虚拟函数也是一个类似的想法,只是您可以轻松地将数据绑定到虚拟函数(作为类成员)。将数据绑定到函数指针并不容易。对我来说,这是主要的概念区别。这里的许多其他答案只是说“因为…多态性!”
其他回答
下面是前两个答案的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's says()“woof”]方法,否则它将是在类树中首先搜索的方法,即派生类可能会覆盖其基类的方法[Eanimal's 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 is d)返回True,其值与0xb79f72cc>处的<main.Dog对象相同。
您必须区分重写和重载。如果没有virtual关键字,则只能重载基类的方法。这只意味着隐藏。假设您有一个基类base和一个派生类Specialized,它们都实现了void foo()。现在有一个指向Base的指针指向Specialized的实例。当您对其调用foo()时,您可以观察到virtual的不同之处:如果该方法是虚拟的,则将使用Specialized的实现,如果缺少,则将选择Base的版本。最好不要重载基类中的方法。使方法非虚拟化是作者告诉你它在子类中的扩展不是有意的。
解释了虚拟功能的需求[易于理解]
#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告诉编译器它不应该执行早期绑定。相反,它应该自动安装执行后期绑定所需的所有机制。为了实现这一点,典型的编译器1为每个包含虚拟函数的类创建一个表(称为VTABLE)。编译器将该特定类的虚拟函数的地址放在VTABLE中。在每个具有虚拟函数的类中,它都会秘密地放置一个指针,称为vpointer(缩写为VPTR),该指针指向该对象的VTABLE。当您通过基类指针进行虚拟函数调用时,编译器会悄悄地插入代码以获取VPTR并在VTABLE中查找函数地址,从而调用正确的函数并导致延迟绑定。
此链接中的详细信息http://cplusplusinterviews.blogspot.sg/2015/04/virtual-mechanism.html
我以对话的形式给出了答案,以便更好地阅读:
为什么我们需要虚拟功能?
因为多态性。
什么是多态性?
基指针也可以指向派生类型对象。
多态性的定义是如何导致对虚拟函数的需求的?
嗯,通过早期绑定。
什么是早期绑定?
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();
事实上,这可能也是一个虚构的例子!