C++对象模型存储与分布
本文介绍C++对象模型的存储与分布,参考编译器为vs2019(不同编译器会有差别),主要介绍对象模型的存储与其成员分布,包括在继承,虚拟继承等多种情况下的对象存储模型
类中成员的存储位置
类中的成员:
- 数据(data)
- 静态数据成员(static member data)
- 非静态数据成员(no-static member data)
- 函数(function)
- 分为静态函数成员(static member function)
- 非静态函数成员(no-static member function)
其中只有非静态数据成员存放在对象中,其余都不会存放在对象中。
如下图表示:
对象内数据的存储分布
在类的内部数据分布中,影响其分布的因素主要有对齐、虚函数、继承、虚拟继承等元素影响
数据对齐:
// Foo.cpp
class Foo {
public:
char c_data;
int i_data;
};
在vs2019中的工具–命令行中输入cl /d1reportSingleClassLayoutFoo Foo.cpp
结果如下:
class Foo size(8):
+---
0 | c_data
| <alignment member> (size=3)
4 | i_data
+---
关于数据对齐,可以看这里,我总结遵循两个对齐规则:1.对象(结构体)内部每个成员的起始地址是其成员数据大小的整数倍;2. 对象(结构体)整体大小是其成员中最大的数据类型的整数倍。
在c_data后面有3个字节的填充,所以上述类的对象大小为:8 bytes。
需要注意的是一个空类(没有数据成员)的对象仍要占用一个字节:
如:
class Foo {
public:
};
结果如下:
class Foo size(1):
+---
+---
因为这样才能保证每个对象有唯一的地址对应。
带有虚函数(带有virtual关键字)的存储分布:
class Vbase {
public:
int i_data;
virtual void f_base() { std::cout << "f_base"; };
virtual void f2_base() { std::cout << "f2_base"; };
Vbase() {}
virtual ~Vbase() {}
};
结果如下:
class Vbase size(8):
+---
0 | {vfptr}
4 | i_data
+---
Vbase::$vftable@:
| &Vbase_meta
| 0
0 | &Vbase::f_base
1 | &Vbase::f2_base
2 | &Vbase::{dtor}
Vbase::f_base this adjustor: 0
Vbase::f2_base this adjustor: 0
Vbase::{dtor} this adjustor: 0
Vbase::__delDtor this adjustor: 0
Vbase::__vecDelDtor this adjustor: 0
带有virtual关键字的对象中会存储一个虚函数表指针(vfptr),大小为4 bytes(编译32位程序,若编译64位程序则为8 bytes),这里讨论模型存储对象,后面虚函数表Vbase....
等内容暂不讨论。
单继承带有虚函数(带有virtual关键字)的存储分布:
Vderiver类继承Vbase类,并实现了虚函数f_base()。
class Vbase {
public:
int i_data;
virtual void f_base() { std::cout << "f_base"; };
virtual void f2_base() { std::cout << "f2_base"; };
Vbase() {}
virtual ~Vbase() {}
};
class Vderiver :public Vbase{
public:
char c_data;
virtual void f_base() { std::cout << "f_derver"; };
};
结果如下:
// Vbase 结构
class Vbase size(8):
+---
0 | {vfptr}
4 | i_data
+---
Vbase::$vftable@:
| &Vbase_meta
| 0
0 | &Vbase::f_base
1 | &Vbase::f2_base
2 | &Vbase::{dtor}
///
// Vderiver 结构
class Vderiver size(12):
+---
0 | +--- (base class Vbase)
0 | | {vfptr}
4 | | i_data
| +---
8 | c_data
| <alignment member> (size=3)
+---
Vderiver::$vftable@:
| &Vderiver_meta
| 0
0 | &Vderiver::f_base
1 | &Vbase::f2_base
2 | &Vderiver::{dtor}
子类继承父类,由于父类带有虚函数,所以子类也会有虚函数表,所以子类size=父类成员变量size+虚函数表指针size + 子类成员变量size。
多继承带有虚函数(带有virtual关键字)的存储分布:
多继承的继承结构如图:
class Vbase {
public:
int i_data;
virtual void f_base() { std::cout << "f_base"; };
virtual void f2_base() { std::cout << "f2_base"; };
Vbase() {}
virtual ~Vbase() {}
};
class Vbase2 {
public:
long i_data;
virtual void f_base2() { std::cout << "f_base2"; };
Vbase2() {}
virtual ~Vbase2() {}
};
class Vderiver :public Vbase,public Vbase2{
public:
char c_data;
void f_base2() override{ std::cout << "f_base2_derver"; };
void f_base() override{ std::cout << "f_derver"; };
};
其结果如下:
// VBase 类结构
class Vbase size(8):
+---
0 | {vfptr}
4 | i_data
+---
Vbase::$vftable@:
| &Vbase_meta
| 0
0 | &Vbase::f_base
1 | &Vbase::f2_base
2 | &Vbase::{dtor}
// Vbase2 的结构
class Vbase2 size(8):
+---
0 | {vfptr}
4 | i_data
+---
Vbase2::$vftable@:
| &Vbase2_meta
| 0
0 | &Vbase2::f_base2
1 | &Vbase2::{dtor}
///
// Vderiver的结构
class Vderiver size(20):
+---
0 | +--- (base class Vbase)
0 | | {vfptr}
4 | | i_data
| +---
8 | +--- (base class Vbase2)
8 | | {vfptr}
12 | | i_data
| +---
16 | c_data
| <alignment member> (size=3)
+---
Vderiver::$vftable@Vbase@:
| &Vderiver_meta
| 0
0 | &Vderiver::f_base
1 | &Vbase::f2_base
2 | &Vderiver::{dtor}
Vderiver::$vftable@Vbase2@:
| -8
0 | &Vderiver::f_base2
1 | &thunk: this-=8; goto Vderiver::{dtor}
多继承的情况与单继承类似,都是继承了父类的成员变量,并将虚函数表指针一并继承过来。
派生类对象的数据访问:
对于下面代码代码:
Vderiver vd;
Vbase *vb = &vd;
Vbase2 * vb2 = &vd;
代码是合法的,vb和vb2可以分别访问对象vd中的Vbase区的i_data和Vbase2区的id_data,那么vb和vb2中的赋值操作又作了什么?
其实编译器在复制时早已计算了派生类对象中的内部嵌套对象的便宜,对于Vbase *vb = &vd
,内部转化为vb = (Vbase*)((char*)&v3d + 0)
,其中0为Vbase对象在子类对象中的偏移;对于Vbase2 *vb2 = &vd
,内部转化为vb2 = (Vbase*)((char*)&v3d + sizeof(Vbase))
。
带有虚拟继承的存储分布:
虚拟继承的模型如下图:
代码如下:
class Vbase0 {
public:
long long ll_data;
virtual void f_base0() { std::cout << "f_base0"; }
};
class Vbase1 :public virtual Vbase0{
public:
int i_data;
virtual void f_base() { std::cout << "f_base"; };
virtual void f2_base() { std::cout << "f2_base"; };
Vbase1() {}
virtual ~Vbase1() {}
};
class Vbase2 :public virtual Vbase0 {
public:
long i_data;
virtual void f_base2() { std::cout << "f_base2"; };
Vbase2() {}
virtual ~Vbase2() {}
};
class Vderiver :public Vbase1, public Vbase2 {
public:
char c_data;
void f_base2() override { std::cout << "f_base2_derver"; };
void f_base() override { std::cout << "f_derver"; };
};
带有virtual
关键字的继承为虚拟继承,代码中Vderiver类中的ll_data
数据成员只有一个成员。(如果继承时没有virtual关键字,ll_data会在类Vbase1区和Vbase2区都有其数据成员)。
各个类的成员数据分布如下:
class Vbase0 size(16):
+---
0 | {vfptr}
8 | ll_data
+---
class Vbase1 size(32):
+---
0 | {vfptr}
4 | {vbptr}
8 | i_data
| <alignment member> (size=4)
+---
+--- (virtual base Vbase0)
16 | {vfptr}
24 | ll_data
+---
class Vbase2 size(32):
+---
0 | {vfptr}
4 | {vbptr}
8 | i_data
| <alignment member> (size=4)
+---
+--- (virtual base Vbase0)
16 | {vfptr}
24 | ll_data
+---
class Vderiver size(48):
+---
0 | +--- (base class Vbase1)
0 | | {vfptr}
4 | | {vbptr}
8 | | i_data
| | <alignment member> (size=4)
| +---
16 | +--- (base class Vbase2)
16 | | {vfptr}
20 | | {vbptr}
24 | | i_data
| | <alignment member> (size=4)
| +---
28 | c_data
| <alignment member> (size=3)
+---
+--- (virtual base Vbase0)
32 | {vfptr}
40 | ll_data
+---
可以看到在类Vderiver中的ll_data成员只有一个,如果将继承中的virtual关键字去掉,class Vbase1 :public virtual Vbase0
和class Vbase2 :public virtual Vbase0
改成class Vbase1 :public Vbase0
和class Vbase2 :public Vbase0
后。其分布结果如下:
class Vbase0 size(16):
+---
0 | {vfptr}
8 | ll_data
+---
class Vbase1 size(24):
+---
0 | +--- (base class Vbase0)
0 | | {vfptr}
8 | | ll_data
| +---
16 | i_data
| <alignment member> (size=4)
+---
class Vbase2 size(24):
+---
0 | +--- (base class Vbase0)
0 | | {vfptr}
8 | | ll_data
| +---
16 | i_data
| <alignment member> (size=4)
+---
class Vderiver size(56):
+---
0 | +--- (base class Vbase1)
0 | | +--- (base class Vbase0)
0 | | | {vfptr}
8 | | | ll_data
| | +---
16 | | i_data
| | <alignment member> (size=4)
| +---
24 | +--- (base class Vbase2)
24 | | +--- (base class Vbase0)
24 | | | {vfptr}
32 | | | ll_data
| | +---
40 | | i_data
| | <alignment member> (size=4)
| +---
48 | c_data
| <alignment member> (size=7)
+---
可以看到类Vderiver中的ll_data数据成员有两个,这时后入使用
Vderiver vd;
vd.ll_data = 0;
会编译出错,显示ll_data不明确,因为没有指定ll_data是从哪个基类获得。需要使用如下代码访问ll_data:
Vderiver vd;
vd.Vbase1::ll_data = 0;
vd.Vbase2::ll_data = 0;
回到虚拟继承,我们看到Vbase1相比于没有虚拟继承的Vbase1有两个不同点:1.继承后的数据的顺序:无虚拟继承的数据顺序是先父类数据,然后的子类数据,而有了虚拟继承后先是子类数据成员,然后是父类数据ll_data;2.继承后的父类成员中多了一个vbptr
成员,这导致Vbase1的大小多了8 bytes的数据(其中有4 bytes是对齐调整导致)。
查看完整Vbase1类的分布:
class Vbase1 size(32):
+---
0 | {vfptr}
4 | {vbptr}
8 | i_data
| <alignment member> (size=4)
+---
+--- (virtual base Vbase0)
16 | {vfptr}
24 | ll_data
+---
Vbase1::$vftable@Vbase1@:
| &Vbase1_meta
| 0
0 | &Vbase1::f_base
1 | &Vbase1::f2_base
2 | &Vbase1::{dtor}
Vbase1::$vbtable@:
0 | -4
1 | 12 (Vbase1d(Vbase1+4)Vbase0)
Vbase1::$vftable@Vbase0@:
| -16
0 | &Vbase0::f_base0
Vbase1::f_base this adjustor: 0
Vbase1::f2_base this adjustor: 0
Vbase1::{dtor} this adjustor: 0
Vbase1::__delDtor this adjustor: 0
Vbase1::__vecDelDtor this adjustor: 0
vbi: class offset o.vbptr o.vbte fVtorDisp
Vbase0 16 4 4 0
vbptr 是指向vbtable的一个指针,vbtable存储内容有:1.vbptr相对于本类的偏移;2.vbptr相对于继承而来的虚基类的偏移。
可以看到Vbase1中的vbtable内容中,第0个槽是-4,表示Vbase1类对象数据起始地址 = vbptr地址-4,第一个槽的值为12,表示Vbase0类的对象数据地址 = vbptr地址+12。基于vbptr和vbtable,基类和子类指针赋值会进行相应的转换。
总结
- 类对象中的成员中只有类的非静态成员变量会存储在对象中
- 在对象内变量按顺序存储,并按照对齐的规则进行存储
- 如果存在虚函数,对象内会添加vtptr对象成员
- 如果存在虚拟继承,对象内会添加vbptr对象成员