结构体字节对齐理解与代码分析

博客原文:http://www.cnblogs.com/dolphin0520/archive/2011/09/17/2179466.html

https://blog.csdn.net/sno_guo/article/details/8042332

https://blog.csdn.net/wshini7316/article/details/8537572

1、内存对齐的概念

在用sizeof运算符求算某结构体所占空间时,并不是简单地将结构体中所有元素各自占的空间相加,这里涉及到内存字节对齐的问题。从理论上讲,对于任何变量的访问都可以从任何地址开始访问,但是事实上不是如此,实际上访问特定类型的变量只能在特定的地址访问,这就需要各个变量在空间上按一定的规则排列,而不是简单地顺序排列,这就是内存对齐。

 

2、为什么需要内存对齐

(1)某些平台只能在特定的地址处访问特定类型的数据;

(2)提高存取数据的速度。比如有的平台每次都是从偶地址处读取数据,对于一个int型的变量,若从偶地址单元处存放,则只需一个读取周期即可读取该变量;但是若从奇地址单元处存放,则需要2个读取周期读取该变量。

 

3、变量的自身对齐参数与对齐参数

(1)自身对齐参数

对于每个变量,它自身有对齐参数,这个自身对齐参数在不同编译环境下不同。下面列举的是两种最常见的编译环境下各种类型变量的自身对齐参数

https://pic002.cnblogs.com/images/2012/288799/2012110714421258.jpg

从上面可以发现,在windows(32)/VC6.0下各种类型的变量的自身对齐参数就是该类型变量所占字节数的大小,而在linux(32)/GCC下double类型的变量自身对齐参数是4,是因为linux(32)/GCC下如果该类型变量的长度没有超过CPU的字长,则以该类型变量的长度作为自身对齐参数,如果该类型变量的长度超过CPU字长,则自身对齐参数为CPU字长,而32位系统其CPU字长是4,所以linux(32)/GCC下double类型的变量自身对齐参数是4,如果是在Linux(64)下,则double类型的自身对齐参数是8。

(2)对齐参数

除了变量的自身对齐参数外,还有一个对齐参数,就是每个编译器默认的对齐参数#pragma pack(n),这个值可以通过代码去设定,如果没有设定,则取系统的默认值。在windows(32)/VC6.0下,n的取值可以为1、2、4、8,默认情况下为8。在linux(32)/GCC下,n的取值只能为1、2、4,默认情况下为4。

 

4、结构体字节对齐规则

(1)结构体每个成员相对结构体首地址的偏移量(offset)是对齐参数的整数倍,如有需要会在成员之间填充字节。编译器在为结构体成员开辟空间时,首先检查预开辟空间的地址相对于结构体首地址的偏移量是否为对齐参数的整数倍,若是,则存放该成员;若不是,则填充若干字节,以达到整数倍的要求。

(2)结构体变量所占空间的大小是对齐参数大小的整数倍。如有需要会在最后一个成员末尾填充若干字节使得所占空间大小是对齐参数大小的整数倍。

 

对于第一条原则,每个变量相对于结构体的首地址的偏移量必须是对齐参数的整数倍,这句话中的对齐参数是取每个变量自身对齐参数和系统默认对齐参数#pragma pack(n)中较小的一个。举个简单的例子,比如在结构体A中有变量int a,a的自身对齐参数为4(环境为windows/vc),而VC默认的对齐参数为8,取较小者,则对于a,它相对于结构体A的起始地址的偏移量必须是4的倍数。

 

对于第二条原则,结构体变量所占空间的大小是对齐参数的整数倍。这句话中的对齐参数有点复杂,它是取结构体中所有变量的对齐参数的最大值系统默认对齐参数#pragma pack(n)比较,较小者作为对齐参数。举个例子假如在结构体A中先后定义了两个变量int a;double b;对于变量a,它的自身对齐参数为4,而#pragma pack(n)值默认为8,则a的对齐参数为4;b的自身对齐参数为8,而#pragma pack(n)的默认值为8,则b的对齐参数为8。由于a的最终对齐参数为4,b的最终对齐参数为8,那么两者较大者是8,然后再拿8和#pragma pack(n)作比较,取较小者作为对齐参数,也就是8,即意味着结构体最终的大小必须能被8整除。

 

5、举例分析

typedef struct node1

{

      int a;

      char b;

      short c;

}S1;

S1 ST1;

 

typedef struct node2

{

      char a;

      int b;

      short c;

}S2;

S2 ST2;

int main(void)

{

      printf("sizeof(ST1) = %d.\n", sizeof(S1));

      printf("ST1.a=%#x, ST1.b=%#x, ST1.c=%#x.\n", &ST1.a, &ST1.b, &ST1.c);

 

      printf("sizeof(S2) = %d.\n", sizeof(S2));

      printf("ST2.a=%#x, ST2.b=%#x, ST2.c=%#x.\n", &ST2.a, &ST2.b, &ST2.c);

}

VS2013中运行结果如下:

为什么会出现这样的结果?

首先分析ST2结构体变量

第一步:用第一条原则分析:

  对于变量a,它的自身对齐参数为1,#pragma pack(n)默认值为8,则最终a的对齐参数为1,为其分配1字节的空间,它相对于结构体起始地址的偏移量为0,能被4整除;

  对于变量b,它的自身对齐参数为4,#pragma pack(n)默认值为8,则最终b的对齐参数为4,接下来的地址相对于结构体的起始地址的偏移量为1,1不能够整除4,所以需要在a后面填充3字节使得偏移量达到4,然后再为b分配4字节的空间;

  对于变量c,它的自身对齐参数为2,#pragma pack(n)默认值为8,则最终c的对齐参数为2,而接下来的地址相对于结构体的起始地址的偏移量为8,能整除2,所以直接为c分配2字节的空间。

此时结构体所占的字节数为1+3+4+2=10字节

第二步:用第二条原则分析

  最后由于a,b,c的最终对齐参数分别为1,4,2,最大为4,#pragma pack(n)的默认值为8,则结构体变量最后的大小必须能被4整除。而10不能够整除4,所以需要在后面填充2字节达到12字节。其存储如下:

   |char|----|----|----|  4字节

    |--------int--------|  4字节

    |--short--|----|----|  4字节

  总共占12个字节

再分析ST1结构体变量

第一步:用第一条原则分析:

  对于变量a,它的自身对齐参数为4,#pragma pack(n)默认值为8,则最终a的对齐参数为4,为其分配4字节的空间,它相对于结构体起始地址的偏移量为0,能被4整除;

  对于变量b,它的自身对齐参数为1,#pragma pack(n)默认值为8,则最终b的对齐参数为1,接下来的地址相对于结构体的起始地址的偏移量为4,4能够整除4;

  对于变量c,它的自身对齐参数为2,#pragma pack(n)默认值为8,则最终c的对齐参数为2,而接下来的地址相对于结构体的起始地址的偏移量为1,不能整除2,所以需要在b后面填充1字节使得偏移量达到2,然后再为b分配2字节的空间;

此时结构体所占的字节数为4+1+1+2=8字节

第二步:用第二条原则分析

  最后由于a,b,c的最终对齐参数分别为1,4,2,最大为4,#pragma pack(n)的默认值为8,则结构体变量最后的大小必须能被4整除。而8能够整除4,所以不需要在后面填充字节。其存储如下:

|--------int--------|  4字节

|char|----|--short--|  4字节

  总共占8个字节

 

再看一个复杂一点的例子:

typedef struct node1

{

    int a;

    char b;

    short c;

}S1;

 

typedef struct node2

{

    bool a;

    S1 s1;

    double b;

    int c;

}S2;

sizeof(S2)=32

对于变量a,其自身对齐参数为1,#pragma pack(n)为8,则a的最终对齐参数为1,为它分配1字节的空间,它相对于结构体起始地址的偏移量为0,能被1整除;

  对于s1,它的自身对齐参数为4(对于结构体变量,它的自身对齐参数为它里面各个变量最终对齐参数的最大值),#pragma pack(n)为8,所以s1的最终对齐参数为4,接下来的地址相对于结构体起始地址的偏移量为1,不能被4整除,所以需要在a后面填充3字节达到4,为其分配8字节的空间;

  对于变量b,它的自身对齐参数为8,#pragma pack(n)的默认值为8,则b的最终对齐参数为8,接下来的地址相对于结构体起始地址的偏移量为12,不能被8整除,所以需要在s1后面填充4字节达到16,再为b分配8字节的空间;

  对于变量c,它的自身对齐参数为4,#pragma pack(n)的默认值为8,则c的最终对齐参数为4,接下来相对于结构体其实地址的偏移量为24,能够被4整除,所以直接为c分配4字节的空间。

  此时结构体所占字节数为1+3+8+4+8+4=28字节。

  对于整个结构体来说,各个变量的最终对齐参数为1,4,8,4,最大值为8,#pragma pack(n)默认值为8,所以最终结构体的大小必须是8的倍数,因此需要在最后面填充4字节达到32字节。其存储如下:

   |--------bool--------|    4字节

   |---------s1---------|    8字节

   |--------------------|     4字节

   |--------double------|    8字节

   |----int----|---------|     8字节

  另外可以显示地在程序中使用#pragma pack(n)来设置系统默认的对齐参数,在显示设置之后,则以设置的值作为标准,其它的和上面所讲的类似,就不再赘述了,读者可以自行上机试验一下。如果需要取消设置,可以用#pragma pack()来取消。

 

6、在GNU中设置对齐

我们可以用#pragma pack(n)来设置对齐参数

用#pragma pack();来取消设置对齐参数

#pragma pack的方式在很多C环境下都是支持的,但是gcc虽然支持,但是不推荐使用。

在GCC中推荐使用对齐指令__attribute__((packed))和__attribute__((alligned(n)))

在GCC中的对齐规则与上面的理解相同,只是通过attribute指令来设置,而不是用#pragma指令设置,下面给出attribute的具体用法实例

#include <stdio.h>
struct A{
  char a;            //1Byte
  int b;            //4B
  unsigned short c;       //2B
  long d;            //4B
  unsigned long long e;     //8B
  char f;            //1B
};

struct B{
  char a;
  int b;
  unsigned short c;
  long d;
  unsigned long long e;
  char f;
}__attribute__((aligned));

struct C{
  char a;
  int b;
  unsigned short c;
  long d;
  unsigned long long e;
  char f;
}__attribute__((aligned(1)));


struct D{
  char a;
  int b;
  unsigned short c;
  long d;
  unsigned long long e;
  char f;
}__attribute__((aligned(4)));

struct E{
  char a;
  int b;
  unsigned short c;
  long d;
  unsigned long long e;
  char f;
}__attribute__((aligned(8)));

struct F{
  char a;
  int b;
  unsigned short c;
  long d;
  unsigned long long e;
  char f;
}__attribute__((packed));

int main(int argc, char **argv){
  printf("A = %d, B = %d, C = %d, D = %d, E = %d, F = %dn",
  sizeof(struct A), sizeof(struct B), sizeof(struct C), sizeof(struct D), sizeof(struct E), sizeof(struct F));
  return 0;
}

在一个 32位机 上运行结果如下:

 

[Copy to clipboard] [ - ]CODE:

[root@Kendo develop]# gcc -o align align.c

[root@Kendo develop]# ./align

A = 28, B = 32, C = 28, D = 28, E = 32, F = 20

[root@Kendo develop]#

 

我们看到

最后一个struct F,1 + 4 + 2 + 4 + 8 + 1 = 20,因为使用了__attribute__((packed));

来表示以最小方式对齐,所以结果刚好为20。

而第一个struct A,因为什么也没有跟,采用默认处理方式:4(1) + 4 + 4(2) + 4 + 8 + 4(1) = 28,括号中是其成员本来的大小。与此相似的是struct D。

接下来看struct E,采用8个字节的方式来对齐:8(1+4+2 ,即a, b, c)+ 8(4, d) + 8 + 8(1, f) = 32。

而在struct C中,试图使用__attribute__((aligned(1))) 来使用1个字节方式的对齐,不过并未如愿,仍然采用了默认4个字节的对齐方式。

在struct B中,aligned没有参数,表示“让编译器根据目标机制采用最大最有益的方式对齐"——当然,最有益应该是运行效率最高吧,呵呵。其结果是与struct E相同。

 

猜你喜欢

转载自blog.csdn.net/weixin_42445727/article/details/84109803
今日推荐