一、什么是内存对齐
- 我们先来讨论一个问题,结构体的大小
struct s1
{
char c1;
int i;
char c2;
};
- 这个结构体的大小是多少呢?我相信你内心一定有一个答案,6字节;按照常理来说两个char型,一个int型,大小就是6字节。但正确答案是12字节。
- 再来看看下面这个结构体的大小
struct s1
{
char c1;
char c2;
int i;
};
- 有的人心理犯嘀咕,这不是一样吗?只是调换了定义顺序,还能影响大小?当然会!这样定义的话,结构体大小是8字节。
- 而这就是内存对齐造成的结果。 在我们学习计算机的时候,可能想当然的认为cpu可以访问内存的任意地方,但事实上cpu只能从内存的特定地方访问有限的数据。
正常情况下应该是这样存储的,但是我们说过,cpu只能从特定的地方访存。现在我们假设cpu只能从4的倍数处访问内存。访问char c1的时候从0(这里的0指的是相对于结构体起始地址的偏移量,后续0地址均指偏移地址)地址处访问;访问int i的时候它只能先访问四个字节然后舍弃第一个字节,在访问第最后一个字节,然后把两个片段拼接起来。这样的话cpu就要访问两次内存,导致效率低下。
而如果我们这样存储,舍弃掉中间三个字节,cpu在访问int i时就可以一次访问完成。
- 总的来说,内存对齐就是舍弃空间赢的时间的做法。
二、如何内存对齐
- 第一个成员在与结构体变量起始地址偏移量为0的地址处
- 其他成员要对齐到某个数字(对齐数)的整数倍的地址处。
- 对齐数 = 编译器默认对齐数与该成员大小的较小值(vs默认8,Linux默认4);如果是聚合数据类型(数组和结构体),对齐数为该聚合类型中成员的最大对齐数。
- 结构体总大小为最大对齐数(每个成员都有一个对齐数)的整数倍。
现在我们再来看一下这个例子
struct s1
{
char c1;
int i;
char c2;
};
在内存中的实际储存情况就是这样子的。这样的话占了9个字节,但是结构体的大小要能整除成员中最大的对齐数。s1中最大的对齐数是4,从9往后可以整除4的就是12了,所以s1结构体的大小为12字节。
再看一下这个例子
struct s1
{
char c1;
int a[3];
char c2;
};
struct s2
{
char c1;
struct s1 A;
char c2;
};
这里s1中,c1在0地址处存储,a[3]对齐数为4,从4号地址开始存,占12个字节。c2对齐数为1,紧挨着a[3],存在16号地址,总共占17个字节,但是结构体大小要能整除自身最大对齐数,所以最终大小为20个字节。
s2中c1还是存在0地址处,第二个元素是结构体,根据定义对齐数为自身成员最大对齐数4,从4号地址开始存,占20个字节。c2存在24号地址,总共占25个字节,保证整除自身最大对齐数,最终大小为28个字节。
三、修改默认对齐数
#pragma pack(4)
struct s1
{
char c1;
int a[3];
char c2;
};
#pragma pack()
struct s2
{
char c1;
struct s1 A;
char c2;
};
#pragma pack()这条语句就是修改默认对齐数的,这里我们先将默认对齐数修改为4,当s1编译后在恢复默认,所以在存储时,s1的默认对齐数是4,s2的默认对齐数是8(vs环境下)。