前言
虚函数的实现包含两个重要部分,虚函数表和虚函数表指针。
- 虚函数表:一个类如果有虚函数,则针对这个类会产生一个虚函数表;
虚函数表属于类。 - 虚函数表指针:生成这个类的对象时,对象里就包含了一个指针(虚函数指针),用来指向这个虚函数表的起始地址;
虚函数表指针属于对象,一般位于对象内存的起始位置。
虚函数表分析
看如下示例代码(代码1),父类Base包含三个虚函数f、g、h,子类Derive重写了其中两个虚函数f、g:
class Base
{
public:
Base() {
}
virtual void f() {
cout << "Base::f()" << endl; }
virtual void g(int n) {
cout << "Base::g(" << n << ")" << endl; }
virtual void h() {
cout << "Base::h()" << endl; }
};
class Derive : public Base
{
public:
Derive() {
}
void f() {
cout << "Derive::f()" << endl; }
void g(int n) {
cout << "Derive::g(" << n << ")" << endl; }
};
int main()
{
Base* pb = new Base;
Base* pd = new Derive;
pd->f();
pd->g(5);
pd->h();
return 0;
}
其运行结果如下所示:
在调试时将断点设置在return 0;处,即可获得如下调试信息:
根据以上代码和调试信息,绘制出父类对象(pb)和子类对象(pd)内存布局如下所示:
- 虚函数的地址在编译器就确定了;
- 虚函数表中存放的是此类对应的虚函数的地址,若子类未重写父类虚函数,则虚函数的地址为父类对应函数的地址,如Derive::h指向Base::h。虚函数表在编译期就行确定。
- 类的虚函数表指针指向虚函数表的首地址。对象的虚函数表指针的值在运行期就行确定,即虚函数表指针的值在运行期会发生变化,故虚函数的调用在运行期才能确定。
虚函数模拟实现
针对以上代码,模拟实现虚函数,实现代码(代码2)如下:
class Base
{
public:
Base() {
__vfptr = vftable;/*模拟编译时插入的代码*/
//... 用户代码
}
void f() {
cout << "Base::f()" << endl; }//virtual void f()
void g(int n) {
cout << "Base::g(" << n << ")" << endl; }//virtual void g()
void h() {
cout << "Base::h()" << endl; }//virtual void h()
void** __vfptr;//虚表指针,跟随对象,模拟编译时插入的代码
static void* vftable[3]; //虚函数表,跟随类
};
void* Base::vftable[3];//定义虚函数表
class Derive : public Base
{
public:
Derive() {
__vfptr = vftable;/*模拟编译时插入的代码*/
//... 用户代码
}
void f() {
cout << "Derive::f()" << endl; }
void g(int n) {
cout << "Derive::g(" << n << ")" << endl; }
static void* vftable[3];
};
void* Derive::vftable[3];//定义虚函数表
int main()
{
/* 模拟编译期确定虚函数表中每一项的值,编译器实现在main函数执行之前
Base::vftable = { &Base::f, &Base::g, &Base::h } */
void(Base:: * b_pf)() = &Base::f;
memcpy(Base::vftable + 0, &b_pf, sizeof(void*));
void(Base:: * b_pg)(int) = &Base::g;
memcpy(Base::vftable + 1, &b_pg, sizeof(void*));
void(Base:: * b_ph)() = &Base::h;
memcpy(Base::vftable + 2, &b_ph, sizeof(void*));
/* Derive::vftable = { &Derive::f, &Derive::g, &Base::h } */
void(Derive:: * d_pf)() = &Derive::f;
memcpy(Derive::vftable + 0, &d_pf, sizeof(void*));
void(Derive:: * d_pg)(int) = &Derive::g;
memcpy(Derive::vftable + 1, &d_pg, sizeof(void*));
void(Derive:: * d_ph)() = &Base::h;
memcpy(Derive::vftable + 2, &d_ph, sizeof(void*));
//运行期
Base* pb = new Base;
Base* pd = new Derive;
//pd->f(); (pd->__vfptr[0])()
void(Base:: * vf)();
memcpy(&vf, pd->__vfptr + 0, sizeof(char*));
(pd->*vf)();
//pd->g(5); (pd->__vfptr[1])(5);
void(Base:: * vg)(int);
memcpy(&vg, pd->__vfptr + 1, sizeof(char*));
(pd->*vg)(5);
//pd->h(); (pd->__vfptr[2])();
void(Base:: * vh)();
memcpy(&vh, pd->__vfptr + 2, sizeof(char*));
(pd->*vh)();
return 0;
}
运行上述代码,得到和代码1一样的结果。
虚函数原理分析
- 虚函数表指针__vfptr是编译时插入的代码,其赋值在构造函数执行时。由于其作为成员变量,故发生复制时,会发生值拷贝。如:
在构造Base的对象时,将对象的虚函数表地址设置为Base::vftable的首地址;当构造Derive对象时,构造的对象的虚函数表指针指向Derive::vftable。复制操作时,成员变量进行复制,此时p的虚函数表地址从Base::vftable变为Derive::vftable。Base* p = new Base;// p->__vfptr指向Base的虚函数表 p = new Derive; // p->__vfptr指向Derive的虚函数表
- 虚函数表的初始化发生在编译期,即在运行期不会发生变化。
- 虚函数的执行,先从虚函数表中找到虚函数的地址,然后调用此地址的函数。