什么是虚表
虚表全称为虚拟函数表。在C++语言中,每个有虚函数的类或者虚继承的子类,编译器都会为它生成一个虚拟函数表,表中的每一个元素都指向一个虚函数的地址。(注意:虚表是从属于类的)
此外,编译器会为包含虚函数的类加上一个成员变量,是一个指向该虚函数表的指针(常被称为vptr),每一个由此类别派生出来的类,都有这么一个vptr。虚表指针是从属于对象的。也就是说,如果一个类含有虚表,则该类的所有对象都会含有一个虚表指针,并且该虚表指针指向同一个虚表。
为什么要有虚表
c++中的虚表是用来实现c++的动态多态性的,指的是当基类指针指向其派生类实例时,可以用基类指针调用派生类中的成员函数。如果基类指针指向不同的派生类,则它调用同一个函数就可以实现不同的逻辑,这种机制可以让基类指针有“多种形态”,它的实现依赖于虚表。
内存布局
在程序的内存布局中,虚表的地址总是存在于对象实例中的最前面(这是为了保证取虚函数表有最高的性能)。下图中的vptr就是虚函数表的指针,指向一个函数地址。
单继承
派生类未覆盖基类虚函数
在这个例子中,子类没有重写父类的任何方法,而是加入了一个新的虚函数。从图中可以看出,子类虚表中先存放基类的虚函数地址,再存放子类的虚函数地址。其中dfunc1()非虚函数,故不放在虚函数表中。
派生类覆盖基类虚函数
在这个例子中,子类重写了父类的vfunc2()函数。从图中可以看出,虚表中派生类覆盖的虚函数的地址被放在了基类相应函数原始的位置, 派生类未覆盖的基类虚函数的地址也继续沿用基类的地址。
多继承
派生类未覆盖基类虚函数
多重继承稍微复杂一点,本例中Deirved类分别继承了Base,Base2,Base3,但是没有覆盖父类中的虚函数。如下图可以看出,有几个基类就有几个虚函数表,子类的虚成员函数被放到了第一个父类的表中(第一个父类是按照声明的顺序来判断的)。
派生类覆盖基类虚函数
本例中,子类覆写了三个基类中各一个函数,从图中可以看到,仍然是有三个虚表,其中每个虚表有一个函数的地址已经换成了子类函数的地址。子类自己的函数仍然放在第一个父类的表中。
一个问题
如果绑定了子类对象的基类指针调用子类覆写的函数,会调用基类的还是子类的?
答:会调用子类的。
因为虽然是基类的指针,但是指向的是子类的地址,找到的也是子类的虚函数表,自然会调用子类的虚函数。
C++中虚表存在的问题
- 通过父类指针访问子类特有的虚函数
这种情况下虽然父类指向子类的虚表,但是编译器会对这种行为报错,编译无法通过。但是在运行时通过指针强行找到位置进行读写的情况编译器无法判断。
- 通过子类指针访问父类的private虚函数
如果父类的虚函数时private或protected的,但这些非public的虚函数也同样会存在于虚表中。所以同样可以通过使用指针访问虚表强行访问这些函数。
附录
下面这个程序演示了如何使用指针强行访问类中的private虚函数。证明了虚表确实存在于对象实例的最前面,并且虚表的排列方式正如我们所述。基于此原理,可以自行做子类访问父类/父类访问子类的private虚函数的实验。
class Base
{
public:
Base(int mem1 = 1, int mem2 = 2) : m_iMem1(mem1), m_iMem2(mem2) { ; }
private:
virtual void vfunc1() { std::cout << "In vfunc1()" << std::endl; }
virtual void vfunc2() { std::cout << "In vfunc2()" << std::endl; }
virtual void vfunc3() { std::cout << "In vfunc3()" << std::endl; }
private:
int m_iMem1;
int m_iMem2;
};
int main()
{
Base b;
// 对象b的地址
int *bAddress = (int *)&b;
// 对象b的vtptr的值
int *vtptr = (int *)*(bAddress + 0);
printf("vtptr: 0x%08x\n", vtptr);
// 对象b的第一个虚函数的地址
// 这里每个指针+2是因为测试时64位机,指针大小为64位,而int只有32位,所以是两倍的int
// 32位机自行改为+1即可
int *pFunc1 = (int *)*(vtptr + 0);
int *pFunc2 = (int *)*(vtptr + 2);
int *pFunc3 = (int *)*(vtptr + 4);
printf("\t vfunc1addr: 0x%08x \n"
"\t vfunc2addr: 0x%08x \n"
"\t vfunc3addr: 0x%08x \n",
pFunc1,
pFunc2,
pFunc3);
// 对象b的两个成员变量的值(用这种方式可轻松突破private不能访问的限制)
int mem1 = (int)*(bAddress + 1);
int mem2 = (int)*(bAddress + 2);
printf("m_iMem1: %d \nm_iMem2: %d \n\n", mem1, mem2);
typedef void (*FUNC)(void);
// 调用虚函数
(FUNC(pFunc1))();
(FUNC(pFunc2))();
(FUNC(pFunc3))();
return 0;
}
# 输出
vtptr: 0x00400cb0
vfunc1addr: 0x00400af4
vfunc2addr: 0x00400b20
vfunc3addr: 0x00400b4c
m_iMem1: 0
m_iMem2: 1
In vfunc1()
In vfunc2()
In vfunc3()