一、前言
大家都知道,不同的数据类型在内存中占的空间大小是不一样的,如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 (备注:物联网项目交流)
静晨出品:静之所想,晨之所计