C++对象模型之虚基类表和虚函数表的布局(二)
注:本文的c++对象结构模型基于vs编译器的win32环境.
链接: C++对象模型之虚基类表和虚函数表的布局(一).
链接: C++对象模型之虚基类表和虚函数表的布局(三).
一、虚函数表
(4)补充
在上文中我们探究虚函数表空间布局的方式是直接用子类对象指针申请子类对象空间,如果我们用父类指针申请子类对象,编译器会如何处理呢?
Base1* derived1 = new Derived;
Base2* derived2 = new Derived;

红色方框框出了两者不一样的部分,我们可以看出,在 Base2* derived2 = new Derived;中寄存器eax被add了8字节,这就说明this指针移动了8字节,正好跳过继承自Base1的虚函数表指针和成员变量m_base1的储存空间,最终指向继承自Base2的空间。
二、虚基类表
在面对祖孙三代继承问题时,为了防止祖父类的成员被孙子类重复多次继承,我们采用虚基类的方式。只有microsoft编译器处理虚基类时使用虚基类表的形式,而其他编译器大多采用在虚函数表中放置虚基类的偏移量的方式。
我们针对菱形的继承关系来探究虚基类问题:
class Grand
{
public:
int m_grand = 1;
};
class Base1 : virtual public Grand
{
public:
int m_base1 = 2;
static int n_base1; //静态成员变量不占用类对象空间
};
int Base1::n_base1 = 10; //静态成员变量在类外初始化
class Base2 : virtual public Grand
{
public:
int m_base2 = 3;
};
class Derived : public Base1, public Base2
{
public:
int m_derived = 4;
};
测试代码:
(1)首先看看这几个类对象占内存的大小
int main()
{
cout << sizeof(Grand) << endl; //4byte
cout << sizeof(Base1) << endl; //12byte
cout << sizeof(Base2) << endl; //12byte
cout << sizeof(Derived) << endl; //24byte
return 0;
}
根据上篇文章所介绍的那样,非静态成员变量会占用对象空间,Grand类为4字节就很好理解,但是Base1、Base2类除去继承自父类的和本身的非静态成员变量之外,各自多出了4字节的空间Derived类多出8字节的空间,这4字节或8字节的空间存放了什么呢?答案正式虚基类表指针。
(2)其次看看类对象的空间分布
int main()
{
Base1 base1;
Base2 base2;
Derived derived;
long* ptrB = reinterpret_cast<long*>(&base1);
printf("0x%p\n", ptrB);
printf("%d\n", *(ptrB + 1));
printf("%d\n", *(ptrB + 2));
cout << "====================================" << endl;
long* ptrB2 = reinterpret_cast<long*>(&base2);
printf("0x%p\n", ptrB2);
printf("%d\n", *(ptrB2 + 1));
printf("%d\n", *(ptrB2 + 2));
cout << "====================================" << endl;
long* ptrD = reinterpret_cast<long*>(&derived);
printf("0x%p\n", ptrD);
printf("%d\n", *(ptrD + 1));
printf("0x%p\n", ptrD + 2);
printf("%d\n", *(ptrD + 3));
printf("%d\n", *(ptrD + 4));
printf("%d\n", *(ptrD + 5));
cout << "pig" << endl;
return 0;
}

这样我们对类对象内存布局有了一个基本的认识------开头会放置一个虚基类表指针vbptr,接着存放自己的数据成员,最后存放继承自虚基类的数据成员。对于孙子类,其内存排布是按顺序存放继承自父类的虚基类指针和数据成员,后存放自己的数据成员,最后存放继承自虚基类的数据成员。下图就是derived对象的空间分布,24字节非常明显。

(3)再看看虚基类表的布局
int main()
{
Base1 base1;
Base2 base2;
Derived derived;
long* ptrB = reinterpret_cast<long*>(&base1);
long* vptrB = reinterpret_cast<long*>(*ptrB);
for (int i = 0; i < 2; i++) //打印虚基类表内容
{
printf("vptrB[%d] = %d\n", i, vptrB[i]);
}
cout << "===========================================" << endl;
long* ptrB2 = reinterpret_cast<long*>(&base2);
long* vptrB2 = reinterpret_cast<long*>(*ptrB2);
for (int i = 0; i < 2; i++) //打印虚基类表内容
{
printf("vptrB[%d] = %d\n", i, vptrB2[i]);
}
cout << "===========================================" << endl;
long* ptrD = reinterpret_cast<long*>(&derived);
long* vptrD = reinterpret_cast<long*>(*ptrD); //虚基类表首地址
for (int i = 0; i < 7; i++) //打印虚基类表内容
{
printf("vptrD[%d] = %d\n", i, vptrD[i]);
}
return 0;
}

可以分析出虚基类表的:
- 0-4字节存放的是:虚基类表指针地址相对于对象首地址偏移值,当类不包含虚函数是,对象首地址与虚基类表指针地址相同,该值为0; 而当类中存在虚函数时,虚函数表指针会存放在对象空间的最前,该值为-4。
- 5+字节存放的是:虚基类表指针地址相对于虚基类子对象数据成员的地址便宜偏移值。
- 最后4字节是结束标志位,放0。

本文深入探讨了C++中虚基类表和虚函数表的布局方式,特别是在MVC环境下,通过具体实例分析了虚基类表指针、虚函数表指针的内存布局,以及它们在多继承中的作用。
2855

被折叠的 条评论
为什么被折叠?



