C++对象模型-1

前言


在C++中经常出现的问题就是求某个对象的大小,包括各种场景如空类,含虚函数的类,不含虚函数的类等等,与之相关的就是C++最重要的特性多态的实现方式——虚函数的内部实现机制。这些问题都涉及到C++的对象模型内存布局,下面就从计算对象空间大小问题引出对象内存的基本布局和对象模型的分类。并在接下来的各篇文章中依次介绍各个继承体系下对象内存布局情况大小,如未特殊说明这些实例均是在G++9.4.0, Gcc9.4.0, Ubuntu20.04, 64bit机器环境下测试结果

关于计算C++对象空间大小


一个空类的对象大小


如下面空类,求该类大小

1
2
3
class EmptyClass
{
};

测试:

1
2
3
4
5
void EmptyClassTest(void)
{
EmptyClass e;
std::cout << "Empty class size is: " << sizeof(e) << std::endl;
}

运行结果:

Empty class size is: 1

一个类什么都没有,怎么还有一个Byte的空间呢?因为空类需要一个占位符,当类实例化对象时候,不同的对象有不同的地址从而区分不同的对象,如:

1
2
EmptyClass ee[3];
std::cout << "ee[0]:" << &ee[0] << " ee[1]:" << &ee[1] << " ee[2]:" << &ee[2] << std::endl;

运行结果:

ee[0]:0x7ffe6f7127b5 ee[1]:0x7ffe6f7127b6 ee[2]:0x7ffe6f7127b7

一个正常类的对象大小


求如下类对象的大小:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class __attribute__ ((packed, aligned(1))) FullClass
{
public:
FullClass()
{}
public:
void Func1(void) // 普通成员函数
{}
static void Func2(void) // 静态成员函数
{}
virtual void Func3(void) // 虚成员函数
{}
private:
int _var1; // 普通成员变量
static int _var2; // 静态成员变量
};

测试:

1
2
3
4
5
void FullClassTest(void)
{
FullClass f;
std::cout << "Full class size is: " << sizeof(f) << std::endl;
}

运行结果:

    Full class size is: 12

可以看到对象的大小为12,通过Gcc查看内存布局的-fdump-lang-class选项命令:

    g++ -fdump-lang-class insideCpp.cpp

-fdump-lang-class命令查看Gcc内存布局的使用方式: g++ -fdump-lang-class [filename], 输出为.class后缀的文件,里面包含该文件内所有类的内存布局信息
-fdump-record-layouts命令查看Clang内存布局的使用方式: clang -cc1 -fdump-record-layouts [filename]
cl命令查看MSVC内存布局的使用方式: cl [filename] -d1reportSingleClassLayout[classname]

可以看到FullClass的Gcc下的内存布局为:

Vtable for FullClass
FullClass::_ZTV9FullClass: 3 entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI9FullClass)
16    (int (*)(...))FullClass::Func3

Class FullClass
   size=12 align=1
   base size=12 base align=1
FullClass (0x0x7f114ac677e0) 0
    vptr=((& FullClass::_ZTV9FullClass) + 16)

即只有一个虚函数表的指针变量vfptr(指针类型(64位)占8 Bytes)和一个普通的成员变量_var1(int占4 Bytes),其余的函数和变量不在对象的内存布局范围内
使用__attribute__ ((packed, aligned(1)))Gcc属性强制让其1字节对齐而不是默认的8字节对齐,从而结果为12 Bytes
静态数据成员、静态成员函数和普通成员函数均不占对象内存空间

考虑字节对齐


在上述FullClass类中去掉__attribute__ ((packed, aligned(1)))属性,使用默认的字节对齐方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class FullClass
{
public:
FullClass()
{}
public:
void Func1(void) // 普通成员函数
{}
static void Func2(void) // 静态成员函数
{}
virtual void Func3(void) // 虚成员函数
{}
private:
int _var1; // 普通成员变量
static int _var2; // 静态成员变量
};

再次查看该类对象的大小:

Full class size is: 16

此时查看对象大小不是8 + 4 = 12,而是8 + 8 = 16,因为默认有8字节对齐(64位按照最大指针大小对齐,数据宽度64位,使总线的运输效率最大化)处理,增加了padding,通过Gcc -fdump-lang-class选项命令的结果:

Vtable for FullClass
FullClass::_ZTV9FullClass: 3 entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI9FullClass)
16    (int (*)(...))FullClass::Func3

Class FullClass
   size=16 align=8
   base size=12 base align=8
FullClass (0x0x7f0a23e1f7e0) 0
    vptr=((& FullClass::_ZTV9FullClass) + 16)

由于最大字节宽度的变量为vfptr虚表指针为8 Bytes所以align为8,虚表大小加上成员变量总共8+4=16字节,通过8Bytes align最终大小为16 Bytes

对象大小总结


从上面的例子可以看出影响一个类对象的内存大小主要包括以下几个方面:

  1. 其非静态数据成员的总和大小
  2. 由于字节对齐而填补的空间大小
  3. 为了支持virtual而由内部产生的额外空间(包括虚函数表指针vfptr和虚拟继承表指针vbptr)

对象模型种类


在C++中由两种类数据成员:static、nostatic;三种类成员函数:static、nostatic、virtual

由于static不能修饰virtual,因为static属于类的而不是某个对象的,不存在this指针,而虚函数实现机制虚函数表需要this指针。所以此处的nostatic成员函数是指普通成员函数

对于类中各种成员的布局,有以下三种模式:

简单对象模型


对象由一系列的slots组成,每个slot指向一个成员,成员按照声明的次序依次被指定一个slot,每个数据成员和成员函数都有自己的slot(包括静态的或非静态的)
对于FullClass类,若用简单对象模型,则如下图所示:

simple_object_model

简单对象模型中只存放各个成员的指针,可以解决“不同类型成员有不同大小的存储空间问题”,所以大小是指针大小与成员个数相乘,各个成员可以通过slot索引来寻址

表格驱动模型(双表格模型)


表格驱动模型中只存放两个表指针:成员数据表和成员函数表,成员函数表内各个slot指向一个函数地址,成员数据表内各个slot存放具体成员变量数据
对于FullClass类,若用表格驱动模型,则如下图所示:

table_driven_model

C++对象模型


C++对象模型对上面两种模型在时间和空间上做了平衡和优化,对象模型内只存放虚函数表指针和具体的成员变量数据(和虚基类指针,若存在的话),静态成员和普通成员函数在对象模型之外
对于FullClass类,若用C++模型,则如下图所示:

cplusplus_object_model

三种对象模型总结


对象模型 计算大小 存储空间大小 数据成员存取效率 是否包含静态成员或普通成员函数
简单对象模型 指针大小与所有成员个数相乘,较大 较大 数据成员两级寻址
表格驱动模型 2个表指针大小,较小 较小 数据成员两级寻址
C++对象模型 虚表指针(加上虚基表指针,若有)大小与真实数据成员大小之和,适中 适中 数据成员一级寻址

从表格中可以看出C++对象模型优点在于空间和存取时间的综合效率上,且为了实现多态在虚函数表的第一个位置上面增加了RTTI运行时类型决定信息

总结


本篇主要介绍了C++类对象大小的计算,并引出了对象模型内部的基本内存布局和三种对象模型,下一篇将继续介绍在各种继承情况下,继承类的内存布局变化情况