c++对象内存布局

直接开始吧。主要是各种dump比较多,内容没多少的!

#include <iostream>

using namespace std;

class TestClass {
public:
	TestClass() :Field1(10), Field2(20){}
	void Print() {
		cout << "TestClass::Print()" << endl;
	}
private:
	int Field1;
	int Field2;
};

通过dump,我们能得到以下的资讯!

已启动生成…
1>------ 已启动生成: 项目: cppRTTI, 配置: Debug Win32 ------
1>Main.cpp
1>Test2.cpp
1>class TestClass	size(8):
1>	+---
1> 0	| Field1
1> 4	| Field2
1>	+---
1>正在生成代码...
1>cppRTTI.vcxproj -> G:\other_workspace\cppRTTI\Debug\cppRTTI.exe
========== 生成: 成功 1 个,失败 0 个,最新 0 个,跳过 0  ==========

既然起始的第一个就是成员对象(固定是int,方便后面的查找,其他类型按大小做偏移就可以了!)

void DumpClassLayout() {
	TestClass class1;
	cout << "sizeof(class1) == " << sizeof(class1) << endl;
	int* p = (int*)&class1;
	for (int i = 0; i < sizeof(class1) / sizeof(int); i++, p++)
	{
		cout << "class1.field" << i+1 << " = " << *p << ", address = " << p << endl;
	}
}

int main() {
	DumpClassLayout();
	int pause;
	cin >> pause;
	return 0;
}

这里解释一下int* p = (int*)&class1;(已经会的可以跳过!) 由于class1对象的内存布局里,第一个是一个int类型,所以第一块内存是存放一个int类型的,但是我们是通过&class1获取到的地址,所以通过强转后,得到的就是第一个成员的地址int *

执行结果:

sizeof(class1) == 8
class1.field1 = 10, address = 003CF670
class1.field2 = 20, address = 003CF674

从现在看到结果看来,函数是不放在对象内存中的!

使得TestClass继承BaseClass

class BaseClass {
public:
	BaseClass() :BaseField1(-10) {}
private:
	int BaseField1;
};

class TestClass : BaseClass {
public:
	TestClass() :Field1(10), Field2(20){}
	void Print() {
		cout << "TestClass::Print()" << endl;
	}
private:
	int Field1;
	int Field2;
};

同样适用前面的代码,得到以下结果:

已启动重新生成…
1>------ 已启动全部重新生成: 项目: cppRTTI, 配置: Debug Win32 ------
1>Main.cpp
1>Test2.cpp
1>class TestClass	size(12):
1>	+---
1> 0	| +--- (base class BaseClass)
1> 0	| | BaseField1
1>	| +---
1> 4	| Field1
1> 8	| Field2
1>	+---
1>正在生成代码...
1>cppRTTI.vcxproj -> G:\other_workspace\cppRTTI\Debug\cppRTTI.exe
========== 全部重新生成: 成功 1 个,失败 0 个,跳过 0  ==========
sizeof(class1) == 12
class1.field1 = -10, address = 010FF774
class1.field2 = 10, address = 010FF778
class1.field3 = 20, address = 010FF77C

从结果来看,子类的内存是在前面的!

改成

class TestClass : virtual BaseClass

使用虚继承看看:

已启动生成…
1>------ 已启动生成: 项目: cppRTTI, 配置: Debug Win32 ------
1>Test2.cpp
1>class TestClass	size(16):
1>	+---
1> 0	| {vbptr}
1> 4	| Field1
1> 8	| Field2
1>	+---
1>	+--- (virtual base BaseClass)
1>12	| BaseField1
1>	+---
1>TestClass::$vbtable@:
1> 0	| 0
1> 1	| 12 (TestClassd(TestClass+0)BaseClass)
1>vbi:	   class  offset o.vbptr  o.vbte fVtorDisp
1>       BaseClass      12       0       4 0
1>cppRTTI.vcxproj -> G:\other_workspace\cppRTTI\Debug\cppRTTI.exe
========== 生成: 成功 1 个,失败 0 个,最新 0 个,跳过 0  ==========
sizeof(class1) == 16
class1.field1 = 5942204, address = 00B5FB88
class1.field2 = 10, address = 00B5FB8C
class1.field3 = 20, address = 00B5FB90
class1.field4 = -10, address = 00B5FB94

虚继承后,内存布局一下子就不一样了!(同样是之前的代码)

先是{vbptr} 然后是TestClass自身的成员,最后才是虚继承的类BaseClass的成员

原因的话,了解菱形继承的估计都能大致猜到原因了!

删除虚继承后尝试添加一个虚函数

class TestClass : BaseClass {
    ...
    virtual void Print2() {
		cout << "TestClass::Print2()" << endl;
	}
    virtual void Print3() {
		cout << "TestClass::Print3()" << endl;
	}
}

TestClass的布局就变成这样了:

已启动重新生成…
1>------ 已启动全部重新生成: 项目: cppRTTI, 配置: Debug Win32 ------
1>Main.cpp
1>Test2.cpp
1>class TestClass	size(16):
1>	+---
1> 0	| {vfptr}
1> 4	| +--- (base class BaseClass)
1> 4	| | BaseField1
1>	| +---
1> 8	| Field1
1>12	| Field2
1>	+---
1>TestClass::$vftable@:
1>	| &TestClass_meta
1>	|  0
1> 0	| &TestClass::Print2
1> 1	| &TestClass::Print3
1>TestClass::Print2 this adjustor: 0
1>TestClass::Print3 this adjustor: 0
1>正在生成代码...
1>cppRTTI.vcxproj -> G:\other_workspace\cppRTTI\Debug\cppRTTI.exe
========== 全部重新生成: 成功 1 个,失败 0 个,跳过 0  ==========

在对象内存的第一个是{vfptr}(不是{vbptr});

如果在有虚函数和虚继承的情况下,内存布局又是怎么样的呢?

已启动生成…
1>------ 已启动生成: 项目: cppRTTI, 配置: Debug Win32 ------
1>Test2.cpp
1>class TestClass	size(20):
1>	+---
1> 0	| {vfptr}
1> 4	| {vbptr}
1> 8	| Field1
1>12	| Field2
1>	+---
1>	+--- (virtual base BaseClass)
1>16	| BaseField1
1>	+---
1>TestClass::$vftable@:
1>	| &TestClass_meta
1>	|  0
1> 0	| &TestClass::Print2
1> 1	| &TestClass::Print3
1>TestClass::$vbtable@:
1> 0	| -4
1> 1	| 12 (TestClassd(TestClass+4)BaseClass)
1>TestClass::Print2 this adjustor: 0
1>TestClass::Print3 this adjustor: 0
1>vbi:	   class  offset o.vbptr  o.vbte fVtorDisp
1>       BaseClass      16       4       4 0
1>cppRTTI.vcxproj -> G:\other_workspace\cppRTTI\Debug\cppRTTI.exe
========== 生成: 成功 1 个,失败 0 个,最新 0 个,跳过 0  ==========

既然第一个地址是虚函数表{vfptr},那么我们尝试一下获取并执行一下看看!

static int g_int = 1111;
typedef void (*Function)(TestClass cl);
void DumpClassLayout() {
	TestClass class1;
	cout << "static int address = " << &g_int << endl;
	cout << "sizeof(class1) = " << sizeof(class1) << " address = "<< &class1 << endl;

	int* p = (int*)&class1;
	for (int i = 0; i < sizeof(class1) / sizeof(int); i++, p++)
	{
		cout << "class1.field" << i+1 << " = " << *p << ", address = " << p << endl;
	}
	int** functionPtr = (int**)*(int**)&class1;
	while (*functionPtr) {
		cout << "vftable address = " << functionPtr << " address Value = " << *functionPtr << endl;
		Function func = (Function)*functionPtr;
		func(class1);
		functionPtr++;
	}
}

添加了数据区地址的获取;(全局静态数据)

static int address = 00A2D000
sizeof(class1) = 16 address = 0133FA54
class1.field1 = 10660932, address = 0133FA54
class1.field2 = -10, address = 0133FA58
class1.field3 = 10, address = 0133FA5C
class1.field4 = 20, address = 0133FA60
vftable address = 00A2AC44 address Value = 00A212F8
TestClass::Print2()
vftable address = 00A2AC48 address Value = 00A21082
TestClass::Print3()

$00A2AC44$=$10660932$

通过对地址的比较,我们知道虚函数表是处于内存的代码段区的(位于全局静态数据区前);而虚表对应的函数地址还要再虚表的地址前。

以上都是在x86的情况运行的,x86的一个指针是4位,在x64的时候是8位,而int都是4位,所以在x64的情况下,我们能看到对齐和被拆成两部分的虚表地址!

static int address = 00007FF79E8CF000
sizeof(class1) = 24 address = 000000DEF70FF5C8
class1.field1 = -1634943120, address = 000000DEF70FF5C8
class1.field2 = 32759, address = 000000DEF70FF5CC
class1.field3 = -10, address = 000000DEF70FF5D0
class1.field4 = 10, address = 000000DEF70FF5D4
class1.field5 = 20, address = 000000DEF70FF5D8
class1.field6 = -858993460, address = 000000DEF70FF5DC
vftable address = 00007FF79E8CBF70 address Value = 00007FF79E8C128A
TestClass::Print2()
vftable address = 00007FF79E8CBF78 address Value = 00007FF79E8C143D
TestClass::Print3()

前面的代码已经具有以下特征:

TestClass

  • int Field1
  • int Field2
  • virtual void Print2()
  • virtual void Print3()

BaseClass

  • BaseField1

现在我们试试在BaseClass里添加三个虚函数

class BaseClass {
public:
    ...
	virtual void Print2() {
		cout << "BaseClass::Print2()" << endl;
	}
	virtual void Print4() {
		cout << "BaseClass::Print4()" << endl;
	}
    void Print5() {
		cout << "BaseClass::Print5()" << endl;
	}
}

改造完成后,TestClass的内存布局就成这样:

已启动生成…
1>------ 已启动生成: 项目: cppRTTI, 配置: Debug Win32 ------
1>Test2.cpp
1>class TestClass	size(16):
1>	+---
1> 0	| +--- (base class BaseClass)
1> 0	| | {vfptr}
1> 4	| | BaseField1
1>	| +---
1> 8	| Field1
1>12	| Field2
1>	+---
1>TestClass::$vftable@:
1>	| &TestClass_meta
1>	|  0
1> 0	| &TestClass::Print2
1> 1	| &BaseClass::Print4
1> 2	| &TestClass::Print3
1>TestClass::Print2 this adjustor: 0
1>TestClass::Print3 this adjustor: 0
1>cppRTTI.vcxproj -> G:\other_workspace\cppRTTI\Debug\cppRTTI.exe
========== 生成: 成功 1 个,失败 0 个,最新 0 个,跳过 0  ==========

{vfptr}虚表放到了基类去了,但是是从整体上看,对象的第一个指针还是虚表!

TestClass::Print2在没有overrider的情况下,还是能实现重载的!

下面在来个virtual继承的大综合看看

class TestClass : virtual BaseClass
已启动生成…
1>------ 已启动生成: 项目: cppRTTI, 配置: Debug Win32 ------
1>Test2.cpp
1>class TestClass	size(28):
1>	+---
1> 0	| {vfptr}
1> 4	| {vbptr}
1> 8	| Field1
1>12	| Field2
1>	+---
1>16	| (vtordisp for vbase BaseClass)
1>	+--- (virtual base BaseClass)
1>20	| {vfptr}
1>24	| BaseField1
1>	+---
1>TestClass::$vftable@TestClass@:
1>	| &TestClass_meta
1>	|  0
1> 0	| &TestClass::Print3
1>TestClass::$vbtable@:
1> 0	| -4
1> 1	| 16 (TestClassd(TestClass+4)BaseClass)
1>TestClass::$vftable@BaseClass@:
1>	| -20
1> 0	| &(vtordisp) TestClass::Print2
1> 1	| &BaseClass::Print4
1>TestClass::Print2 this adjustor: 20
1>TestClass::Print3 this adjustor: 0
1>vbi:	   class  offset o.vbptr  o.vbte fVtorDisp
1>       BaseClass      20       4       4 1
1>cppRTTI.vcxproj -> G:\other_workspace\cppRTTI\Debug\cppRTTI.exe
========== 生成: 成功 1 个,失败 0 个,最新 0 个,跳过 0  ==========

对比一下没有使用虚继承:

  • 16 vs 28

首先是对象大小,虚继承的对象内存是28;对比知道,多了{vfptr}{vbptr}(vtordisp for vbase BaseClass) 每个都是4字节(x86);

  • vtordisp

关于 vtordisp for vbase BaseClass,MSDN,虚继承中派生类重写了基类的虚函数,并且在构造函数或者析构函数中使用指向基类的指针调用了该函数,编译器会为虚基类添加vtordisp域

  • vfptr

{vfptr}这里有两份,一份是在BaseClass, 一份在TestClass;但是这种情况不是一定的,如果TestClassBaseClassVirtual全部一致(TestClass也不能多!),那么{vfptr}就只有一份了!(内存也就减少了!)

以下就是将TestClass::Print3Virtual删除,添加virtual TestClass::Print4()

已启动生成…
1>------ 已启动生成: 项目: cppRTTI, 配置: Debug Win32 ------
1>Test2.cpp
1>class TestClass	size(24):
1>	+---
1> 0	| {vbptr}
1> 4	| Field1
1> 8	| Field2
1>	+---
1>12	| (vtordisp for vbase BaseClass)
1>	+--- (virtual base BaseClass)
1>16	| {vfptr}
1>20	| BaseField1
1>	+---
1>TestClass::$vbtable@:
1> 0	| 0
1> 1	| 16 (TestClassd(TestClass+0)BaseClass)
1>TestClass::$vftable@:
1>	| -16
1> 0	| &(vtordisp) TestClass::Print2
1> 1	| &(vtordisp) TestClass::Print4
1>TestClass::Print2 this adjustor: 16
1>TestClass::Print4 this adjustor: 16
1>vbi:	   class  offset o.vbptr  o.vbte fVtorDisp
1>       BaseClass      16       0       4 1
1>cppRTTI.vcxproj -> G:\other_workspace\cppRTTI\Debug\cppRTTI.exe
========== 生成: 成功 1 个,失败 0 个,最新 0 个,跳过 0  ==========

到这里c++对象的内存基本算是完成了!