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 条评论
登录 后参与评论

相关文章

来自专栏进击的君君的前端之路

this_原型链_继承

972
来自专栏菜鸟计划

javascript 基本概念

一、在HTML中使用javascript 1.直接是用<script></script>标签。 2.外部引入 <script type="javascript"...

2533
来自专栏转载gongluck的CSDN博客

c++ 中__declspec 的用法

c++ 中__declspec 的用法 语法说明: __declspec ( extended-decl-modifier-seq ) 扩展修饰符: ...

4517
来自专栏ml

C++知识整理(在此感谢大牛的整理)

这篇文章主要讲解如何在C++中使用cin/cout进行高级的格式化输出操作,包括数字的各种计数法(精度)输出,左或右对齐,大小写等等。通过本文,您可以完全脱离s...

2514
来自专栏性能与架构

快速了解 YAML

什么是 YAML? YAML 是一个数据序列化的标准,适用于所有开发语言,最大的特点是可读性好 YAML 的一个主要应用方向就是编写配置文件,有非常多的系统和框...

3225
来自专栏编程

使用dict和set

Python内置了字典:dict的支持,dict全称dictionary,在其他语言中也称为map,使用键-值(key-value)存储,具有极快的查找速度。-...

18910
来自专栏MyBlog

Effective.Java 读书笔记(11)关于clone方法

说到clone方法,我们来提一提Cloneable这个接口,这个接口是用来作为一种标记某对象允许被clone的一种混合接口,可是不幸运的是,这个接口并没能起到该...

832
来自专栏小文博客

写出这个数——《C语言代码笔记》

1402
来自专栏Python爬虫与数据挖掘

Python正则表达式初识(一)

首先跟大家简单唠叨两句为什么要学习正则表达式,为什么在网络爬虫的时候离不开正则表达式。正则表达式在处理字符串的时候扮演着非常重要的角色,在网络爬虫的时候...

562
来自专栏偏前端工程师的驿站

Java魔法堂:四种引用类型、ReferenceQueue和WeakHashMap

一、前言                               JDK1.2以前只提供一种引用类型——强引用 Object obj = new Objec...

1897

扫码关注云+社区