C语言之玩转结构体2——字节对齐

  • Post author:
  • Post category:其他


在这里插入图片描述



一、前言

大家都知道,不同的数据类型在内存中占的空间大小是不一样的,如char占1个字节,short占两个字节,int占四个字节等。那如果把这些数据类型放在同一个结构体中,结构体的大小是否就刚好等于这些数据类型的大小之和呢?答案是不一定的,因为系统可能会对结构体存储空间进行优化,以提高访问速度,这其中涉及到的知识就是字节对齐。



二、名词解释

1.1、什么是字节对齐?

在现代计算机中,内存空间都是按照字节(byte)划分的。从理论上讲对任何类型的变量的访问可以从任何地址开始,但实际情况是,访问特定类型的变量的时候经常在待定的内存地址访问,这就需要各种类型的数据按照一定规则在空间上排列,而不是顺序地一个接一个地排放,这种所谓的规则就是字节对齐。

1.2、几个名词解释。

自身对齐值:数据类型本身的对齐值,如char类型是1,short类型是2。

指定对齐值:编译器或程序员指定的对齐值,如c-free默认对齐值是8;32位单片机的对齐值是4;可以通过伪指令#pragma pack(n)来改变这个默认值。

#pragma pack(4)//指定4字节对齐
typedef struct _student3_t
{
  char a;
  double b;
  char c;
}student3_t;
#pragma pack()

有效对齐值:自身对齐值和指定对齐值中较小的那个。如double自身对齐值是8,指定对齐值是4,则它的有效对齐值就是4。



三、字节对齐的规则

字节对齐有两个规则:

1、存放成员的起始地址必须是该成员有效对齐值的整数倍。

2、不但结构体的成员有有效对齐值,结构体本身也有有效对齐值,根据规则1算完成员占用空间后,最后要将其补齐为该结构体有效对齐值的整数倍。结构体的有效对齐值是其最大数据成员的自身对齐值。

下面我们来通过一个例子,看下编译器是如何进行字节对齐的。



四、实战练习

我这里以C-free编译器(指定对齐值是8),列举个例子:

//demo1
typedef struct _student_t
{
  int a;   //0x0000-0x0004
  char b;  //0x0004-0x0005
  short c; //0x0006-0x0007
  double d; //0x0008-0x0016
  char e;   //0x0016-0x0017
}student_t;
int main(void)
{
  printf("%d\r\n",sizeof(student_t));
  return 0;  
}

得到运行结果是24,那这个24是怎么来的呢?怎么不是4+1+2+8+1 =16?

下面我们来分析:

我们假定该结构体的首地址为0x0000

a成员的自身对齐值是4,根据指定对齐值是8,算出它的有效对齐值是4(4和8中较小的那个),它的起始地址0x0000是4的整数倍,满足规则1,故它占有4个字节。此时地址用了0x0000-0x0004.

b成员的自身对齐值是1,指定对齐值是8,有效对齐值是1,它的起始地址0x0004是1的整数倍,故它占有1个字节,地址用了0x0004-0x0005.

c成员的自身对齐值是2,指定对齐值是8,有效对齐值是2,它的起始地址是0x0005,不是2的整数倍,因此需要在b成员后填充一个字节,让c成员的起始地址为0x0006,故它占有2个字节,地址用了0x0006-0x0007

以此类推:

d成员的自身对齐值是8,指定对齐值是8,有效对齐值是8,起始地址是0x0008-0x0010

e成员的自身对齐值是1,指定对齐值是8,有效对齐值是1,起始地址是0x0011-0x0012

结构体A的有效对齐值是其最大数据成员的自身对齐值,也就是d成员的8,根据规则2,算下0x0012 = 18不是8的倍数,需要在末尾补齐,也就是补到24个字节即可。

故最后算的该结构体的长度为24.

结构体的成员位置可能会影响到结构体大小。

我们如果将上述结构体成员换个顺序:

typedef struct _student_t
{
  char b;   
  char e;  
  short c;  
  int a;   
  double d; 
}student_t;
int main(void)
{
  printf("%d\r\n",sizeof(student_t));
  return 0;  
}

b成员的自身对齐值是1,指定对齐值是8,有效对齐值是1,起始地址是0x0000-0x0001.

e成员的自身对齐值是1,指定对齐值是8,有效对齐值是1,起始地址是0x0001-0x0002.

c成员的自身对齐值是2,指定对齐值是8,有效对齐值是2,起始地址是0x0002-0x0004.

a成员的自身对齐值是4,指定对齐值是8,有效对齐值是4,起始地址是0x0004-0x0008.

d成员的自身对齐值是8,指定对齐值是8,有效对齐值是8,起始地址是0x0008-0x00010.

按照上述同样的规则,算得结构体长度为16.

我们发现这次编译器没有为结构体填充任何空间,在定义结构体时,我们要尽量达到这种效果。

我们再来看一个内嵌数组的例子

typedef struct _student_t
{
  char a;  //0 - 1 
  char b[8]; //1 - 9
  int c;   //12-16
}student_t;
int main(void)
{
  printf("%d\r\n",sizeof(student_t));
  return 0;
}

运行结果是16

在这里插入图片描述

这里其实b数组,我们可以看成是8个char类型的变量,然后往下面继续算即可。

我们再来看一个内嵌结构体的例子

typedef struct _student1_t
{
  int a;
  short b;
  float d;
}student1_t;

typedef struct _student_t
{
  char a;  
  student1_t student;
  short d;
}student_t;

int main(void)
{
  printf("%d\r\n",sizeof(student_t));
  return 0;
}

算得结构体的大小为20

在这里插入图片描述

这里计算的时候,同样的我们可以把这个内嵌的结构体成员直接搬过来进行计算即可,它大小等同于下面:

typedef struct _student_t
{
  char a;  
  int c;
  short b;
  float e;
  short d;
}student_t;

int main(void)
{
  printf("%d\r\n",sizeof(student_t));
  return 0;
}

最后,我们加上伪指令,改变上述结构体的指定对齐值为2,看看效果:

#pragma pack(2)
typedef struct _student_t
{
  char a;  //0 - 1 
  int c;
  short b;
  float e;
  short d;
}student_t;
#pragma pack()
int main(void)
{
  printf("%d\r\n",sizeof(student_t));
  return 0;
}

运行得到结构体大小是14

在这里插入图片描述

我们看到,结构体的大小由20变成了14,改变指定对齐值影响到了该结构体的大小。

原因就是成员的有效对齐值因为指定对齐值变小而变小了。

根据规则1:存放成员的起始地址必须是该成员有效对齐值的整数倍,这样我们就可以用较少空间来存放该结构体。



五、结语

如您在使用过程中有任何问题,请加QQ群进一步交流。

QQ交流群:906015840 (备注:物联网项目交流)

静晨出品:静之所想,晨之所计

在这里插入图片描述



版权声明:本文为qq_24258593原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。