前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >sizeof 知多少? (下)

sizeof 知多少? (下)

原创
作者头像
serena
修改2021-08-03 14:56:10
6010
修改2021-08-03 14:56:10
举报
文章被收录于专栏:社区的朋友们社区的朋友们

作者:郁旭斌

《sizeof 知多少? (上)》

8. 单继承

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

代码语言:javascript
复制
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会复用第一个有虚函数表的基类的虚函数表指针.

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

代码语言:javascript
复制
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会被放置于实例首部,另外的,虚拟基类的成员也会被放置在继承类的尾部,而不是像普通继承那样从继承类的头部开始(细节可以参考上面小节).

考虑下面的类型定义:

代码语言:javascript
复制
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的有趣的点可以再提一下,那就是空类型的内存大小:

代码语言:javascript
复制
struct E
{
};

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

代码语言:javascript
复制
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

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

代码语言:javascript
复制
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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 8. 单继承
  • 9. 多继承
  • 10. 虚拟继承
  • 11. 杂项
相关产品与服务
数据库
云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档