c/c++ 内存对齐

话说今天写程序,遇到一个问题:

class TreeNode {
    char c;
    int val;
    TreeNode(){}
    virtual ~TreeNode(){}
};

这个TreeNode类的大小是多少呢?我最开始一位是1+4+4(虚函数表指针),但是实际一输出是12.这是为什么呢?这就涉及到C语言中内存对齐的概念了。

为什么要进行内存对齐呢?

CPU会以它“最舒服的”数据长度来读取内存数据,比如说每次4字节长度的指令准备被读取进CPU处理,就会有两种情况出现:

    1、4个字节起始地址刚好就在CPU读取的地址处,这种情况下,CPU可以一次就把这个指令读出,并执行。

     2、而当4个字节按照如下图所示分布时并没有以4的整数倍起始进行分布,首先CPU会进行读取4的整数倍地址起始进行读取,然后发现这不是一个int想要的数据,所以再往后补充,再次读取才能读到想要的int.

    第二种情况进行了两次内存读取,相较第一种直接取出多了一次操作,咋看一下好像就多一次没什么影响,但是考虑到CPU做大量的数据运算和操作,如果遇到这种情况很多的话,CPU将做出不可忽略的量的“多余操作”,严重影响处理速度。所以从CPU效率的角度,是需要内存对齐的。

那么,内存对齐有哪些原则呢?我总结了一下大致分为三条:
第一条:第一个成员的首地址为0
第二条:每个成员的首地址是自身大小的整数倍,比如int就需要以4的倍数作为起始进行存储。
第三条:最后以结构总体对齐。(VS2017默认是8的整数倍,当然也可以用pragma pack(n)取得n的整数倍)

这个pragma pack(b),表示内存对齐的整数倍的单元大小。比如说n=2就是每两个字节都需要对齐。

    上述的三原则听起来还是比较抽象,那么接下来我们通过一个例子来加深对内存对齐概念的理解,下面是一个结构体,我们动手算出下面结构体一共占用多少内存?假设我们以32位平台并且以4字节对齐方式:

  1. #pragma pack(4)  
  2. typedef struct MemAlign  
  3. {  
  4.     char a[18];  
  5.     double b;     
  6.     char c;  
  7.     int d;    
  8.     short e;      
  9. }MemAlign;  

下图为对齐后结构如下:

c/c++ 内存对齐

我们就以这个图来讲解是如何对齐的:
第一个成员(char a[18]):首先,假设我们把它放到内存开始地址为0的位置,由于第一个成员占18个字节,所以第一个成员占用内存地址范围为0~18。
第二个成员(double b):由于double类型占8字节,又因为8字节大于4字节,所以就以4字节对齐为基准。由于第一个成员结束地址为18,那么地址18并不是4的整数倍,我们需要再加2个字节,也就是从地址20开始摆放第二个成员。
第三个成员(char c):由于char类型占1字节,任意地址是1字节的整数倍,所以我们就直接将其摆放到紧接第二个成员之后即可。
第四个成员(int d):由于int类型占4字节,但是地址29并不是4的整数倍,所以我们需要再加3个字节,也就是从地址32开始摆放这个成员。
第五个成员(short e):由于short类型占2字节,地址36正好是2的整数倍,这样我们就可以直接摆放,无需填充字节,紧跟其后即可。
    这样我们内存对齐就完成了。但是离成功还差那么一步,那是什么呢?对,是对整个结构体补齐,接下来我们就补齐整个结构体。那么,先让我们回顾一下补齐的原则:“以4字节对齐为例,取结构体中最大成员类型倍数,如果超过4字节,都以4字节整数倍为基准对齐。”在这个结构体中最大类型为double类型(占8字节),又由于8字节大于4字 节,所以我们还是以4字节补齐为基准,整个结构体结束地址为38,而地址38并不是4的整数倍,所以我们还需要加额外2个字节来填充结构体,如下图红色的就是补齐出来的空间:

c/c++ 内存对齐

到此为止,我们内存对齐与补齐就完毕了!接下来我们用实验来证明真理,程序如下:

  1. #include <stdio.h>  
  2. #include <memory.h>  
  3.   
  4. // 由于VS2010默认是8字节对齐,我们  
  5. // 通过预编译来通知编译器我们以4字节对齐  
  6. #pragma pack(4)  
  7.   
  8. // 用于测试的结构体  
  9. typedef struct MemAlign  
  10. {  
  11.     char a[18]; // 18 bytes  
  12.     double b;   // 08 bytes   
  13.     char c;     // 01 bytes  
  14.     int d;      // 04 bytes  
  15.     short e;    // 02 bytes  
  16. }MemAlign;  
  17.   
  18. int main()  
  19. {  
  20.     // 定义一个结构体变量  
  21.     MemAlign m;  
  22.     // 定义个以指向结构体指针  
  23.     MemAlign *p = &m;  
  24.     // 依次对各个成员进行填充,这样我们可以  
  25.     // 动态观察内存变化情况  
  26.     memset( &m.a, 0x11, sizeof(m.a) );  
  27.     memset( &m.b, 0x22, sizeof(m.b) );  
  28.     memset( &m.c, 0x33, sizeof(m.c) );  
  29.     memset( &m.d, 0x44, sizeof(m.d) );  
  30.     memset( &m.e, 0x55, sizeof(m.e) );  
  31.     // 由于有补齐原因,所以我们需要对整个  
  32.     // 结构体进行填充,补齐对齐剩下的字节  
  33.     // 以便我们可以观察到变化  
  34.     memset( &m, 0x66, sizeof(m) );  
  35.     // 输出结构体大小  
  36.     printf( "sizeof(MemAlign) = %d", sizeof(m) );  
  37. }  

 

程序运行过程中,查看内存如下:
c/c++ 内存对齐
其中,各种颜色带下划线的代表各个成员变量,蓝色方框的代表为内存对齐时候填补的多余字节,由于这里看不到补齐效果,我们接下来看下图,下图篮框包围的字节就是与上图的交集以外的部分就是补齐所填充的字节。

c/c++ 内存对齐

在最后,我在谈一谈关于补齐的作用,补齐其实就是为了让这个结构体定义的数组变量时候,数组内部,也同样满足内存对齐的要求,为了更好的理解这点,我做了一个跟本例子相对照的图:

c/c++ 内存对齐