C++对象模型之虚基类表和虚函数表的布局(一)
注:本文的c++对象结构模型基于vs编译器的win32环境,只有microsoft编译器处理虚基类时使用虚基类表的形式,而其他编译器大多采用在虚函数表中放置虚基类的偏移量的方式。
链接: C++对象模型之虚基类表和虚函数表的布局(二).
链接: C++对象模型之虚基类表和虚函数表的布局(三).
一、虚函数表
当一个类包含一个或多个虚函数时,编译器会在他的对象中创建一个虚函数指针,这个虚函数指针指向虚函数表,虚函数表里记录者这些虚函数的地址。很多人好奇虚函数指针、虚函数表、虚函数分别处于内存的什么位置?
答案只有看看汇编部分才能知晓,据笔者的观察,虚函数表位于全局数据区,可供各个对象调用;而虚函数位于代码区。
而当一个子类继承了多个父类时,子类的虚函数表就会变得复杂,我们通过代码实验的方式,来探究c++对象模型中虚函数表的布局。
class Base1
{
public:
int m_base1;
static int n_base1;
public:
virtual void x() { cout << "Base1::x()" << endl; }
virtual void y() { cout << "Base1::y()" << endl; }
virtual void z() { cout << "Base1::z()" << endl; }
};
int Base1::n_base1 = 1; //静态成员变量在类外初始化
class Base2
{
public:
int m_base2;
public:
virtual void a() { cout << "Base2::a()" << endl; }
virtual void b() { cout << "Base2::b()" << endl; }
};
class Derived : public Base1, public Base2
{
public:
int m_derived;
public:
virtual void y() { cout << "Derived::y()" << endl; }
virtual void m() { cout << "Derived::m()" << endl; }
virtual void n() { cout << "Derived::n()" << endl; }
};
测试代码:
(1)首先看看这几个类对象占内存的大小
int main()
{
cout << sizeof(Base1) << endl; //8byte
cout << sizeof(Base2) << endl; //8byte
cout << sizeof(Derived) << endl; //20byte
return 0;
}
分析:占用类对象空间的成员是非静态成员变量、虚函数表指针。静态成员变量和成员函数均不占用类对象空间,他们跟着类走。
- 对于Base1,4byte的虚函数表指针vftpr1 + 4byte的m_base1 = 8byte
- 对于Base2,4byte的虚函数表指针vfptr2 + 4byte的m_base2 = 8byte
- 对于Derived,4byte的虚函数表指针vftpr1 + 4byte的m_base1 + 4byte的虚函数表指针vbptr2 + 4byte的m_base2 + 4byte的m_derived = 20byte
(2)其次看看类对象的空间分布
int main()
{
typedef void(*Func)(void);
Base1 *base1 = new Base1;
long* ptrB = reinterpret_cast<long*>(base1);
printf("ptrB is 0x:%p\n", ptrB);
printf("m_base1 is %d\n", *(ptrB + 1));
long* vptrB = reinterpret_cast<long*>(*ptrB); //虚函数表首地址
for (int i = 0; i < 4; i++) //打印虚函数表内容
{
printf("vptrD[%d] = 0x:%p\n", i, vptrD[i]);
}
cout << "=======================================================" << endl;
//观察虚函数
Func ptr0 = (Func)vptrB[0];
Func ptr1 = (Func)vptrB[1];
Func ptr2 = (Func)vptrB[2];
Func ptr3 = (Func)vptrB[3];
ptr0();
ptr1();
ptr2();
ptr3();
delete base1; //养成好习惯,防止内存泄漏
base1 = NULL; //养成好习惯,防止出现野指针
return 0;
}
以上结果足以说明,如果类中有虚函数,那么该类的对像空间布局为:首先放置虚函数表指针,随后放置成员变量。(当然堆区空间是由低地址向高地址生长的)我们随后观察子类来验证并扩充这一点。下图正式子类对象空间布局图。
(3)再看看虚函数表的布局
我们直接从最复杂的子类入手:
int main()
{
typedef void (*Func)(void);
Derived *derived = new Derived;
long* ptrD = reinterpret_cast<long*>(derived);
//printf("ptrD is 0x:%p\n", ptrD);
//printf("m_base1 is %d\n", *(ptrD + 1));
long* vptrD = reinterpret_cast<long*>(*ptrD); //虚函数表首地址
for (int i = 0; i < 10; i++) //打印虚函数表内容
{
printf("vptrD[%d] = 0x:%p\n", i, vptrD[i]);
}
cout << "===========================================================" << endl;
//观察虚函数
Func ptr0 = (Func)vptrD[0];
Func ptr1 = (Func)vptrD[1];
Func ptr2 = (Func)vptrD[2];
Func ptr3 = (Func)vptrD[3];
Func ptr4 = (Func)vptrD[4];
Func ptr7 = (Func)vptrD[7];
Func ptr8 = (Func)vptrD[8];
ptr0();
ptr1();
ptr2();
ptr3();
ptr4();
ptr7();
ptr8();
cout << "===========================================================" << endl;
cout << "===========================================================" << endl;
long* vptrD2 = reinterpret_cast<long*>(*(ptrD+2)); //虚函数表首地址
for (int i = 0; i < 3; i++) //打印虚函数表内容
{
printf("vptrD2[%d] = 0x:%p\n", i, vptrD2[i]);
}
cout << "===========================================================" << endl;
//观察虚函数
Func ptr00 = (Func)vptrD2[0];
Func ptr11 = (Func)vptrD2[1];
ptr00();
ptr11();
delete derived;
}
由此可以看出:
- 1.一个带有虚函数的类会产生一个虚函数表,继承自父类的虚函数表指针指向该虚函数表的不同位置。
- 2.子类自己的虚函数归在第一个父类的虚函数指针指向区域中。
- 3.虚函数表区域结束以0x00000000为标志。
- 4.子函数重写父类的虚函数会自动替换虚函数表中的虚函数。
Derived类产生的虚函数表: