c和c++

关于预处理指令#pragma pack

2017-06-14  本文已影响86人  Umiade
#pragma pack的大致作用即为改变编译器的对齐方式,先从指令和定义上来分析其功能。
部分内容参考http://www.cnblogs.com/King-Gentleman/p/5297355.html 以及MSDN。

简单理解#pragma

作为较为复杂的预处理指令之一,它的作用为更改编译器的编译状态以及为特定的编译器提供特定的编译指示,这些指示是具体针对某一种(或某一些)编译器的,其他编译器可能不知道该指示的含义又或者对该指示有不同的理解,也即是说,#pragma的实现是与具体平台相关的。可以简单将其理解为该预处理指令是开发者和编译器交互的一个工具。

#pragma pack指令说明

由于内存的读取时间远远小于CPU的存储速度,这里用设定数据结构的对齐系数,即牺牲空间来换取时间的思想来提高CPU的存储效率。

这里先说编译器的对齐配置。以vc6为例,vc6中的编译选项有 /Zp[1|2|4|8|16] ,/Zp1表示以1字节边界对齐,相应的,/Zpn表示以n字节边界对齐。n字节边界对齐的意思是说,一个成员的地址必须安排在成员的尺寸的整数倍地址上或者是n的整数倍地址上,取它们中的最小值。也就是:
min ( sizeof ( member ), n)
实际上,1字节边界对齐也就表示了结构成员之间没有空洞。
/Zpn选项是应用于整个工程的,影响所有的参与编译的结构。
要使用这个选项,可以在vc6中打开工程属性页,c/c++页,选择Code Generation分类,在Struct member alignment可以选择。
而如果要专门针对某些结构定义使用对齐选项,可以使用#pragma pack编译指令。指令语法如下: #pragma pack( [ show ] | [ push | pop ] [, identifier ] , n )

指令用法说明:

  1. pack提供数据声明级别的控制,对定义不起作用;
  2. 调用pack时不指定参数,n将被设成默认值;
  3. 一旦改变数据类型的alignment,直接效果就是占用memory的减少,但是performance会下降。

语法具体分析:

  1. show:可选参数;显示当前packing aligment的字节数,以warning message的形式被显示;
  2. push:可选参数;将当前指定的packing alignment数值进行压栈操作,这里的栈是the internal compiler stack,同时设置当前的packing alignment为n;如果n没有指定,则将当前的packing alignment数值压栈;
  3. pop:可选参数;从internal compiler stack中删除最顶端的record;如果没有指定n,则当前栈顶record即为新的packing alignment数值;如果指定了n,则n将成为新的packing aligment数值;如果指定了identifier,则internal compiler stack中的record都将被pop直到identifier被找到,然后pop出identitier,同时设置packing alignment数值为当前栈顶的record;如果指定的identifier并不存在于internal compiler stack,则pop操作被忽略;
  4. identifier:可选参数;当同push一起使用时,赋予当前被压入栈中的record一个名称;当同pop一起使用时,从internal compiler stack中pop出所有的record直到identifier被pop出,如果identifier没有被找到,则忽略pop操作;
  5. n:可选参数;指定packing的数值,以字节为单位;缺省数值是8,合法的数值分别是1、2、4、8、16。

先以#pragma pack(n)的使用举例,该条指令功能与意义和/Zpn选项相同。#pragma pack(4)表示后续结构单元的对齐系数为4(具体规则见下面说明),#pragma pack()表示更改当前对齐系数为默认值(未修改时,工程对齐系数默认为8)。

#pragma pack(4)
    struct test{
        char m1;
        short int m2;
        int m3;
    };
#pragma pack()

    test t;

    printf("%d\n", sizeof(t));

上述代码段执行结果为8,在初识时可能会误作是12,为8的原因在于“结构体中的数据成员,除了第一个是始终放在最开始的地方,其它数据成员的地址必须是它本身大小或对齐参数两者中较小的一个的倍数。”

数据对齐规则

经过上面这个例子,我们再来看对齐的规则。

  1. 复杂类型中各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个类型的地址相同;
  2. 每个成员分别对齐,即每个成员按自己的方式对齐,并最小化长度;规则就是每个成员按其类型的对齐参数(通常是这个类型的大小)和指定对齐参数中较小的一个对齐;
  3. 结构体、联合体或者类的数据成员,第一个放在偏移为0的地方;以后每个数据成员的对齐,按照#pragma pack指定的数值和这个数据成员自身长度两个中比较小的那个进行;也就是说,当#pragma pack指定的值等于或者超过所有数据成员长度的时候,这个指定值的大小将不产生任何效果;
  4. 复杂类型(如结构体)整体的对齐是按照结构体中长度最大的数据成员和#pragma pack指定值之间较小的那个值进行;这样当数据成员为复杂类型(如结构体)时,可以最小化长度;
  5. 复杂类型(如结构体)整体长度的计算必须取所用过的所有对齐参数的整数倍,不够补空字节;也就是取所用过的所有对齐参数中最大的那个值的整数倍,因为对齐参数都是2的n次方;这样在处理数组时可以保证每一项都边界对齐。

另外,在相同的对齐方式下,结构体内部数据定义的顺序不同,结构体整体占据内存空间也不同。举例如下:

struct A {
int a;            // a的自身对齐值为4,偏移地址为0x00~0x03,a的起始地址0x00满足0x00%4=0;
char b;           // b的自身对齐值为1,由于紧跟a之后的地址,即0x04满足0x04%1=0,所以b存放在0x04地址空间
short c;          // c的自身对齐值为2,由于紧跟b之后的地址0x05%2不是0,而0x06%2=0,因此c的存放起始地址为0x06,存放在0x06~0x07空间。
                  // 在b和c之间的0x05地址 则补空字节。
};

结构体A中包含了4字节长度的int一个,1字节长度的char一个和2字节长度的short型数据一个。所以A用到的空间应该是7字节。但是因为编译器要对数据成员在空间上进行对齐。而由于结构体自身对齐值取数据成员中自身对齐值的最大值,即4,并且0x00~0x07的8字节空间满足8%4=0,所以sizeof(strcut A)值为8。
现在把该结构体调整成员变量的顺序。

struct B {
char b;           // b的自身对齐值为1,其起始地址为0x00,由于满足0x00%1=0,所以b存放在0x00地址空间
int a;            // a的自身对齐值为4,由于紧跟b之后的地址0x01%4不是0,而0x04%4=0,因此c的存放起始地址为0x04,存放在0x04~0x07空间。
                  //  在b和a之间的0x01~0x03地址则补3个空字节。
short c;          // c的自身对齐值为2,由于紧跟a之后的地址0x08%2=0,因此c的存放起始地址为0x08,存放在0x08~0x09空间。
};

这时候同样是总共7个字节的变量,但是由于结构体自身对齐值取数据成员中自身对齐值的最大值,即4,并且0x00~0x09的10字节空间不满足10%4=0,而12%4=0,所以sizeof(struct B)的值却是12,即在紧跟c之后的0x0A~0x0B地址还需补两个空字节,使得整个结构体占用的字节空间为12个字节。

现在我们使用预处理指令来指定结构体的对齐系数:

#pragma pack (2) /*指定按2字节对齐,等价于#pragma pack(push,2)*/
struct C {
char b;
int a;
short c;
};
#pragma pack () /*取消指定对齐,恢复缺省对齐,等价于#pragma pack(pop)*/

由上述规则4可知,结构体的对齐是按照结构体中长度最大的数据成员和#pragma pack指定值之间较小的那个值进行的,所以这里该值为2,sizeof(struct C)值是8。

这些例子引出了几个具体的概念如下:

  1. 数据类型自身的对齐值:就是上面交代的基本数据类型的自身对齐值。
  2. 指定对齐值:#pragma pack (value)时的指定对齐值value。
  3. 结构体或者类的自身对齐值:其数据成员中自身对齐值最大的那个值。
  4. 数据成员、结构体和类的有效对齐值:自身对齐值和指定对齐值中小的那个值。

其他参数使用举例

#pragma pack(push, 1)  // 将对齐系数1压入internal compiler stack,同时设置当前的packing alignment为1
#pragma pack(push, 4)  // 将4也压入栈,同时设置当前的packing alignment为4
#pragma pack(show)    // 显示当前packing alignment的字节数
#pragma pack(pop)     // 弹出internal compiler stack顶端的一条记录,这里弹出了4
#pragma pack(show)   // 此时顶端的packing alignment值为1,
    struct test{
        char m1;
        short m2; 
        int m3;
    };
#pragma pack()  // 更改为缺省值8 
#pragma pack(show)

在VS2013下的编译结果如下图所示。


编译结果

另外值得一提的是,在ARM平台的编译器中,没有提供如“#pragma pack”这样带参数的对齐指令,只有一个关键字 __packed。
__packed 限定符将所有有效类型的对齐边界设置为 1,如果一个结构没有这个限定符,默认向表数能力最强的那个数据类型对齐。

typedef __packed struct 
{
    double dValue1;
    char   u8Value2;
    int    u32Value3;
} ASampleStructor;

上例中,size值为13,说明1字节对齐后,该结构总长为13字节。去掉__packed对齐后,为16字节。

上一篇下一篇

猜你喜欢

热点阅读