1、什么是内存对齐?

元素是按照定义顺序一个一个放到内存中去的,但并不是紧密排列的。从结构体存储的首地址开始,每个元素放置到内存中时,它都会认为内存是按照自己的大小(通常它为4或8)来划分的,因此元素放置的位置一定会在自己宽度的整数倍上开始,这就是所谓的内存对齐。

编译器为程序中的每个“数据单元”安排在适当的位置上。C语言允许你干预“内存对齐”。如果你想了解更加底层的秘密,“内存对齐”对你就不应该再模糊了。

下图是结构体在32bit和64bit环境下各基本数据类型所占的字节数:
image.png

2、以一个例子开始了解

我们先看以下列子,对比上图所给的字节数,大家计算一下以下结构体所占的字节数:

  1. struct ZLStruct1{
  2. int x;
  3. char y;
  4. } struct1;
  5. int main(int argc, char * argv[]) {
  6. NSString * appDelegateClassName;
  7. @autoreleasepool {
  8. NSLog(@"%lu",sizeof(struct1));
  9. appDelegateClassName = NSStringFromClass([AppDelegate class]);
  10. }
  11. return UIApplicationMain(argc, argv, nil, appDelegateClassName);
  12. }

以上例子的运行结果为 8,为什么呢?如果按照每个成员所占的字节数来计算的话,结果应该为 4 + 1 = 5才对啊。其实这就是内存对齐所导致的。

3、为什么要内存对齐?

  1. 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
  2. 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
  • 假如没有内存对齐机制,数据可以任意存放,现在一个int变量存放在从地址1开始的联系四个字节地址中,该处理器去取数据时,要先从0地址开始读取第一个4字节块,剔除不想要的字节(0地址),然后从地址4开始读取下一个4字节块,同样剔除不要的数据(5,6,7地址),最后留下的两块数据合并放入寄存器。这需要做很多工作。
  • 现在有了内存对齐的,int类型数据只能存放在按照对齐规则的内存中,比如说0地址开始的内存。那么现在该处理器在取数据时一次性就能将数据读出来了,而且不需要做额外的操作,提高了效率。

4、内存对齐规则

官方解释如下,简直太长不看,我们直接上案例

  1. 基本类型的对齐值就是其sizeof值;
  2. 数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的对齐按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行;
  3. 结构(或联合)的整体对齐规则:在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行;
  1. struct ZLStruct1 {
  2. int i;
  3. char c1;
  4. char c2;
  5. } struct1;
  6. struct ZLStruct2 {
  7. char c1;
  8. int i;
  9. char c2;
  10. } struct2;
  11. struct ZLStruct3 {
  12. char c1;
  13. char c2;
  14. int i;
  15. } struct3;
  16. int main(int argc, char * argv[]) {
  17. NSString * appDelegateClassName;
  18. @autoreleasepool {
  19. NSLog(@"%lu",sizeof(struct1)); // 8
  20. NSLog(@"%lu",sizeof(struct2)); // 12
  21. NSLog(@"%lu",sizeof(struct3)); // 8
  22. appDelegateClassName = NSStringFromClass([AppDelegate class]);
  23. }
  24. return UIApplicationMain(argc, argv, nil, appDelegateClassName);
  25. }

看以上结构体,我们可以看到结构体中最长的数据类型为4个字节,所以有效对齐单位为4字节,下面根据上面所说的规则以第二个结构体来分析其内存布局: 首先使用规则1,对成员变量进行对齐:

  • sizeof(c1) = 1 <= 4(有效对齐位),按照1字节对齐,占用第0单元;
  • sizeof(i) = 4 <= 4(有效对齐位),相对于结构体首地址的偏移要为4的倍数,占用第4,5,6,7单元;
  • sizeof(c2) = 1 <= 4(有效对齐位),相对于结构体首地址的偏移要为1的倍数,占用第8单元;

然后使用规则2,对结构体整体进行对齐:

第二个结构体中变量i占用内存最大占4字节,而有效对齐单位也为4字节,两者较小值就是4字节。因此整体也是按照4字节对齐。由规则1得到struct2占9个字节,此处再按照规则2进行整体的4字节对齐,所以整个结构体占用12个字节。

根据上面的分析,不难得出上面例子三个结构体的内存布局如下:
image.png

5、分析结构体嵌套结构体

接下来我们挑战升级,在一个结构体中嵌套另外一个结构体,分析它所占用的字节数:

  1. struct ZLStruct1 {
  2. int i;
  3. char c1;
  4. char c2;
  5. } struct1;
  6. struct ZLStruct2 {
  7. char c1;
  8. int i;
  9. char c2;
  10. struct ZLStruct1 struct1;
  11. } struct2;
  12. int main(int argc, char * argv[]) {
  13. NSString * appDelegateClassName;
  14. @autoreleasepool {
  15. NSLog(@"%lu",sizeof(struct1)); // 8
  16. NSLog(@"%lu",sizeof(struct2)); //
  17. appDelegateClassName = NSStringFromClass([AppDelegate class]);
  18. }
  19. return UIApplicationMain(argc, argv, nil, appDelegateClassName);
  20. }

根据我们上面所说的内存对齐规则,可以看到struct1的长度为8字节,但struct1中最长的数据类型为sizeof(i) = 4,

  • sizeof(c1) = 1 <= 4(有效对齐位),按照1字节对齐,占用第0单元;
  • sizeof(i) = 4 <= 4(有效对齐位),相对于结构体首地址的偏移要为4的倍数,占用第4,5,6,7单元;
  • sizeof(c2) = 1 <= 4(有效对齐位),相对于结构体首地址的偏移要为1的倍数,占用第8单元;
  • sizeof(struct1) = 8 <= 4(有效对齐位),相对于结构体首地址的偏移要为4的倍数,占用第12,13,14,15,16,17,18,19单元;

因此对齐后,可以得出所占字节数为20的内存分布图:
image.png

6、总结

结构体作为一种复合数据类型,其构成元素既可以是基本数据类型的变量,也可以是一些复合型类型数据。对此,编译器会自动进行成员变量的对齐以提高运算效率。默认情况下,按自然对齐条件分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同,向结构体成员中size最大的成员对齐。

许多实际的计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是某个数k(通常它为4或8)的倍数,而这个k则被称为该数据类型的对齐模数。