c语言内存对齐与#pragma pack(n)

时间:2023-01-31 12:51:13

一、什么是内存对齐,为什么要内存对齐 

    • 现在计算机内存空间都是按照byte字节划分的,理论上讲对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特定的内存地址*问,这就需要各种数据类型按照一定的规则在空间上排列,而不是一个接一个的排放,这就是内存对齐。
    • cpu对内存的读取不是连续的而是分块读取的,块的大小只能是2i个字节数,从cpu的读取性能和效率来考虑,若读取的数据未对齐,则需要两次总线周期来访问内存,因而效率会大打折扣
    • 另外某些固定的硬件平台只能从规定的相对地址处读取特定类型的数据,否则会产生硬件异常。
    • 如果不按照适合平台要求对数据存放进行对齐,会存在效率上的损失。比如有些平台每次读都是从偶地址开始,如果一个int型(32位系统)存放在偶地址开始的地方,那么一个周期就可以读出这32bit,而如果存放在奇地址开始的地方,就需要两个读周期,并对两次读出结果的高低字节进行拼凑才能得到该32bit数据。显然在读取效率上下降很多。
      • c语言内存对齐与#pragma pack(n)
  • 上图说明的是cpu如何与内存进行数据交换的模型,左边是cpu,右边是内存空间,内存上边的0~3是内存地址。这张图以32位cpu作为代表。
  • 32位cpu是以双字为单位进行数据传输的,正因为这个原因,如果我们的数据只有8位或16位,cpu是不是就会以我们数据的位数进行传输呢,答案是否定的,这样会使的cpu硬件变得复杂,所以32位cpu传输数据无论是8位或是16位都是以双字进行传输的。
  • 比如一个int类型的4字节数据如果放在上图内存地址1开始的地方,那么这个数据占用的内存地址为1~4,那么这个数据就被分成了2部分,一部分在0~3地址上,一部分在4~7地址上。又因为32位cpu以双字传输,所以cpu会分两次进行读取,一次先读取0~3地址上的内容,再一次读取4~7地址上的数据。最后cpu提取并组合出正确的int类型数据。舍弃掉无关数据。
  • 如果把这个int类型数据存放在从地址0开始的地方,cpu只需要一次读取就可以得到这个int数据类型了。这次cpu只用了一个周期就得到了数据,可见内存摆放很重要,合理的内存对齐可以减少cpu的使用资源

 

 

二、内存对齐规则

  • 在不用#pagrama pack()包裹的情况下,结构体或联合体按照编译器默认的对齐方式有以下三个对其原则:
    • 数据成员对齐原则:结构(struct或union)的数据成员,第一个数据成员存放在offset为0的地方,以后每个数据成员存储的起始位置都要从该成员占用内存大小的整数倍开始
    • 结构体作为成员的原则:如果一个结构中有某些结构体成员,则结构的成员要从其内部最大元素大小的整数倍地址开始存储。(struct a里有struct b,b里有char,int,double等元素,那b应该从8的整数倍开始)
    • 结构(或联合)的整体对齐原则:在数据成员各自对齐后,结构(或联合)本身也要进行对齐,即以结构体内部占用内存空间最大的数据类型进行对齐。(等同于sizeof该结构体的结果必须是其内部最大成员占用内存的整数倍)
      • 1 struct mystruct
        2 {
        3 char a;//偏移量为0;a占用一个字节
        4 double b;//下一个可用地址偏移量为1,不是sizeof(double)=8的整数倍,需要补7个字节
        5 int c;//下一个可用地址偏移量为1+7+8=16,是sizeof(int)=4的整数倍,满足Int的对齐方式
        6 }//所有成员变量都分配了空间,空间大小=1+7+8+4=20,不是最大空间类型double的整数倍,所以需要填充4个字节以满足结构体大小为sizeof(double)=8的整数倍
        7 sizeof(mystruct)=24;

         

      • 对于结构体整体对齐:

      •             c语言内存对齐与#pragma pack(n) 

 三、#pragma pack()自定义数据对齐规则

  • #pragma pack(n):每个特定平台上的编译器都有自己默认的对齐系数,程序员可以通过预编译指令#pragma pack(n),n=1,2,4,8,16来改变这一系数
    • 数据成员对齐规则:结构或联合的数据成员,第一个数据成员在offset为0的地方,以后每个数据成员的对齐方式都按照#pragma pack指定的数值这个数据成员自身占用内存中比较小的那个进行。
    • 结构或联合整体对其原则:在数据成员完成自身对齐后,结构或联合本身也要进行对齐,对齐按照#pragma pack(n)指定的数值和结构或联合最大数据成员占用内存中比较小的那个进行。
      •  1 # pragma pack (4)
         2 struct node
         3 {
         4 int e;
         5 char f;
         6 short int a;
         7 char b;
         8 }
         9 struct node n;
        10 printf("%d",sizeof(n));
        11 
        12 32位系统结果为:12
        13 4+1+1+2+1+3=12包括数据成员对齐与结构整体对齐;
  • #pragma pack(push),#pragma pack(pop),#pragmapack()
    • d当有些时候想用4字节对齐,有些时候又想用1字节或8字节对齐时,便用到了push和pop
    • #pragma pack()能够取消自定义的对齐方式,恢复为默认的对齐方式
    • #pragma pack(push):压栈,编译器编译到此处时将保存对齐状态(即push指令之前的对齐状态)
      #pragma pack(pop):出栈,编译器编译到此处将恢复push指令前保存的对齐状态,在使用#pragma pack(pop)之前需要使用#pragma pack(push)指令
      push和pop指令是一对应该同时出现的名词,只有pop没有push不起作用,只有push没有pop则可以保存之前的对齐状态,但也是没必要了
      • 当我们想让一个结构体按照4字节对齐时,可以使用#pragma pack(4),最后又想使用 默认对齐方式时,可以使用#pragma pack();
      • 也可以使用:
        • 1 #pragma pack(push)
          2 #pragma pack(4)
          3 struct......
          4 #pragma pack(pop)
          5 这样在push和pop之间的结构体就可以按照pack指定的字节对齐了,而之后的结构体则按照#pragma pack (push)之前的对齐方式对齐
        • 需要注意的是:#pragma pack()是取消自定义对齐方式,恢复默认方式,而push之后pop是返回到push指令之前的对齐方式

 

    • 举个例子:
      •  1 #include <stdio.h>
         2 #pragma   pack(2) 
         3 #pragma pack(push) 
         4 #pragma   pack(4) 
         5 struct CC {
         6     double d;
         7     char b;
         8     int a;
         9     short c;
        10 };
        11  
        12 #pragma   pack(1) 
        13 struct BB{
        14     double d;
        15     char b;
        16     int a;
        17     short c;
        18 };
        19 #pragma pack(pop)
        20 struct AA{
        21     double d;
        22     char b;
        23     int a;
        24     short c;
        25 };
        26 int main(void)
        27 {
        28     
        29     printf("%u\n%u\n%u\n",sizeof(struct CC),sizeof(struct BB),sizeof(struct AA));
        30     return 0;
        31 }
        32 
        33 运行结果为:201516
        34 先按照2字节对齐,然后push保存2字节,然后强制4字节对齐,然后打印cc为20字节,然后强制1字节对齐,打印bb为15字节,然后pop,pop之后会
          让编译器回到Push之前的对齐方式(这里是2字节对齐)打印AA(按照2字节对齐)16字节
    • #pragma pack(push,1)  作用:将原来的对齐方式设置为压栈,并设置新的对齐方式为1个字节对齐方式。