首页
社区
课程
招聘
[原创]由结构体对齐所引发的对C++类对象内存模型的思考
发表于: 2017-11-26 17:01 8903

[原创]由结构体对齐所引发的对C++类对象内存模型的思考

2017-11-26 17:01
8903

来北京昌平十五派学习有一段时间了,总想写点东西,难得今天周日有时间,就写写之前所思考的一些东西,就当是把 C++再复习复习,当然这过程中要感谢这里的老师的帮忙,好了,下面进入正题。
(注:本文的实验环境是在VS201X中进行的)

一般而言,结构体变量内存中成员的排布如下:(从第一个成员依次向下排)
图片描述
结构体数据的地址开始于第一个声明的成员的地址,结束于最后一个成员的地址。在他们中间,按照声明的顺序存储着所有的数据成员,但真相远非如此,还存在着内存对齐的问题。
对于类似于下面这样的结构体:

其输出结果为:24
有些刚学的童鞋可能会问,char占一个字节,double占八个字节,8+1+1不是等于10么,难道电脑出问题了???好,为了一探究竟,下面我们查看其内存:
图片描述
0x0036FAC8是obj的起始地址,也就是char类型的成员a的地址,但是比较奇怪的是,在a后面并没有仅接着就存储b,而是空了7个字节之后再存储b,b占了8个字节,之后c占据了一个字节之后(在内存中已经显示出b),整个结构体并未结束,在其后又空置了7个字节,故整个结构体占24个字节.这对于不了解内存对齐规则的人来说是很困惑的.

通过查阅相关文献,内存对齐规则主要为以下三点:

下面结合之前的那个代码对以上规则进行解释:
第一个成员a为字符型,占一个字节,存储的位置是0x0036FAC8,存储之后,第二个成员b为双精度浮点型,占8个字节,同时编译器默认规定的对齐数为8,这两个数取较小值,还是8,所以它存储的位置偏移应该为8的整数倍,刚好0x0036FAC8-0x0036FAD0为8,因此在0x0036FAD0这个地方存储b占用8个字节,之后准备开始存储c,c是字符型,占据一个字节,系统默认对齐数为8,二者取其小,它存储的地址应该是1的整数倍,很明显,任何偏移都是1的整数倍,直接存在了b的后面。到这里本该结束了,但是还有规则3,即在进行b的存储的时候选出来的某个数是8,在存储c的时候选出来的某个数是1,在8和1中选出一个最大值,结构体的大小应该为8的最小整数倍,现在的结构体大小为8+8+1也就是17,最小整数倍只能是24了。所以在c的后面又空置了7个字节。促成了结构体的24个字节的大小。
还有两点要说明:
1 以上提到了一个编译器默认的对齐数,这个数是可以更改的。使用#pragma pack (1//2//4//8//16)来更改,只能改为1,2,4,8,16中的一个值。
2 在网络传输,使用结构体指针指向某一个数据区获取数据,读取文件等时候,经常由于结构体的对齐问题而出错,是一个值得注意的问题。

希望通过以下的例子能够再次熟悉结构体的对齐:

输出结果:24

输出结果:16
不知各位读者注意到没,结构体中不同类型的数据排列不同,得到的结构体大小也将不同,就是由于内存对齐所造成的。

从一段简单的代码看起:

输出结果为24;
我们把上一篇的例子中struct改成class关键字,再在class中添加pubilc属性,保证在外部能访问数据,又添加了两个静态成员和一个成员函数。显示结果与结构体是一模一样的。
由此可以验证:

其输出结果为:
图片描述
我们发现结果出乎我们的意料,sizeof(基类)+sizeof(测试类)与sizeof(派生类)居然不相等!那是为什么呢?
下面我们来分析这个结果:
首先看一看派生类对象,其内存模型图如下:
图片描述
可以看出前三个值是1,2,3,也就是在基类构造函数中初始化的数据成员,可以验证,在派生类对象中,开始存放的是基类的数据成员。前3个int型成员占据12个字节后,之后是double型,依据之前的内存对齐规则,我们可以推测,成员d所在位置偏移需要为8的整数倍,故而再隔了4个字节之后开始存放d,而上图正好验证了我们的推测!随后存放成员e,此时基类大小已经为25了,但是基类总大小应该为8的整数倍(参见上面的内存规则),所以后面又空置了7个字节(用cc来填充),随后存储的是派生类中的数据成员,仅有一个int型,总大小是36,但是真相并非如此,后面又补了4个字节,那是因为整个派生类对象的对齐同时考虑基类和派生类,故而大小为8的整数倍.
我们单看基类和测试类,它们大小分别为32字节和4字节,它们之间是没有继承关系的,有了继承关系组合到一起后的派生类竟然超过没有继承关系时两个类大小的总和!有点意思.既然基类和派生类会"扩充"变大,那么它们会融合么??
下面看代码:

输出结果:
图片描述

派生类内存模型如下:
图片描述
可以看出,临近的两个char类型数据并没有融合到一起,基类与子类泾渭分明。并且又由于整体对齐的原因,又是继承关系的子类大小大于基类与测试类大小的总和。
下面,我们再看一段代码 :

输出结果:
图片描述
查看派生类对象内存模型:
图片描述
可以看出,父类与子类,依然泾渭分明,派生类部分被挤榨的仅为12个字节,整体上来说32字节还是保证了为8的倍数。此时就出现了让我们大跌眼镜的一步,具有继承关系的派生类的大小竟然小于父类与测试类大小的和。
通过之前的分析,我们可以得出具有继承关系的派生类的大小既可以大于也可以小于测试类大小的和,那么可以等于么?答案是显而易见的。
下面我们看代码:

输出结果:
图片描述
至于这里为什么是等于的,我想通过之前的分析,大家应该明白其中的缘由,这里就不多说了.
总结:

这段代码中,公有继承的子类对象或者指针是可以直接赋值给父类的。Inherit2 是私有继承自Base类的,当派生类是私有或者保护继承于基类时,是不能直接把派生类的地址赋值给基类的,需要强制转换。
一般我们使用的都是指针,当我们把派生类的地址赋值给一个基类的时候,基类就会按照基类的方式去访问这段内存,不过,派生类的开头存储的正是由基类继承来的数据,类对象的内存模型完美支持这一点。另外从面向对象的角度来看,这么做是很合理的,比如动物类和兔子类,很明显兔子是动物,自然能够赋值,但是反过来,我说动物是兔子,这就未免会有问题了,下面探讨一下基类向派生类的转换。
当基类转换为派生类的时候,就按照派生类的方式去访问内存,在基类的内存中这块区域中,应该没有什么问题,但是当越过基类内存的时候,能访问到什么,修改了什么就很难说了。

基类向派生类进行转换需要强制转换,并且也仅限于指针。
总结:

很多时候,一个子类可能有多个父类,比如美人鱼既是人也是鱼,冬虫夏草,可以看视频可以上网的手机,为了增强代码复用能力,就有了多继承,示例代码如下:

代码中,Inherit的对象,就能够使用从两个父类继承下来的所有数据和方法(需要考虑权限问题)。我们来看一下它的内存模型:

图片描述
可以看到,子类对象包含着父类的全部数据,我们再看另外一种情况:

内存模型如下:
图片描述
此时我们可以得出一些简单的结论:

查看狼人类内存模型:
图片描述
我们发现有两份腿的数量,这是因为子类对象会包含全部的父类成员。对于狼来说,自然会包含动物类中的腿的数量。对于人来说,也是如此。对于狼人来说,会同时包含狼类和人类的所有成员。故而腿的数量这个字段,在狼人对象中依然是出现两份,一份在狼中,一份在人中,这是典型的菱形继承问题。

为了解决上面这个问题,产生了一种叫做虚继承的机制:
虚继承是为了解决二义性的问题而产生的语法。用法是在继承之前加上一个virtual,我们来看一下最为简单的情况,下面的例子可以帮助我们理解虚继承:

我们可以看一看输出结果:(结果可能会让你大吃一惊哦)
图片描述
有人可能会问不是应该为8个字节么,怎么会是12呢,那多出来的四个字节究竟是什么?好,下面我们看一看它的内存模型:
图片描述
我们可以看到在整个对象的开头多了一个奇怪的数据,并且神奇的是子类数据位于基类数据的上面,我们来解释它在干什么:
通过查阅相关文献,得知头四个字节实际上是一个地址,即0x01186b30,
我们可以查看一下:
图片描述
刚才的那个地址,我们称之为虚基类表指针,指向的位置存储的是一共有两个元素,分别是两个差值:
1 本类地址与虚基类表指针地址的差
2 虚基类地址与虚基类表指针地址的差

这里我们着重关注第二个,它能够实现这样的事情:基类与派生类可以不挨在一起,是通过虚基类表中的差值,从派生类就可以找到基类的数据。
我们直接看复杂一些的情况,结合上面的例子更加容易理解一些:

输出结果:
图片描述
这个结果估计大多数人都没有猜到,呵呵
我们可以来看一下它的内存模型:
图片描述
可以看出:
从上到下的顺序是A,B,派生类,基类Base。Base类被甩到了最后,并且只有一个。Inherit_A与Inherit_B共用一个虚基类。
这个机制,无论是几个中间内一层的类,都能保证虚基类的数据只有一份,这就是虚继承解决多继承中二义性的问题:
小结一下:

 
 
 
struct Test {
    char a;
    double b;
    char c;
};
int _tmain(int argc, _TCHAR* argv[])
{
    Test  obj;
    obj.a = 'a';
    obj.b = 1564654.325;
    obj.c = 'b';
    cout << sizeof(Test);
}
struct Test {
    char a;
    double b;
    int  n;
};

int main()
{
    Test obj;
    obj.a = 'c';
    obj.b = 889089;
    obj.n = 1234;
    cout << sizeof(Test);

    return 0;
}
struct Test {
    char a;
    int  n;
    double b;

};

int main()
{
    Test obj;
    obj.a = 'c';
    obj.b = 889089;
    obj.n = 1234;
    cout << sizeof(Test);

    return 0;
}
class Test {
public:
    char a;
    double b;
    char c;
    static int d;
    static int e;
    void fun()
    {
        printf("Hello world!");
    }
};

int main()
{

    Test Teobj;
    Teobj.a = 'a';
    Teobj.b = 1564654.325;
    Teobj.c = 'b';
    cout << sizeof(Test);

    return 0;
}
class  Base
{
public:
    Base() :a(0x1), b(0x2), c(0x3),d('d'), e('e') {}
    int a;
    int b;
    int c;
    double d;
    char e;
};
class  Inherit1 :public Base
{
public:
    Inherit1() :m_Inherit1a(0xF) {}
    int m_Inherit1a;
};
class  CTest
{
public:
    CTest() :m_Inherit1a(0xF) {}
    int m_Inherit1a;
};
int  main() {
    Base     obj1;
    CTest    obj2;
    Inherit1 obj3;
    cout << "基类大小  :" << sizeof(obj1) << endl;
    cout << "派生类大小  :" << sizeof(obj3) << endl;
    cout << "测试类大小:" << sizeof(obj2) << endl;
    return 0;
}
class  Base
{
public:
    Base() :a(0x1), b(0x2) {}
    int a;
    char b;
};
class  Inherit1 :public Base
{
public:
    Inherit1() :m_Inherit1a(0xF) {}
    char m_Inherit1a;
};
class CTest
{
public:
    CTest() :m_Inherit1a(0xF) {}
    char m_Inherit1a;
};


int main()
{

    Base     obj1;
    CTest    obj2;
    Inherit1 obj3;
    cout << "基类大小  :" << sizeof(obj1) << endl;
    cout << "测试类大小:" << sizeof(obj2) << endl;
    cout << "派生类大小  :" << sizeof(obj3) << endl;

    return 0;
}

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

收藏
免费 2
支持
分享
最新回复 (8)
雪    币: 6818
活跃值: (153)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
不错!!
2017-11-26 19:20
0
雪    币: 5676
活跃值: (1303)
能力值: ( LV17,RANK:1185 )
在线值:
发帖
回帖
粉丝
3
C++的类的成员顺序,如果我没记错的话,编译器开了优化会自动从大到小排列的
2017-11-26 19:44
0
雪    币: 416
活跃值: (162)
能力值: ( LV7,RANK:100 )
在线值:
发帖
回帖
粉丝
4
确实如此
2017-11-26 20:28
0
雪    币: 416
活跃值: (162)
能力值: ( LV7,RANK:100 )
在线值:
发帖
回帖
粉丝
5
holing C++的类的成员顺序,如果我没记错的话,编译器开了优化会自动从大到小排列的
你说的没错,现在的C++程序编译过程,很多东西,不是我们所能决定的,编译器有时候也有优化的过程。
 
2017-11-26 20:31
0
雪    币: 2046
活跃值: (265)
能力值: ( LV7,RANK:104 )
在线值:
发帖
回帖
粉丝
6
很清晰,之前学习的时候并没有过多的关注,现在学习了,谢谢楼主!
2017-11-27 04:21
0
雪    币: 283
活跃值: (48)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
本来觉得还不错,但是看到评论说编译器会优化,顿时感觉就不那么贴近现实了。白费功夫
2017-11-28 22:24
0
雪    币: 7016
活跃值: (4227)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
感谢分享
2017-12-4 15:17
0
雪    币: 68
活跃值: (101)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
9
对结构体的起始地址要求对齐吗?比如 0x0036FAC8 这个地址。
最后于 2019-5-12 07:44 被sodarkbit编辑 ,原因:
2019-5-10 19:40
0
游客
登录 | 注册 方可回帖
返回
//