#pragma pack,__attribute__选项和结构体对齐相关问题详解

  • Post author:
  • Post category:其他


自己编程测试时,

#pragma pack(show)这个老是提示unknown action ‘show’ for ‘#pragma pack’ – ignored [-Wpragmas]
程序编译器对结构的存储的特殊处理确实提高CPU存储

变量

的速度,但是有时候也带来了一些麻烦,我们也屏蔽掉变量默认的对齐方式,自己可以设定变量的对齐方式。

编译器中提供了

#pragma

pack(n)来设定变量以n

字节对齐

方式。n字节对齐就是说变量存放的起始地址的

偏移量

有两种情况:第一、如果n大于等于该变量所占用的字节数,那么偏移量必须满足默认的对齐方式,第二、如果n小于该变量的类型所占用的字节数,那么偏移量为n的倍数,不用满足默认的对齐方式。结构的总大小也有个约束条件,分下面两种情况:如果n大于所有

成员变量

类型所占用的字节数,那么结构的总大小必须为占用空间最大的变量占用的空间数的倍数;否则必须为n的倍数。

下面举例说明其用法。

#pragma

pack(push) //保存对齐状态
#pragma pack(4)//设定为4

字节对齐
struct test
{
char m1;
double m4;
int m3;
};
#pragma pack(pop)//恢复对齐状态
以上

结构体

的大小为16,下面分析其存储情况,首先为m1分配空间,其

偏移量

为0,满足我们自己设定的对齐方式(4

字节对齐

),m1大小为1个字节。接着开始为m4分配空间,这时其偏移量为1,需要补足3个字节,这样使偏移量满足为n=4的倍数(因为sizeof(double)大于4),m4占用8个字节。接着为m3分配空间,这时其偏移量为12,满足为4的倍数,m3占用4个字节。这时已经为所有

成员变量

分配了空间,共分配了16个字节,满足为n的倍数。如果把上面的

#pragma

pack(4)改为#pragma pack(8),那么我们可以得到结构的大小为24。
作用:指定

结构体

、联合以及类成员的packing alignment;
语法:

#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。

重要规则:1,复杂类型中各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个类型的地址相同;2,每个成员分别对齐,即每个成员按自己的方式对齐,并最小化长度;


规则就是每个成员按其类型的对齐参数(通常是这个类型的大小)和指定对齐参数中较小的一个对齐;


3,结构、联合或者类的


数据成员


,第一个放在偏移为0的地方;以后每个数据成员的对齐,按照


#pragma


pack指定的数值和这个数据成员自身长度两个中比较小的那个进行;也就是说,当#pragma pack指定的值等于或者超过所有数据成员长度的时候,这个指定值的大小将不产生任何效果;4,复杂类型(如结构)整体的对齐<注意是“整体”>是按照结构体中长度最大的数据成员和#pragma pack指定值之间较小的那个值进行;这样在成员是复杂类型时,可以最小化长度;5,结构整体长度的计算必须取所用过的所有对齐参数的整数倍,不够补空字节;


也就是取所用过的所有对齐参数中最大的那个值的整数倍,因为对齐参数都是2的n次方;这样在处理

数组

时可以保证每一项都

边界对齐


对齐的算法: 由于各个平台和

编译器

的不同,现以本人使用的gcc version 3.2.2编译器(32位x86平台)为例子,来讨论编译器对struct数据结构中的各成员如何进行对齐的。

在相同的对齐方式下,

结构体

内部数据定义的顺序不同,结构体整体占据内存空间也不同,如下: 设结构体如下定义: struct A { int a; char b; short c; }; 结构体A中包含了4

字节

长度的int一个,1字节长度的char一个和2字节长度的short型数据一个。所以A用到的空间应该是7字节。但是因为编译器要对

数据成员

在空间上进行对齐。所以使用sizeof(strcut A)值为8。 现在把该结构体调整

成员变量

的顺序。 struct B { char b; int a; short c; }; 这时候同样是总共7个字节的

变量

,但是sizeof(struct B)的值却是12。 下面我们使用

预编译

指令

#pragma

pack (value)来告诉

编译器

,使用我们指定的对齐值来取代缺省的。 #pragma pack (2) /*指定按2

字节对齐

,等价于#pragma pack(push,2)*/ struct C { char b; int a; short c; }; #pragma pack () /*取消指定对齐,恢复缺省对齐,等价于#pragma pack(pop)*/ sizeof(struct C)值是8。 修改对齐值为1:#pragma pack (1) /*指定按1字节对齐*/ struct D { char b; int a; short c; }; #pragma pack () /*取消指定对齐,恢复缺省对齐*/ sizeof(struct D)值为7。


对于char型数据,其自身对齐值为1,对于short型为2,对于int,float,long类型,其自身对齐值为4,double,long long类型,其自身对齐值为8,单位

字节



这里面有四个概念值:


1.数据类型自身的对齐值:就是上面交代的基本数据类型的自身对齐值。 2.指定对齐值:

#pragma

pack (value)时的指定对齐值value。 3.

结构体

或者类的自身对齐值:其

数据成员

中自身对齐值最大的那个值。 4.数据成员、结构体和类的有效对齐值:自身对齐值和指定对齐值中小的那个值。


有了这些值,我们就可以很方便的来讨论具体数据结构的成员和其自身的对齐方式。


有效对齐值N是最终用来决定数据存放地址方式的值,最重要。有效对齐N,就是表示“对齐在N上”,也就是说该数据的”存放起始地址%N=0″.而数据结构中的数据

变量

都是按定义的先后顺序来排放的。第一个数据变量的起始地址就是数据结构的起始地址。

结构体



成员变量

要对齐排放,结构体本身也要根据自身的有效对齐值圆整(就是结构体成员变量占用总长度需要是对结构体有效对齐值的整数倍,


结合下面例子理解)。这样就不能理解上面的几个例子的值了。 例子分析: 分析例子B; struct B { char b; int a; short c; }; 假设B从


地址空间


0x0000开始排放。该例子中没有定义指定对齐值,在笔者环境下,该值默认为4。
第一个成员变量b的自身对齐值是1,比指定或者默认指定对齐值4小,所以其有效对齐值为1,所以其存放地址0x0000符合0x0000%1=0.
第二个

成员变量

a,其自身对齐值为4,所以有效对齐值也为4,所以只能存放在起始地址为0x0004到0x0007这四个连续的字节空间中,符合0x0004%4=0, 且紧靠第一个变量。
第三个

变量

c,自身对齐值为2,所以有效对齐值也是2,可以存放在0x0008到0x0009这两个字节空间中,符合0x0008%2=0。所以从0x0000到0x0009存放的都是B内容。
再看数据结构B的自身对齐值为其变量中最大对齐值(这里是a)和指定对齐值(这里是4)中较小的那个,所以就是4,所以

结构体

的有效对齐值也是4。根据结构体圆整的要求,0x0009到0x0000=10

字节

,(10+2)%4=0。所以0x0000A到0x000B也为结构体B所占用。故B从0x0000到0x000B共有12个字节,sizeof(struct B)=12; 同理,分析上面例子C:

#pragma

pack (2) /*指定按2

字节对齐

*/ struct C { char b; int a; short c; }; #pragma pack () /*取消指定对齐,恢复缺省对齐*/ 第一个

变量

b的自身对齐值为1,指定对齐值为2,所以,其有效对齐值为1,假设C从0x0000开始,那么b存放在0x0000,符合0x0000%1=0;
第二个变量,自身对齐值为4,指定对齐值为2,所以有效对齐值为2,所以顺序存放在0x0002、0x0003、0x0004、0x0005四个连续字节中,符合0x0002%2=0。
第三个变量c的自身对齐值为2,所以有效对齐值为2,顺序存放在0x0006、0x0007中,符合0x0006%2=0。所以从0x0000到0x00007共八

字节

存放的是C的

变量


又C的自身对齐值为4,所以C的有效对齐值为2。又8%2=0,C只占用0x0000到0x0007的八个字节。所以sizeof(struct C)=8


pack 为 struct, union 和 class 等的成员对齐指定字节边界. 与编译选项的 /Zp 开关不同, 它不针对整个项目, 而仅针对模块, 比如一个编译单元.








1. #pragma pack(show)




以警告信息的形式显示当前字节对齐的值.




2. #pragma pack(n)




将当前字节对齐值设为 n .




3. #pragma pack()




将当前字节对齐值设为默认值(通常是8) .




4. #pragma pack(push)




将当前字节对齐值压入编译栈栈顶.




5. #pragma pack(pop)




将编译栈栈顶的字节对齐值弹出并设为当前值.




6. #pragma pack(push, n)




先将当前字节对齐值压入编译栈栈顶, 然后再将 n 设为当前值.




7. #pragma pack(pop, n)




将编译栈栈顶的字节对齐值弹出, 然后丢弃, 再将 n 设为当前值.




8. #pragma pack(push, identifier)




将当前字节对齐值压入编译栈栈顶, 然后将栈中保存该值的位置标识为 identifier .




9. #pragma pack(pop, identifier)




将编译栈栈中标识为 identifier 位置的值弹出, 并将其设为当前值. 注意, 如果栈中所标识的位置之上还有值, 那会先被弹出并丢弃.




10. #pragma pack(push, identifier, n)




将当前字节对齐值压入编译栈栈顶, 然后将栈中保存该值的位置标识为 identifier, 再将 n 设为当前值.




11. #pragma pack(pop, identifier, n)




将编译栈栈中标识为 identifier 位置的值弹出, 然后丢弃, 再将 n 设为当前值. 注意, 如果栈中所标识的位置之上还有值, 那会先被弹出并丢弃.








注意: 如果在栈中没有找到 pop 中的标识符, 则编译器忽略该指令, 而且不会弹出任何值.








// 代码段 1: 弹出编译栈的顺序跟压入的顺序相反




#pragma pack(show)     // 8 (默认值)




#pragma pack(push, 16) // 默认值 8 压入编译栈栈顶, 并将当前对齐值设为 16 .




#pragma pack(show)     // 上句设定的 16




#pragma pack(push, 4)  // 上上句 16 压入编译栈栈顶, 并将当前对齐值设为 4 .




#pragma pack(show)     // 上句设定的 4




#pragma pack(push, 2)  // 上上句 4 压入编译栈栈顶, 并将当前对齐值设为 2 .




#pragma pack(show)     // 上句设定的 2




#pragma pack(push, 1)  // 上上句 2 压入编译栈栈顶, 并将当前对齐值设为 1 .




#pragma pack(show)     // 上句设定的 1




#pragma pack(pop)      // 弹出编译栈栈顶的 2 , 并将其设为当前对齐值.




#pragma pack(show)     // 2




#pragma pack(pop)      // 弹出编译栈栈顶的 4 , 并将其设为当前对齐值.




#pragma pack(show)     // 4




#pragma pack(pop)      // 弹出编译栈栈顶的 16 , 并将其设为当前对齐值.




#pragma pack(show)     // 16




#pragma pack(pop)      // 弹出编译栈栈顶的 8 , 并将其设为当前对齐值.




#pragma pack(show)     // 8








// 代码段 2: pop 带有参数 n 时, 当前字节对齐值被设为了 n, 而不是从栈顶弹出的之前所压入的值.




#pragma pack(show)     // 8 (默认值)




#pragma pack(push, 16) // 默认值 8 压入编译栈栈顶, 并将当前对齐值设为 16 .




#pragma pack(show)     // 16




#pragma pack(push, 4)  // 上上句 16 压入编译栈栈顶, 并将当前对齐值设为 4 .




#pragma pack(show)     // 4




#pragma pack(push, 2)  // 上上句 4 压入编译栈栈顶, 并将当前对齐值设为 2 .




#pragma pack(show)     // 2




#pragma pack(push, 1)  // 上上句 2 压入编译栈栈顶, 并将当前对齐值设为 1 .




#pragma pack(show)     // 1




#pragma pack(pop, 8)   // 弹出编译栈栈顶的 2 , 然后丢弃, 再将当前对齐值设为 8 .




#pragma pack(show)     // 8




#pragma pack(pop, 1)   // 弹出编译栈栈顶的 4 , 然后丢弃, 再将当前对齐值设为 1 .




#pragma pack(show)     // 1




#pragma pack(pop, 2)   // 弹出编译栈栈顶的 16 , 然后丢弃, 再将当前对齐值设为 2 .




#pragma pack(show)     // 2




#pragma pack(pop, 16)  // 弹出编译栈栈顶的 8 , 然后丢弃, 再将当前对齐值设为 16 .




#pragma pack(show)     // 16








// 代码段3: push 和 pop 可以带有标识符, 此标识符能够弹出指定值. 但是, 位于栈中指定值之上的那些值均会被弹出并丢弃.




#pragma pack(show)                   // 8 (默认值)




#pragma pack(push, identifier_1, 1)  // 默认值 8 压入编译栈栈顶, 并将栈中 8 对应的位置用 identifier_1 标识起来, 然后将当前对齐值设为 1 .




#pragma pack(show)                   // 1




#pragma pack(push, identifier_2, 2)  // 上上句 1 压入编译栈栈顶, 并将栈中 1 对应的位置用 identifier_2 标识起来, 然后将当前对齐值设为 2 .




#pragma pack(show)                   // 2




#pragma pack(push, identifier_3, 4)  // 上上句 2 压入编译栈栈顶, 并将栈中 2 对应的位置用 identifier_3 标识起来, 然后将当前对齐值设为 4 .




#pragma pack(show)                   // 4




#pragma pack(push, identifier_4, 8)  // 上上句 4 压入编译栈栈顶, 并将栈中 4 对应的位置用 identifier_4 标识起来, 然后将当前对齐值设为 8 .




#pragma pack(show)                   // 8




#pragma pack(push, identifier_5, 16) // 上上句 8 压入编译栈栈顶, 并将栈中 8 对应的位置用 identifier_5 标识起来, 然后将当前对齐值设为 16 .




#pragma pack(show)                   // 16




#pragma pack(push, identifier_6)     // 上上句 16 压入编译栈栈顶, 并将栈中 16 对应的位置用 identifier_6 标识起来.




#pragma pack(show)                   // 16




#pragma pack(pop, identifier_6)      // 将标识符 identifier_6 对应的栈中值 16 弹出, 并将其设为当前对齐值.




#pragma pack(show)                   // 16




#pragma pack(pop, identifier_5, 2)   // 将标识符 identifier_6 对应的栈中值 8 弹出, 然后丢弃, 再将当前对齐值设为 2 .




#pragma pack(show)                   // 2




#pragma pack(pop, identifier_1)      // 按入栈顺序进行弹出, 直到遇到标识符 identifier_1 标识的8 .




#pragma pack(show)                   // 8




__attribute__选项


该属性设定一个指定大小的对齐格式(以字节 为单位),例如:



struct S {


short b[3];

} __attribute__ ((aligned (8)));


结果是8




typedef int int32_t __attribute__ ((aligned (8)));


结果是4




struct S s{


int b;



} __attribute__ ((aligned (8)));


结果是8




该声明将强制编译器确保(尽它所能)变量类 型为struct S 或者int32_t 的变量在分配空间时采用8 字节对齐方式。

如上所述,你可以手动指定对齐的格式,同 样,你也可以使用默认的对齐方式。如果aligned 后面不紧跟一个指定的数字值,那么编译器将依据你的目标机器情况使用最大最有益的对齐方式。例如:


struct S {



short b[3];



} __attribute__ ((aligned));


这里,如果sizeof (short )的大小为2 (byte ),那么,S 的大小就为6 。取一个2 的次方值,使得该值大于等于6 ,则该值为8 ,所以编译器将设置S 类型的对齐方式为8 字节。



aligned 属性使被设置的对象占用更多的空间,相反的,使用packed 可以减小对象占用的空间。



需要注意的是,attribute 属性的效力与你的连接器也有关,如果你的连接器最大只支持16 字节对齐,那么你此时定义32 字节对齐也是无济于事的。



struct stu{

char sex;

int length;

char name[10];

}__attribute__((aligned (1)));


struct stu my_stu;

则sizeof(my_stu)可以得到大小为20。




struct stu{

char sex;

int length;

char name[10];

}__attribute__((packed));

struct stu my_stu;

则sizeof(my_stu)可以得到大小为18。

struct stu22{




char sex;



int length;



char name[10];



}__attribute__((aligned));

结果是32.


另外,还有如下的一种方式:


·


__attribute((aligned(n)))


,让所作用的结构成员对齐在


n


字节自然边界上。如果结构中有成员的长度大于


n


,则按照最大成员的长度来对齐。


·


__attribute__((packed))


,取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐。

#pragmapack(n)和__attribute__((aligned(m)))的区别:

前者告诉编译器结构体或

类内部的成员变量相对于第一个变量的地址的偏移量的对齐方式

,缺省情况下,编译器按照自然边界对齐,

当变量所需的自然对齐边界比


n





时,按照


n


对齐,否则按照自然边界对齐



后者告诉编译器一个结构体或者类或者联合或者一个类型的变量


(


对象


)


分配地址空间时的地址对齐方式。



就是所,如


果将


__attribute__((aligned(m)))


作用于一个类型,那么该类型的变量在分配地址空间时,其存放的地址一定按照


m


字节对齐


(m





须是


2


的幂次方


)

。并且其占用的空间,即大小,也是m的整数倍,以保证在申请连续存储空间的时候,每一个元素的地址也是按照m字节对齐。 __attribute__((aligned(m)))也可以作用于一个单独的变量。举例说明:

#include<stdio.h>

#pragma pack(4)

typedef struct{

uint32_t f1;

uint8_t f2;

uint8_t f3;

uint32_t f4;

uint64_t f5;

}__attribute__((aligned(1024)))ts;

int main()

{

printf(“Structsize is: %d, aligned on 1024\n”,sizeof(ts));

printf(“Allocatef1 on address: 0x%x\n”,&(((ts*)0)->f1));

printf(“Allocatef2 on address: 0x%x\n”,&(((ts*)0)->f2));

printf(“Allocatef3 on address: 0x%x\n”,&(((ts*)0)->f3));

printf(“Allocatef4 on address: 0x%x\n”,&(((ts*)0)->f4));

printf(“Allocatef5 on address: 0x%x\n”,&(((ts*)0)->f5));

return 0;

}

输出:

Struct size is:1024, aligned on 1024

Allocate f1 onaddress: 0x0

Allocate f2 onaddress: 0x4

Allocate f3 onaddress: 0x5

Allocate f4 onaddress: 0x8

Allocate f5 onaddress: 0xc

注意

绿色部分表明了__attribute__((aligned(1024)))的作用


红色部分说明#pragma pack(4)只对大小大于4的成员变量的地址偏移起作用


紫色部分说明对于大小大于4的成员变量,其地址偏移按照4字节对齐


·


使用伪指令


#pragma pack (n)





C


编译器将按照


n


个字节对齐。


·


使用伪指令


#pragma pack ()


,取消自定义字节对齐方式。