C++规定了虚函数的行为,但将实现方法留给了编译器作者。通常,编译器处理虚函数的方法是:给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组的指针。这种数组称为虚函数表(virtual function table, vtbl)。虚函数表中存储了为类对象进行声明的虚函数的地址。
例如,基类对象包含这样一个指针,该指针指向基类中所有虚函数的地址表。派生类对象将包含一个指向独立地址表的指针。如果派生类提供了虚函数的新定义,该虚函数表将保存新函数的地址;如果派生类没有重新定义虚函数,该vtbl将保存函数原始版本的地址。如果派生类定义了新的虚函数,则该函数的地址也将被添加到vtbl中。注意,无论类中包含的虚函数是1个还是10个,都只需要在对象中添加1个地址成员,只是表的大小不同而已。
-------------摘自《C++ Primer Plus》
出于方便定位虚函数表的考虑,编译器一般将虚函数表的地址存放在对象内存的开头。
参考上图,我们可以做以下实验,
#include <iostream>
using namespace std;
typedef void (*PFUNC)(void);
class Base
{
private:
int price;
public:
virtual void prnmsg()
{
cout << "Where are you?" << endl;
}
};
class Derive : public Base
{
public:
void prnmsg()
{
cout << "I'm here!" << endl;
}
virtual void prnmsg01()
{
cout << "Look out!" << endl;
}
};
int main()
{
Derive der;
der.prnmsg();
long vtl = *(long*)&der;
PFUNC fnc = (PFUNC)*((long*)vtl);
fnc();
fnc = (PFUNC)*((long*)vtl+1);
fnc();
return 0;
}
如上代码,为了将虚函数地址赋给函数指针,并使用函数指针执行该虚函数,
首先取对象的地址,&der
,由于虚函数表地址是存放在对象内存开头且是一个地址,在64位机中,地址占有8个字节的长度即long类型大小。因此(long*)&der
,将对象的首地址强转成(long*),再使用*运算符 *(long*)&der
,得到对象首地址存放的虚拟表地址数值,使用vtl保存该数值
long vtl = *(long*)&der;
vtl保存的8字节数值实际上是一个有效地址,指向虚函数表数组的首地址,因此再次使用*运算符便可以得到对象第一个虚函数的入口地址,
PFUNC fnc = (PFUNC)*((long*)vtl);
同理,对象第二个虚函数的入口地址为
fnc = (PFUNC)*((long*)vtl+1);