C++中的虚函数(Virtual Function)是用来实现动态多态性的,指的是当基类指针指向其派生类实例时,可以用基类指针调用派生类中的成员函数。如果基类指针指向不同的派生类,则它调用同一个函数就可以实现不同的逻辑,这种机制可以让基类指针有“多种形态”,它的实现依赖于虚函数表。虚函数表(Virtual Table)是指在每个包含虚函数的类中都存在着一个函数地址的数组。本文将详细介绍虚函数表的实现及其内存布局。
1. 虚函数表概述
首先我们要知道虚函数表的地址总是存在于对象实例中最前面的位置,其后依次是对象实例的成员。下图中vtptr就是虚函数表的地址,可看出虚函数表中的每个成员都对应类中的一个虚函数的地址。据图所述,我们可以使用对象实例的地址来得到虚函数表的地址,进而获得具体的虚函数的地址,然后进行调用。
假如有如下定义 Base b;
那么虚函数表的地址vtptr的值就是:(int*)*(int*)&b
,第一个虚函数vfunc1的地址就是:*(int*)*(int*)&b
,vfunc2的地址是:*( (int*)*(int*)&b + 1 )
,详见本节后文所附代码。
下文为验证代码,其中Base类包含3个虚函数 vfunc1~vfunc3
和两个数据成员m_iMem1, m_iMem2
,该类与上图中的保持一致。在main中,详细描述了怎么获取虚表的地址,怎么获取成员变量,怎么通过虚表地址获取虚函数的地址
class Base { public: Base(int mem1 = 1, int mem2 = 2) : m_iMem1(mem1), m_iMem2(mem2){ ; } 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 _tmain(int argc, _TCHAR* argv[]) { Base b; // 对象b的地址 int *bAddress = (int *)&b; // 对象b的vtptr的值 int *vtptr = (int *)*(bAddress + 0); printf("vtptr: 0x%08x ", vtptr); // 对象b的第一个虚函数的地址 int *pFunc1 = (int *)*(vtptr + 0); int *pFunc2 = (int *)*(vtptr + 1); int *pFunc3 = (int *)*(vtptr + 2); printf(" vfunc1addr: 0x%08x " " vfunc2addr: 0x%08x " " vfunc3addr: 0x%08x ", pFunc1, pFunc2, pFunc3); // 对象b的两个成员变量的值(用这种方式可轻松突破private不能访问的限制) int mem1 = (int)*(bAddress + 1); int mem2 = (int)*(bAddress + 2); printf("m_iMem1: %d m_iMem2: %d ",mem1, mem2); // 调用虚函数 (FUNC(pFunc1))(); (FUNC(pFunc2))(); (FUNC(pFunc3))(); return 0; }
程序运行结果如下面两幅图所示,其中左边部分是程序运行结果,右边部分为调试窗口中显示的类中各成员的值,可以发现两者结果一致。同时在运行结果窗口中可见直接使用地址调用虚函数的方法也是正确的,这就验证了我们本节开始部分的阐述。
2. 单继承下的虚函数表
2.1 派生类未覆盖基类虚函数
下面我们来看下派生类没有覆盖基类虚函数的情况,其中Base类延用上一节的定义。从图中可看出虚函数表中依照声明顺序先放基类的虚函数地址,再放派生类的虚函数地址。
其对应的代码如下所示:
class Derived : public Base { public: Devired(int mem = 3) : m_iDMem1(mem){ ; } virtual void vdfunc1() { std::cout << "In Devired vfunc3()" << std::endl; } void dfunc1() { std::cout << "In Devired dfunc1" << std::endl; } private: int m_iDMem1; }; int _tmain(int argc, _TCHAR* argv[]) { Derived d; int *dAddress = (int*)&d; /* 1. 获取对象的内存布局信息 */ // 虚表地址 int *vtptr = (int*)*(dAddress + 0); // 数据成员的地址 int mem1 = (int)*(dAddress + 1); int mem2 = (int)*(dAddress + 2); int dmem1 = (int)*(dAddress + 3); /* 2. 输出对象的内存布局信息 */ int *pFunc1 = (int *)*(vtptr + 0); int *pFunc2 = (int *)*(vtptr + 1); int *pFunc3 = (int *)*(vtptr + 2); int *pdFunc1 = (int *)*(vtptr + 3); (FUNC(pFunc1))(); (FUNC(pFunc2))(); (FUNC(pFunc3))(); (FUNC(pdFunc1))(); printf(" vfunc1addr: 0x%08x " " vfunc2addr: 0x%08x " " vfunc3addr: 0x%08x " " vdfunc1addr: 0x%08x ", pFunc1, pFunc2, pFunc3, pdFunc1 ); printf("m_iMem1: %d, m_iMem2: %d, m_iDMem3: %d ", mem1, mem2, dmem1); return 0; }
其输出结果如下图所示,可见与本节开始介绍的结论是一致的。
2.2 派生类覆盖基类虚函数
我们再来看一下派生类覆盖了基类的虚函数的情形,可见:1. 虚表中派生类覆盖的虚函数的地址被放在了基类相应的函数原来的位置 2. 派生类没有覆盖的虚函数延用基类的
代码如下所示,注意这里只给出了类的定义,main函数的测试代码与上节一样:
class Devired : public Base { public: // 覆盖基类的虚函数 virtual void vfunc2() { std::cout << "In Devired vfunc2()" << std::endl; } public: Devired(int mem = 3) : m_iDMem1(mem){ ; } virtual void vdfunc1() { std::cout << "In Devired vdfunc1()" << std::endl; } void dfunc1() { std::cout << "In Devired dfunc1" << std::endl; } private: int m_iDMem1; };
运行结果如下所示:
3. 多继承下的虚函数表
3.1 无虚函数覆盖
如果是多重继承的话,问题就变得稍微复杂一丢丢,主要有几点:1. 有几个基类就有几个虚函数表 2. 派生类的虚函数地址存依照声明顺序放在第一个基类的虚表最后,见下图所示:
Base类延用本文之前的定义,其余部分代码如下所示:
class Base2 { public: Base2(int mem = 3) : m_iBase2Mem(mem){ ; } virtual void vBase2func1() { std::cout << "In Base2 vfunc1()" << std::endl; } virtual void vBase2func2() { std::cout << "In Base2 vfunc2()" << std::endl; } private: int m_iBase2Mem; }; class Base3 { public: Base3(int mem = 4) : m_iBase3Mem(mem) { ; } virtual void vBase3func1() { std::cout << "In Base3 vfunc1()" << std::endl; } virtual void vBase3func2() { std::cout << "In Base3 vfunc2()" << std::endl; } private: int m_iBase3Mem; }; class Devired: public Base, public Base2, public Base3 { public: Devired(int mem = 7) : m_iMem1(mem) { ; } virtual void vdfunc1() { std::cout << "In Devired vdfunc1()" << std::endl; } private: int m_iMem1; }; int _tmain(int argc, _TCHAR* argv[]) { // Test_3 Devired d; int *dAddress = (int*)&d; /* 1. 获取对象的内存布局信息 */ // 虚表地址一 int *vtptr1 = (int*)*(dAddress + 0); int basemem1 = (int)*(dAddress + 1); int basemem2 = (int)*(dAddress + 2); int *vtpttr2 = (int*)*(dAddress + 3); int base2mem = (int)*(dAddress + 4); int *vtptr3 = (int*)*(dAddress + 5); int base3mem = (int)*(dAddress + 6); /* 2. 输出对象的内存布局信息 */ int *pBaseFunc1 = (int *)*(vtptr1 + 0); int *pBaseFunc2 = (int *)*(vtptr1 + 1); int *pBaseFunc3 = (int *)*(vtptr1 + 2); int *pBaseFunc4 = (int *)*(vtptr1 + 3); (FUNC(pBaseFunc1))(); (FUNC(pBaseFunc2))(); (FUNC(pBaseFunc3))(); (FUNC(pBaseFunc4))(); // .... 后面省略若干输出内容,可自行补充 return 0; }
调试输出如下图,这里的展示结果与本节开始所展示的内存布局图是一致的
3.2 有虚函数覆盖
本节不再给出任何分析,读者如果想彻底搞明白可以根据本文上述内容自行画图写代码验证。
https://jocent.me/2017/08/07/virtual-table.html