C++虚函数究竟是如何实现的?有虚函数的对象的内存结构是什么样的?写几行代码测试一下就很容易理解了。
目录
一、测试代码
二、运行测试
三、分析结果
四、结论
一、测试代码
首先用VS2022建立一个C++控制台项目(或者随便什么C++项目,别的VS版本当然也没问题),添加下面的代码:
class A
{
public:long long a = 3;virtual void f1() {}virtual void f2() {}
};
class A2
{
public:long long a = 4;virtual void f1() {}virtual void f2() {}
};
class B :public A,public A2
{
public:long long b = 5;virtual void f1() {}virtual void f3() {}
};
void test()
{A a;A2 a2;B b;A* pA = &b;A2* pA2 = &b;int aa = sizeof(A);int bb = sizeof(B);b.b += 1;
}
解释一下代码:两个基类A和A2,各有两个虚函数和一个成员变量,成员变量有特定初值,方便确认内存数据。子类B,多继承自A和A2,同时有一个新增的虚函数。
二、运行测试
在main函数开始处调用test()函数,在test函数最后一句设置断点,然后调试执行,见下图:
图中下部是监视窗口,悬浮部分为内存窗口,在监视窗口选择一行,点右键,复制数据,贴到内存窗口,删掉多余部分,回车,即可显示指定的内存,拉伸内存窗口,使窗口刚好每行16个字节。
三、分析结果
鼠标移到变量aa和bb上面显示A的大小为16字节、B的大小为40字节,这是因为A包含一个虚表指针和一个成员(long long为8字节),而B包含两个虚表指针(为什么是两个后面分析)和三个成员。
观察内存窗口的数据(从&a开始显示),第一行是变量A a,后面全cc的是debug版的填充行,方便检测内存越界的,第四行是变量A2 a2,这两个变量的结构都是8字节虚表指针和8字节数据,预设的特征值3、4下面画了红线,第七行开始是变量B b,先是一个A,再是一个A2,第九行开始是B增加的变量,预设值是5。
现在我们观察B的数据,第七行和第八行开始处分别是父对象A和A2的虚表指针,注意它们和独立定义的A和A2对象(第一行和第四行)的虚表指针完全不同,而第八行A2的虚表指针比第七行A的虚表指针大16,刚好是两个指针长度,这说明每种类型都有自己的虚表(为什么不是每个对象?后面解释),对象的父对象的虚表指针指向虚表的某个位置,对象的第一个虚表指针同时也是第一个父对象的虚表指针。
修改上面的代码,增加一个独立的A类型的变量,观察内存会发现两个独立的A对象的虚表指针相同,说明虚表是类型共同所有的,注意,是类型,而不是类型中包含的继承部分,示例代码中的A、A2和B是类型,B类型对象继承的A的虚表指针和独立A类型的虚表指针不相同(所以才能实现虚函数指针调用子类的虚函数实现)。
四、结论
含有虚函数的对象拥有多个虚表指针,整个类型则共享一个虚表,虚表指针计算在sizeof里,虚表则不在里面。虚表指针的数量与基类数量有关,但计算上比较麻烦,因为第一个虚表指针同时也是第一个有虚函数的父类的虚表指针,有很多继承层级和多继承的时候很复杂。
对象的每个有虚函数的父类都有一个虚表指针,指向对象的虚表的某个部分,如果父类没有虚函数怎么办?修改上面的代码,删掉父类A的虚函数,会发现子类B的布局变成了“A2-A-B”,也就是说,为了节省内存,有虚函数的父类被放在了前面,保证对象和第一个有虚函数的父类共享第一个虚表指针。
现在很容易理解,从子类指针到父类指针是如何转换的,以及,虚函数调用是如何实现的。
另:C++对象的内存布局是编译器决定的,没有标准,以上只代表某种情形。
(这里是文档结束)