一、虚函数表作用
指向派生类对象的基类指针(或基类引用)能够调用正确的虚函数,这种动态绑定机制使得对象具有了“神奇的”选择能力和自我决定能力,这是通过虚函数表(virtualfunctiontable,vftable)实现的。
二、虚函数工作机制
(1)编译器会为含有虚函数的类创建一个虚函数表,并在该表中记录各虚函数的地址。
(2)在生成该类的对象时,通常会在该对象的首地址放置一个虚函数指针(vfptr),并把该指针初始化为虚函数表的地址。
(3)在通过基类指针(或基类引用)调用虚函数时,编译器会首先获得(基类或派生类)对象的虚函数指针,据此找到派生类的虚函数表,然后在虚函数表中查询虚函数的地址,从而完成动态绑定,并调用正确的虚函数。这就是虚函数大致的工作机制。
(4)当然,设置每个类的虚函数表,初始化每个对象的虚函数指针,为虚函数调用插入代码,所有这些都由系统自动完成。
三、具体解释
(1)请看下面的Base类,它声明了N个虚函数:
class Base
{
public:
virtual void Funcl()
{
//Funcl imDlementation
}
virtual void Func2()
{
//Func2 imDlementation
}
......//继续下去
virtual void FuncN()
{
//FuncN implementation
}
下面的Derived类继承了Base类,并覆盖了除Base::Func2()外的其他所有虚函数:
class Derived:public Base
{
public:
virtual void Funcl()
{
//Func2 overrides Base::Func3()
}
//注意Func2没有在这里实现
virtual void Func3()
{
//Func3 overrides Base::Func3()
}
.......//继续下去
virtual void FuncN()
{
//FuncN imDlementation
}
编译器见到这种继承层次结构后,知道Base定义了一些虚函数,并在Derived中覆盖了它们。在这种情况下,编译器将为实现了虚函数的基类和覆盖了虚函数的派生类分别创建一个虚函数表。换句话说,Base和Derived类都将有自己的虚函数表。实例化这些类的对象时,将创建一个隐藏的指针(我们称之为虚函数指针vfptr),它指向相应的虚函数表VFT。可将VFT视为一个包含函数指针的静态数组,其中每个指针都指向相应的虚函数,其存储按其虚函数声明顺序,如下图所示:
每个虚函数表都由函数指针组成,其中每个指针都指向相应虚函数的实现。在类Derived的虚函数表中,除一个函数指针外,其他所有函数指针都指向Derived本地的虚函数实现。Derived没有覆盖Base::Func2(),因此相应的函数指针指向Base类的Func2()实现。
这意味着遇到下述代码时,编译器将查找Derived类的VFT,确保调用Base::Func2()的实现:
Derived obj;
obj.Func2();
调用被覆盖的方法时,也将如此:
void DoSomething(Base& objBase)
{
objBase.Func1();//invoke Derived::Func1
}
int main()
{
Derived obj;
DoSomething(obj);
}
在这种情况下,虽然将objDerived传递给了objBase,进而被解读为一个Base实例,但该实例的vfptr指针仍指向Derived类的虚函数表,因此通过该VTF执行的是Derived::Funcl();
同理,通过基类指针实现多态也是这个原理:
void DoSomething(Base* objBase)
{
objBase->Func1();//invoke Derived::Func1
}
int main()
{
Derived obj;
DoSomething(&obj);
}
将obj的指针传给objBase,那么objBase将指向Derived的实例对象,该对象的首地址将放置一个vfptr,指向VFT, 然后在虚函数表中查询虚函数的地址,从而完成动态绑定,并调用正确的虚函数。
虚函数表就是这样帮助实现c++多态的。
(2)验证
先看不含虚函数的基类和派生类对象占多少字节,考虑以下代码:
#include <iostream>
using namespace std;
class Base{
public:
Base(int x,int y):a(x),b(y){
}
~Base(){
}
void display(){
cout<<"Base display"<<endl;
}
void fun(){
cout<<"Base fun"<<endl;
}
private:
int a;
int b;
};
class D1:public Base{
public:
D1(int x,int y,int z):Base(x,y),c(z){
}
~D1(){
}
void display(){
cout<<"D1 display"<<endl;
}
void fun(){
cout<<"D1 fun"<<endl;
}
private:
int c;
};
class D2:public D1{
public:
D2(int x,int y,int z,int w):D1(x,y,z),d(w){
}
~D2(){
}
void display(){
cout<<"D2 display"<<endl;
}
void fun(){
cout<<"D2 fun"<<endl;
}
private:
int d;
};
int main(){
Base b(1,2);
cout<<"the size of b: "<<sizeof(b)<<endl;//12
D1 d(1,2,3);
cout<<"the size of d: "<<sizeof(d)<<endl;//16
D2 d2(1,2,3,4);
cout<<"the size of d2: "<<sizeof(d2)<<endl;//20
return 0;
}
结果:
the size of b: 8
the size of d: 12
the size of d2: 16
请按任意键继续. . .
再看含虚函数的基类和派生类对象占多少字节,考虑以下代码
#include <iostream>
using namespace std;
class Base{
public:
Base(int x,int y):a(x),b(y){
}
virtual ~Base(){
}
virtual void display(){
cout<<"Base display"<<endl;
}
virtual void fun(){
cout<<"Base fun"<<endl;
}
private:
int a;
int b;
};
class D1:public Base{
public:
D1(int x,int y,int z):Base(x,y),c(z){
}
~D1(){
}
virtual void display(){
cout<<"D1 display"<<endl;
}
virtual void fun(){
cout<<"D1 fun"<<endl;
}
private:
int c;
};
class D2:public D1{
public:
D2(int x,int y,int z,int w):D1(x,y,z),d(w){
}
~D2(){
}
virtual void display(){
cout<<"D2 display"<<endl;
}
virtual void fun(){
cout<<"D2 fun"<<endl;
}
private:
int d;
};
int main(){
Base b(1,2);
cout<<"the size of b: "<<sizeof(b)<<endl;//12
D1 d(1,2,3);
cout<<"the size of d: "<<sizeof(d)<<endl;//16
D2 d2(1,2,3,4);
cout<<"the size of d2: "<<sizeof(d2)<<endl;//20
return 0;
}
结果:
the size of b: 12
the size of d: 16
the size of d2: 20
请按任意键继续. . .
上面两端代码唯一不同是,第一段不含有虚函数,第二段代码每个基类和派生类均含有两个虚函数;结果的区别是含虚函数的对象比不含虚函数的对象多4个字节,就Base对象来看,含有两个int数据成员,那么应该占8个字节,而含有虚函数的却占12个字节,说明在对象的首地址确实含有一个vfptr,且占4个字节,该指针指向虚函数表;
注意:只要有虚函数(包括虚析构表),那么对象首地址都含有vfptr指向虚函数表
四、多继承情况的虚函数表
上面的解释针对于单继承,多继承的虚函数表可研读另一篇文章:
http://blog.youkuaiyun.com/haoel/article/details/1948051/
此文章详细介绍了多继承下是否有虚函数,成员覆盖下的虚函数表
参考数目:
1.张俊主编,C++面向对象程序设计 第2版,中国铁道出版社,2012.08.
2. (美)罗奥著;袁国忠译,21天学能C++ 第7版,人民邮电出版社,2012.12.