虚函数算是C++最关键和核心的内容之一,是组件的基础。下面先列出一些相关名词,再围绕它们举例说明虚函数的本质实现原理。
1)绑定:Connectinga function call to a function body is called binding.(把函数调用和函数实现关联的过程)
2)早绑定:Whenbinding is performed before the program is run (by the compiler and linker),it‘ s calledearly binding(程序运行前,即编译和链接阶段,完成的绑定即为早绑定)。
3)迟绑定:latebinding, which means the binding occurs at runtime, based on the type of theobject.When a language implements late binding, there must be some mechanism todetermine the type of the object at runtime and call the appropriate memberfunction.(迟绑定发生在运行时,不同类型的对象绑定不同函数。实现迟绑定,必须有某种机制确定对象的具体类型然后调用合适的成员函数)。
4)虚函数表(VTable):一个存储于常量区的函数指针表,类似函数指针数组。每个含有虚函数的类(基类及派生类)各自包含一张虚函数表(一个类一张表),表中依次存放虚函数地址。派生类vtable继承它各个基类的vtable,这里继承是指:基类vtable中包含某item,派生类vtable中也将包含同样item,但值可能不同。如派生类(override)重新实现了某虚函数,则它的vtable中该项item指向新写的虚函数,若未重新实现则沿用基类vtable中对应项的值。
5) 指向虚函数表的指针(vtptr):所有包含虚函数的类所实例化的对象里,都包含该指针,运行时对象借助于它寻址到虚函数表,从而完成后绑定。因此每个包含虚函数的对象,相比普通对象会额外多占用一个指针型的存储空间。
魔术与揭秘
class Base //基类,包含virtual函数
{
public:
virtual void output(){ cout << "Base::output()" << endl;}
};
//派生两个类Drv0和Drv1
class Drv0 : public Base
{
public:
void output () { cout << "Drv0:: output ()" << endl;}
};
class Drv1 : public Base
{
public:
void output () { cout << "Drv1:: output ()" << endl;}
};
void main()
{
Base b;
Base* pb = &b;
pb-> output (); //输出Base::output()
Drv0 d1;
pb =reinterpret_cast<Base*>(&d1);
pb-> output (); //输出Drv0:: output()
Drv0 d2;
pb =reinterpret_cast<Base*>(&d2);
pb-> output (); //输出Drv0:: output()
Drv1 d3;
pb = reinterpret_cast<Base*>(&d3);
pb-> output ();
//输出Drv1:: output()
}
经过一些中间封装变换,最终同样的”pb-> output ()”运行时选择了不同函数,得到不同结果。奇妙的魔术?别急,下面用结构体实现类似功能:
typedef void (*pvfun)( );
const pvfun pf_Base[1]= {Base_output};
const pvfun pf_Drv0[1]= {Drv0_output};
const pvfun pf_Drv1[1]= {Drv1_output};
typedef struct BASE
{
void *vtptr;
int mBase;
}Base;
void Base_output() { printf("Base::output()"); }
typedef struct DRV0
{
void *vtptr;
int mBase;
int mDrv0
}Drv0;
void Drv0_output() { printf("Drv0::output()"); }
typedef struct DRV1
{
void *vtptr;
int mBase;
int mDrv1
}Drv1;
void Drv1_output() { printf("Drv1::output()"); }
void main()
{
Base b;
b.vtptr =pf_Base ①
Base* pb =&b; //
*((pvfun)(pb->vtptr+0))(); //调用Base_output()
Drv0 d1;
d1.vtptr = pf_Drv0 ②
pb = (Base*)(&d1);
*((pvfun)(pb->vtptr+0))(); //调用Drv0_output ()
Drv0 d2;
d2.vtptr = pf_Drv0
pb = (Base*)(&d2);
*((pvfun)(pb->vtptr+0))() //调用Drv0_output ()
Drv1 d3;
d3.vtptr = pf_Drv1 ③
pb = (Base*)(&d3);
*((pvfun)(pb->vtptr+0))() //调用Drv1_output ()
}
上例同样实现了用相同形式调用不同函数,但这次能清楚看出猫腻所在: ①②③处分别为结构体成员vtptr赋了不同值。魔术揭穿了,还记得么:”所有软件问题都可以通过增加一个中间层解决”。表面的神奇是依靠VTable和vtptr组成的中间层在背后耍把戏。
例中pf_Base/pf_Drv0/pf_Drv1就是虚函数表VTable;各结构体的成员vtptr就是指向VTable的指针;①②③处是把vtptr与各自struct对应的VTable关联。只不过这些在C++中都隐藏不可见,由编译器自动生成和处理:
1)虚函数表与类关联:编译器在编译时自动为每个包含虚函数的类及其派生类各自单独生成一张虚函数表,用于存放虚函数指针。注意:基类与派生类各有各的虚表,独立存放于不同地址,唯的一关联是:派生类如果没重新实现某基类虚函数,其VTable对应条目中默认存放基类虚函数地址以做后备。
2)对象与虚函数表关联:对包含虚函数的类,C++编译器为其每个对象插入一个指针成员vtptr,指向该类的虚函数表,即同类对象的vtptr值相同。vtptr值在构造函数中初始化(编译器自动加入),即使该类没定义构造函数,默认构造函数也会初始化vtptr。
3)上面两步说明对象实例化一完毕,就已经和具体虚函数实现挂钩,调用时看似智能的选择不过是顺藤摸瓜:
Drv0 d1; //这一步背后d1->vtptr= VTable(Drv0),其中VTable[0]=(*Drv0::output)()
pb =reinterpret_cast<Base*>(&d1); //编译器支持指针强制向上类型转换,把派生类对象的地址赋给基类指针,pb值仍是&d1
pb-> output(); //d1->vtptr[0](),即调用Drv0:: output ()
总结虚函数实现原理:
编译期建立vtable表,设定表中元素;
执行期间在对象创建时的构造函数中关联vtptr和vtable表;
借助于指针支持的以小引大,通过强制转换将派生类对象的地址赋给基类指针;
通过基类指针调用虚函数,先取得对象中的vtptr(obj->vtptr),再找到其所指的对应于特定父类或子类的虚函数表(VTable=*(vtptr)),然后表头加偏移量寻址到相应函数指针(vfunptr = VTable[offset]),最后执行*vfunptr()。
这就是C++通过虚函数实现多态的背后原理,多态使我们可统一用指向基类对象的指针调用所有基类/派生类的虚函数实现,到底会调哪个,关键看对象的vtptr指针指向了哪个类的VTable,而这点在对象实例化时会通过构造函数隐含设置好。
以一个问题结尾,可否在类的构造函数中调用虚函数,为什么?
原文地址:http://blog.csdn.net/ipmux/article/details/44856549