sizeof 知多少? (下)

作者:郁旭斌

《sizeof 知多少? (上)》

8. 单继承

接着我们来看一下类型间单继承的情况,看看内存布局会有什么变化:

struct cv1
{
    int m_1;
    virtual ~cv1() {};
};

struct cv2 : public cv1
{
    short m_2;
    virtual ~cv2() {};
};

一般而言,如果基类或者继承类存在虚函数表指针的话,vptr会置于类型的内存布局首部(继承类会复用基类的虚函数表指针),然后放置基类的数据成员,最后放置继承类的数据成员,放置方法仍然遵循之前所讲的对齐和填充规则,所以我们仍然可以套用公式来计算cv2的大小:

首先对齐各个成员:

sizeof’(cv2, 0) = 0

sizeof’(cv2, 1) = ceil(sizeof’(cv2, 0) / alignof(cv2.vptr)) alignof(cv2.vptr) + sizeof(cv2.vptr) = ceil(0 / 4) 4 + 4 = 4

sizeof’(cv2, 2) = ceil(sizeof’(cv2, 1) / alignof(cv2.m_1)) alignof(cv2.m_1) + sizeof(cv2.m_1) = ceil(4 / 4) 4 + 4 = 8 (cv2.m_1来自于基类cv1)

sizeof’(cv2, 3) = ceil(sizeof’(cv2, 2) / alignof(cv2.m_2)) alignof(cv2.m_2) + sizeof(cv2.m_2) = ceil(8 / 2) 2 + 2 = 10

然后做一次整体填充:

maxalignof = max(alignof(cv2.vptr), alignof(cv2.m_1), alignof(cv2.m_2)) = max(4, 4, 2) = 4

sizeof(cv2) = ceil(sizeof’(cv2, 3) / maxalignof) maxalignof = ceil(10 / 4) 4 = 12

cv2的内存布局如下:

图: cv2内存布局

9. 多继承

C++还支持多继承特性,一个类型可以继承于多个基类(假设基类分别为B1, B2, …, Bn),其中每个基类都可能有成员数据及虚函数表,继承类I也必须能够无缝的向下转形为任一基类,其内存布局的一般规则如下:

a. 首先放置B1的虚函数指针(I会复用该虚函数表指针)

b. 接着放置B1的数据成员,并逐个执行内存对齐

c. 接着放置B2的虚函数指针(如果有的话)

d. 接着放置B2的数据成员,并逐个执行内存对齐

e. 对接下来的基类Bi重复c和d两个步骤,直到 Bn

f. 接着放置I自身的数据成员,并逐个执行内存对齐

g. 最后对I整体做一次数据填充

其中,如果B1没有虚函数表,但是后面的Bi有虚函数表,我们就把Bi提前放置(其实就是把之前的基类列表(B1, B2, …, Bi-1, Bi, Bi+1, …, Bn)映射(重排)成了(Bi, B1, B2, …, Bi-1, Bi+1, …, Bn));如果基类都没有虚函数表,但是I自身有虚函数表的话,I的首部则会放置自身的虚函数表指针,否则,I会复用第一个有虚函数表的基类的虚函数表指针.

看一个例子可能更清晰些:

struct b1
{
    int m_1;
    char m_2;
    virtual ~b1() {};
};

struct b2
{
    short m_3;
};

struct I : public b1, public b2
{
    int m_4;
    virtual ~I() {};
};

我们按照之前的公式来计算一下I的大小:

首先对齐各个成员:

sizeof’(I, 0) = 0

sizeof’(I, 1) = ceil(sizeof’(I, 0) / alignof(b1.vptr)) alignof(b1.vptr) + sizeof(b1.vptr) = ceil(0 / 4) 4 + 4 = 4 (b1的虚函数表指针,I会复用该指针)

sizeof’(I, 2) = ceil(sizeof’(I, 1) / alignof(b1.m_1)) alignof(b1.m_1) + sizeof(b1.m_1) = ceil(4 / 4) 4 + 4 = 8

sizeof’(I, 3) = ceil(sizeof’(I, 2) / alignof(b1.m_2)) alignof(b1.m_2) + sizeof(b1.m_2) = ceil(8 / 1) 1 + 1 = 9

sizeof’(I, 4) = ceil(sizeof’(I, 3) / alignof(b2.m_3)) alignof(b2.m_3) + sizeof(b2.m_3) = ceil(9 / 2) 2 + 2 = 12

sizeof’(I, 5) = ceil(sizeof’(I, 4) / alignof(I.m_4)) alignof(I.m_4) + sizeof(I.m_4) = ceil(12 / 4) 4 + 4 = 16

然后做一次整体填充:

maxalignof = max(alignof(b1.vptr), alignof(b1.m_1), alignof(b1.m_2), alignof(b2.m_3), alignof(I.m_4)) = max(4, 4, 1, 2, 4) = 4

sizeof(I) = ceil(sizeof’(I, 5) / maxalignof) maxalignof = ceil(16 / 4) 4 = 16

I的内存布局如下:

图:I内存布局

10. 虚拟继承

虚拟继承偏于复杂,一般也不推荐使用,讨论虚拟继承相关的内存布局实际来看意义不大,仅供有兴趣的朋友参考 :)

一般C++的教科书中都会提一下虚拟继承,并说明一下虚拟继承的目的是为了解决菱形继承导致的重复基类问题,如果我们想要计算虚拟继承类型的内存大小,就必须首先了解一下编译器对于虚拟类型的内存布局方法. 这里首先要说明的一点是,就VC和GCC而言,两者对于虚拟继承类型的内存布局方法是有很大不同的,我们先说下VC的布局方法:

一个类型如果定义了虚拟函数,VC便会为该类型创建虚函数表,同样的,如果定义了虚拟继承,VC便会为该类型创建虚基类表,并在类型实例中添加虚基类表指针(vbptr),一般而言,vbptr会被放置在vptr之后,如果类型没有vptr,则vbptr会被放置于实例首部,另外的,虚拟基类的成员也会被放置在继承类的尾部,而不是像普通继承那样从继承类的头部开始(细节可以参考上面小节).

考虑下面的类型定义:

struct b1
{
    int m_1;
};

struct b2 : public virtual b1
{
    char m_2;
    virtual ~b2() {};
};

struct b3 : public virtual b1
{
    short m_3;
    virtual ~b3() {};
};

struct I : public b2, public b3
{
    int m_4;
    virtual ~I() {};
};

我们还是使用之前的公式来计算一下I的大小:

首先对齐各个成员:

sizeof’(I, 0) = 0

sizeof’(I, 1) = ceil(sizeof’(I, 0) / alignof(b2.vptr)) alignof(b2.vptr) + sizeof(b2.vptr) = ceil(0 / 4) 4 + 4 = 4 (b2的虚函数表指针,I会复用该指针)

sizeof’(I, 2) = ceil(sizeof’(I, 1) / alignof(b2.vbptr)) alignof(b2.vbptr) + sizeof(b2.vbptr) = ceil(4 / 4) 4 + 4 = 8 (b2的虚基类指针,用以索引b1)

sizeof’(I, 3) = ceil(sizeof’(I, 2) / alignof(b2.m_2)) alignof(b2.m_2) + sizeof(b2.m_2) = ceil(8 / 1) 1 + 1 = 9

sizeof’(I, 4) = ceil(sizeof’(I, 3) / alignof(b3.vptr)) alignof(b3.vptr) + sizeof(b3.vptr) = ceil(9 / 4) 4 + 4 = 16 (b3的虚函数表指针)

sizeof’(I, 5) = ceil(sizeof’(I, 4) / alignof(b3.vbptr)) alignof(b3.vbptr) + sizeof(b3.vbptr) = ceil(16 / 4) 4 + 4 = 20 (b3的虚基类指针,用以索引b1)

sizeof’(I, 6) = ceil(sizeof’(I, 5) / alignof(b3.m_3)) alignof(b3.m_3) + sizeof(b3.m_3) = ceil(20 / 2) 2 + 2 = 22

sizeof’(I, 7) = ceil(sizeof’(I, 6) / alignof(I.m_4)) alignof(I.m_4) + sizeof(I.m_4) = ceil(22 / 4) 4 + 4 = 28

sizeof’(I, 8) = ceil(sizeof’(I, 7) / alignof(b1.m_1)) alignof(b1.m_1) + sizeof(b1.m_1) = ceil(28 / 4) 4 + 4 = 32 (b1被放置在了尾部)

然后做一次整体填充:

maxalignof = max(alignof(b2.vptr), alignof(b2.vbptr), alignof(b2.m_2), alignof(b3.vptr), alignof(b3.vbptr), alignof(b3.m_3), alignof(I.m_4), alignof(b1.m_1)) = max(4, 4, 1, 4, 4, 2, 4, 4) = 4

sizeof(I) = ceil(sizeof’(I, 8) / maxalignof) maxalignof = ceil(32 / 4) 4 = 32

I的内存布局如下:

图: I内存布局

而GCC采用了不同的方法来实现虚拟继承机制,之前提到VC会为虚拟继承类型生成虚基类表,并在实例中插入虚基类表指针,GCC同样也会为虚拟继承类型生成虚基类表,但是GCC并不会在实例中插入虚基类表指针,相反,GCC”合并”了虚函数表指针(vptr)和虚基类表指针(vbptr),

或者说GCC只使用了vptr来实现虚函数的重载和虚基类的索引,方法是通过正向索引vptr来定位虚函数(vptr + offset),通过负向索引vptr来定位虚基类(vptr - offset),所以在内存布局上会比VC生成的内存布局小一些,这里我们同样来计算一下GCC为上面的类型I生成的内存布局大小:

首先对齐各个成员:

sizeof’(I, 0) = 0

sizeof’(I, 1) = ceil(sizeof’(I, 0) / alignof(b2.vptr)) alignof(b2.vptr) + sizeof(b2.vptr) = ceil(0 / 4) 4 + 4 = 4 (b2的虚函数表指针,I会复用该指针)

sizeof’(I, 2) = ceil(sizeof’(I, 1) / alignof(b2.m_2)) alignof(b2.m_2) + sizeof(b2.m_2) = ceil(4 / 1) 1 + 1 = 5 (b2不包含vbptr)

sizeof’(I, 3) = ceil(sizeof’(I, 2) / alignof(b3.vptr)) alignof(b3.vptr) + sizeof(b3.vptr) = ceil(5 / 4) 4 + 4 = 12 (b3的虚函数表指针)

sizeof’(I, 4) = ceil(sizeof’(I, 3) / alignof(b3.m_3)) alignof(b3.m_3) + sizeof(b3.m_3) = ceil(12 / 2) 2 + 2 = 14 (b3不包含vbptr)

sizeof’(I, 5) = ceil(sizeof’(I, 4) / alignof(I.m_4)) alignof(I.m_4) + sizeof(I.m_4) = ceil(14 / 4) 4 + 4 = 20

sizeof’(I, 6) = ceil(sizeof’(I, 5) / alignof(b1.m_1)) alignof(b1.m_1) + sizeof(b1.m_1) = ceil(20 / 4) 4 + 4 = 24 (b1被放置在了尾部)

然后做一次整体填充:

maxalignof = max(alignof(b2.vptr), alignof(b2.m_2), alignof(b3.vptr), alignof(b3.m_3), alignof(I.m_4), alignof(b1.m_1)) = max(4, 1, 4, 2, 4, 4) = 4

sizeof(I) = ceil(sizeof’(I, 6) / maxalignof) maxalignof = ceil(24 / 4) 4 = 24

I的内存布局如下:

图: I内存布局

11. 杂项

有个关于sizeof的有趣的点可以再提一下,那就是空类型的内存大小:

struct E
{
};

空类型的大小一般为1,之所以不为0,是为了支持空类型实例的取址,所以我们可以把空类型看做是一种大小为1,对齐值也为1的类型,这样就可以使用之前的公式来计算一些包含空类型的复合结构的内存大小:

struct s7
{
    E m_1;
    int m_2;
    E m_3;
    short m_4;
};

我们来计算一下s7的内存大小:

首先对齐各个成员:

sizeof’(s7, 0) = 0

sizeof’(s7, 1) = ceil(sizeof’(s7, 0) / alignof(s7.m_1)) alignof(s7.m_1) + sizeof(s7.m_1) = ceil(0 / 1) 1 + 1 = 1

sizeof’(s7, 2) = ceil(sizeof’(s7, 1) / alignof(s7.m_2)) alignof(s7.m_2) + sizeof(s7.m_2) = ceil(1 / 4) 4 + 4 = 8

sizeof’(s7, 3) = ceil(sizeof’(s7, 2) / alignof(s7.m_3)) alignof(s7.m_3) + sizeof(s7.m_3) = ceil(8 / 1) 1 + 1 = 9

sizeof’(s7, 4) = ceil(sizeof’(s7, 3) / alignof(s7.m_4)) alignof(s7.m_4) + sizeof(s7.m_4) = ceil(9 / 2) 2 + 2 = 12

然后做一次整体填充:

maxalignof = max(alignof(s7.m_1), alignof(s7.m_2), alignof(s7.m_3), alignof(s7.m_4)) = max(1, 4, 1, 2) = 4

sizeof(s7) = ceil(sizeof’(s7, 4) / maxalignof) maxalignof = ceil(12 / 4) 4 = 12

有个注意点就是如果类型是继承于空类型而不是包含空类型时,编译器往往会把空类型的占位空间(那一个字节)给优化掉,考虑以下定义:

struct s8 : public E
{
    int m_1;
};

按照之前公式的计算,你会得出s8的内存大小为8,但是由于有之前所说的编译器优化(即空基类优化),所以实际上s8的大小一般为4,当然,如果你把此时的空类型看做一种大小为0,对齐值为1的结构的话,仍然可以使用之前的公式计算得出正确答案:

首先对齐各个成员:

sizeof’(s8, 0) = 0

sizeof’(s8, 1) = ceil(sizeof’(s8, 0) / alignof(E)) alignof(E) + sizeof(E) = ceil(0 / 1) 1 + 0 = 0 (注意此处E类型的特殊处理)

sizeof’(s8, 2) = ceil(sizeof’(s8, 1) / alignof(s8.m_1)) alignof(s8.m_1) + sizeof(s8.m_1) = ceil(0 / 4) 4 + 4 = 4

然后做一次整体填充:

maxalignof = max(alignof(E), alignof(s8.m_1)) = max(1, 4) = 4

sizeof(s8) = ceil(sizeof’(s8, 2) / maxalignof) maxalignof = ceil(4 / 4) 4 = 4

原创声明,本文系作者授权云+社区-专栏发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏debugeeker的专栏

《coredump问题原理探究》Linux x86版6.2节C++风格数据结构内存布局之有成员变量的类

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/xuzhina/article/detai...

461
来自专栏Petrichor的专栏

python: map函数

对 sequence 中的 item 依次执行 function(item),将 执行结果 组成一个 List 返回。

702
来自专栏云霄雨霁

Java虚拟机--运行时栈帧结构

1906
来自专栏海天一树

小朋友学经典算法(12):分割字符串

在分割字符串之前,先来了解一些跟字符串相关的变量或函数: (1)size_type:size_type由string类类型和vector类类型定义的类型,用以保...

882
来自专栏淡定的博客

python入门基础语法总结

693
来自专栏C/C++基础

虚调用及其调用的具体形式

虚调用是相对于实调用而言,它的本质是动态联编。在发生函数调用的时候,如果函数的入口地址是在编译阶段静态确定的,就是是实调用。反之,如果函数的入口地址要在运行时通...

681
来自专栏小樱的经验随笔

C++STL中map容器的说明和使用技巧(杂谈)

1、map简介 map是一类关联式容器。它的特点是增加和删除节点对迭代器的影响很小,除了那个操作节点,对其他的节点都没有什么影响。对于迭代器来说,可以修改实值,...

3045
来自专栏我是攻城师

理解Java8的数据类型和运行时数据区域

Java虚拟机包含对对象的显式支持,对象要么是动态分配的类实例,要么是静态数组,对对象的引用我们可以叫做指针或者引用,一个对象可以有多个引用,对象总是通过引用的...

703
来自专栏mwangblog

python元组(Turple)

1295
来自专栏遊俠扎彪

C++中的字符数组、字符串、字符指针的一些笔记

1、sizeof会计算实际内存空间,strlen会计算C风格的字符串的实际字符数(不包括\0)。

17710

扫码关注云+社区