在开始讨论之前,我们先看两个结构体类型,分别为 struct A和 struct B。
struct A //A数据结构1
{
long 1A; //lA long 数据
char cB; // cB char 数据
short nC; // nC short 数据
};
struct A
{
char cB
long 1A:
short nC;
};
在 32 位的机器上,char、short、long 3 种类型的长度分别为 1、2、4。在Visual Studio2010 上测试 struct A 和 struct B 的存储长度,sizeof(struct A)= 8,sizeof(struct A)= 12。这也许会让你惊讶,char、short、long分别占用1、2、4字节,但是按照不同的顺序组合成一个结构体后,结构体的长度会大于3个长度的总和。这就是本实用经验要重点讨论的内容结构体的内存布局。
结构体元素的布局是结构体定义过程中需要考虑到的。优化结构体元素的布局主要有两个方面的原因:一是节省内存空间;二是提高数据存取速度。
说到节省内存空间和提高数据存取速度,对齐是一个必须讨论的问题。现代计算机中内存空间都是按照 byte 划分的。理论上讲,似乎对任何类型变量的访问都可以从任何地址开始,但实际情况是在访问特定的变量时经常从特定的存储地址开始访问。这就要求各类型数据按照一定的规则在空间上排列,而不是顺序地一个接一个排放。这就是数据的对齐。
然而为什么要进行数据对齐呢?各个硬件平台在存储空间的处理上有很大的不同,一些平台对某些特定类型的数据只能从某些特定地址开始存取,其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台的要求对数据存放进行对齐,就会在存取效率上带来损失。比如有些平台每次读都是从偶地址开始,如果一个int型(假设为32位系统)数据存放在偶地址开始的地方,那么一个读周期就可以读出;而如果存放在奇地址开始的地方,就可能会需要两个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该int数据,显然在读取效率上下降很多。这也是空间和时间的博弈。
对齐的实现方式是什么?通常,我们不需要考虑对齐问题,编译器会默认选择目标平台的对齐策略。当然,我们也可以自己指定数据的对齐方法。
内存对齐
(1) 内存数据对齐,降低数据存取的CPU时钟周期;适当的内存对齐策略,可降低内存使用量。
(2) 内存对齐一般由编译器完成,无特殊需要不需要人为干预。但是,正因为编译器替我们对数据存放做了对齐,但我们并不知道编译器替我们做了这些,所以我们常常会对一些问题感到迷惑,最常见的就是struct 数据结构的
sizeof 结果。为此,我们需要对对齐算法有所了解。对齐通常会影响结构体、联合、类等复合类型数据的存储区域分布。
一般情况下,对齐算法遵循下述4个规则:
● 在复合类型中,各数据成员按照它们的声明顺序在内存中顺序存储,第一个成员放到复合类型的起始位置(相对偏移为0)。
● 每个成员按照自己的对齐方式并最小化长度进行自身的数据存储。
● 复合类型的整体对齐按照类型中长度最大的数据成员和#pragma pack 指定值较小的那个值进行对齐。
● 整个复合类型的长度必须为所采用的对齐参数的整数倍,不够的补空字节。
现在,我们按照上述的对齐准则,再次从理论的高度分析为什么会得到上述运算结果。首先我们分析 struct A。IA 为 long 类型4,采用 4 字节对齐。cB 为 char 型,采用1字节对齐。nC为 short类型,采用2字节对齐。整个 struct采用4字节对齐。所以可以得出如下结论:1A放置于 structA的起始位置,占用4字节;cB排列到1A之后之后占用1字节;nC为 short型2字节对齐,起始地址必须为2的整数倍,所以cB之后必须空闲1字节,其后面可以存储 short类型的nC。所以struct A在内存中的排列如图2-5所示。按照同样的道理,struct B的内存分布如图 2-6 所示。
可以看出,同样的3个数据由于排列顺序不同,导致 struct的占用空间发生了很大的变化。所以在定义 struct 数据类型时,struct中的数据排列顺序是需要重点考虑的。
如果在空间紧张的情况下定义结构体类型时,数据变量的排列顺序应遵守如下原则:一是把结构体中的变量按照类型大小从小到大顺序声明,尽量减少中间的空闲填充字节;二是以空间换取时间,即显式地填补空间进行对齐,例如,有一种使用空间换时间的做法是显式地插入reserved 成员:
struct A
{
char a;
char reserved[3]; //使用空间换时间
int b;
}
提示
(1)reserved对程序没有什么意义,它只是填补空间,以达到字节对齐的目的。
(2)即使不加入reserved成员,通常编译器也会自动填补对齐。而加上它,只是显式的提醒作用。
接下来看下面这段代码片段:
unsigned int i =0x12345678;
unsigned char * p =NULL;
unsigned short *p1 = NULL;
P = &i;
*p = 0x00;
p1 =(unsigned short *)(p+1);
*p1 =0x0000;
最后两句代码从奇数边界访问 unsigned short 型变量,显然不符合对齐规定。在x86上,这种操作只会影响效率,但是在 MIPS 或者 SPARC上,这种操作可能就是一个error,因为它们要求必须字节对齐。
如果你从事的是网络协议栈开发,则可能会为这样的事情而烦恼:在定义协议报头时,由于协议报头的这段顺序是固定的,无法按照类型从小到大的顺序声明,最终导致程序出现异常现象。
例如,在小端CPU格式下,IP协议头定义如下:
//IP头部,总长度20字节
struct IP HDR
{
unsigned char ihl:4; // 首部长度
unsigned char version:4, // 版本
unsigned char tos; // 服务类型
unsigned short tot_len; // 总长度
unsigned short id; // 标志
unsigned short frag off: // 分片偏移
unsigned char ttl; // 生存时间
unsigned char protocol; // 协议
unsigned short chk sum; // 检验和
struct in addr srcaddr; // 源IP地址
struct in addr dstaddr; // 目的IP地址
};
在IP协议头中任何两个逻辑上相邻的字段都必须在内置中相邻,中间不能出现因为对齐而添加的空闲字段。为了达到取消空闲字段的目的,编译器允许用户自己根据需要设置复合类型(结构体、联合、位段)的对齐方式实现。这就是 pragma pack()宏。
它的功能说明如下:
#pragma pack([show]| [push| pop][, identifier], n)
功能说明
● pack 提供数据声明级别的控制,对定义不起作用。
● 调用 pack时不指定参数,n 将被设成默认值。
● 一旦改变数据类型的对齐格式,直接效果就是占用内存的减少,但是性能会下降。
语法说明
● show:可选参数;显示当前 packing alignment 的字节数,以 warning message的形式被显示。
● push:可选参数;将当前指定的 packing alignment 数值进行压栈操作。这里的栈是 the internal compiler stack,同时设置当前的 packing alignment 为 n;如果n 没有指定,则将当前的 packing alignment 数值压栈。
● pop:可选参数;从 internal compiler stack 中删除最顶端的 record;如果没有指定 n,则当前栈顶 record 即为新的 packing alignment 数值;如果指定了 n,则 n将成为新的 packing alignment 数值;如果指定了 identifier,则 internal compilerstack 中的 record 都将被弹出,直到 identifier 被找到,然后弹出,identifier,同时设置 packing alignment 数值为当前栈顶的 record;如果指定的 identifier 并不存在于 internal compiler stack,则弹出,操作被忽略。
● identifier:可选参数;当同 push 一起使用时,赋予当前被压入栈中的 record 一个名称;当同 pop 参数一起使用时,从 internal compiler stack 中弹出所有的record 直到 identifier 被弹出,如果 identifier 没有被找到,则忽略 pop 操作。
● n:可选参数;指定对齐的数值,以字节为单位;默认数值是8,合法的数值分别是1、2、4、8、16。最后,看一下 pragma pack()复合类型内存对齐的影响。对比下面 3 组代码片段,观察同一结构体在不同对齐方式下的长度。
片段一:通过 pragma pack 指定对齐格式为 1。sizeof(struct A)=7。
#pragma pack(1)
struct A
{
char b:
int a;
short c:
};
pragma pack()
片段二:通过 pragma pack 指定对齐格式为 2。sizeof(struct A)=8。
#pragma pack(2)
struct A
{
char b;
int a;
short c;
};
pragma pack()
片段三:通过 pragma pack 指定对齐格式为 4。sizeof(struct A)= 12。
#pragma pack(4)
struct A
{
char b;
int a;
short c;
};
pragma pack0
可以看出,将结构体的对齐方式设置为1,结构体A就不会自动填充空闲字段,结构体A的长度就是各元素所占字节之和7。采用某种对齐方式,整个结构体的总长度就必须能被对齐方式整除。如对齐方式设置为2,结构体总长度为8可被2整除;对齐方式设置为4,结构体总长度为12可被4整除。
请谨记
掌握复合类型中元素的对齐规则,合理调整复合类型中元素的布局,不仅可以节省空间,还可以提高数据存取效率。