一:对齐规则
在没有__attribute__((aligned(n)))或#pragma pack修饰的声明下,字节对齐遵循下面三个原则:
1:结构体(struct)的数据成员,第一个数据成员存放的地址为结构体变量偏移量为0的地址处,结构体变量的首地址能够被其最宽基本类型成员的大小所整除。
数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组,结构体等)的整数倍开始(比如int在32位机为4字节,则要从4的整数倍地址开始存储。
2:其他结构体成员自身对齐时,存放的地址为min{自身对齐值, 指定对齐值} 的最小整数倍的地址处.
注:自身对齐值:结构体变量里每个成员的自身大小
注:指定对齐值:有宏 #pragma pack(N) 指定的值,这里面的 N一定是2的幂次方.如1,2,4,8,16等.如果没有通过宏那么在32位Linux主机上默认指定对齐值为4,64位的默认对齐值为8,AMR CPU默认指定对齐值为8;__attribute__((aligned(n)))作用在结构体总体大小上,不影响结构体内部成员的排序。备注:结构体A中嵌套结构体B时,结构体B的地址按照B内成员最大宽度来对齐。计算大小时结构体B在结构体A中展开。结构体作为成员,如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储.(struct a里存有struct b,b里有char,int ,double等元素,那b应该从8的整数倍开始存储.)
3:结构体的总大小,也就是sizeof的结果,总体对齐时,字节大小是min{所有成员中自身对齐值最大的, 指定对齐值} 的整数倍.不足的要补齐。__attribute__((aligned(n)))指定是最小对齐值,n < min{所有成员中自身对齐值最大的, 指定对齐值}时忽略,n > min{所有成员中自身对齐值最大的, 指定对齐值}时,总大小与n对齐。
二:测试样例
下面以32位处理器来看几个例子:
测试1:
struct test
{
char a; //1
char b; //1
char c; //1
};
32位系统下,自然对齐,sizeof大小是3字节。
测试2:
struct test
{
char a; //1
short b; //2
char c; //1
};
32位系统下,自然对齐,sizeof大小是6字节。
来结合对齐规则来看一下,
1、第一个成员首地址为0(准确说是偏移量),这个没什么好说,
2、每个成员的首地址是自身大小的整数倍,因为b是short类型的,占用两个字节,所以,必须以2字节对齐,也就是说你可以把b放在0啊,2啊,4啊这些地址,但是你不能放在1,3,5这样的地址。a的地址是0,下一个地址是1,不能放,只能空掉,放在2位置处。这样,a和b就占了4个字节了,接下来c占一个字节。
3,看第三条规则,结构体的总大小,为其成员中所含最大类型的整数倍。所以,在这个例子中,结构体总大小应该要为2的整数倍,所以是6,
测试3:
struct test
{
char a; //1+3
int b; //4
short c; //2+2
};
32位系统下,自然对齐,sizeof大小是12字节。
测试4:
struct test
{
char d[7];
double a;
short b;
char* c;
};
32位系统下,自然对齐,sizeof大小是24字节。
指针在32位系统中都是占4个字节
typedef struct bb
{
int id; //[0]....[3]
double weight; //[8].....[15] 原则1
float height; //[16]..[19],总长要为8的整数倍,补齐[20]...[23] 原则3
}BB;
typedef struct aa
{
char name[2]; //[0],[1]
int id; //[4]...[7] 原则1
double score; //[8]....[15]
short grade; //[16],[17]
BB b; //[24]......[47] 原则2
}AA;
int main(void)
{
AA a;
printf("a size %d\n",sizeof(a));
return 0;
}
输出结果:a size 48
32位系统下,自然对齐,sizeof大小是48字节。
三、为什么要字节对齐
需要字节对齐的根本原因在于CPU访问数据的效率问题。假设上面整型变量的地址不是自然对齐,比如为0x00000002,则CPU如果取它的值的话需要访问两次内存,第一次取从0x00000002-0x00000003的一个short,第二次取从0x00000004-0x00000005的一个short然后组合得到所要的数据,如果变量在0x00000003地址上的话则要访问三次内存,第一次为char,第二次为short,第三次为char,然后组合得到整型数据。而如果变量在自然对齐位置上,则只要一次就可以取出数据。一些系统对对齐要求非常严格,比如sparc系统,如果取未对齐的数据会发生错误。
对于标准数据类型,它的地址只要是它的长度的整数倍就行了,而非标准数据类型按下面的原则对齐:
数组 :按照基本数据类型对齐,第一个对齐了后面的自然也就对齐了。
联合 :按其包含的长度最大的数据类型对齐。
结构体: 结构体中每个数据类型都要对齐。
在设计不同CPU下的通信协议时,或者编写硬件驱动程序时寄存器的结构这两个地方都需要按一字节对齐。即使看起来本来就自然对齐的也要使其对齐,以免不同的编译器生成的代码不一样.
四:__attribute__选项
1. __attribute__ 是什么
__attribute__是GCC里的编译参数,用法有很多种,感兴趣可以阅读一下gcc的相关文档。这里说一下__attribute__对变量和结构体对齐的影响。这里的影响大概分为两个方面,对齐和本身占用的字节数的大小,即sizeof(变量)的值。
2,__attribute__在字节对齐上的使用
__attribute__((packed)):取消对其原则,按照紧密对齐(也就是1字节对齐)
__attribute__((__aligned__(n))):n可以为1/2/4/8等,按照n字节对齐。
正确理解__attribute__((aligned(x))):
__attribute__((__aligned__(n)))定义的是最小对齐边界,它可以用来修饰structure也可以用来修饰变量或者structure成员变量,它只能增加 structure、变量或者structure成员变量的对齐值(GCC手册说在修饰 typedef 类型时可以增加或减少对齐值)。
3. __attribute__((__aligned__(n)))修饰变量
使用的语法如下:
int a __attribute__((__aligned__(8))) = 10;
这个修饰的影响主要是对齐,所谓对齐是存储为值的起始地址。变量a的地址&a,本来是4字节对齐,变成了8字节对齐(有的环境对最大对齐数值有限制)。8字节对齐就是&a的地址是8的整数倍。
sizeof(a) = 4; //a 占用的字节数还是4个字节
下面看看在使用了typedef之后对对齐和变量大小的影响:
typedef int int32_t __attribute__((__aligned__(8))) ;
sizeof(int32_t ) = 4; //占用的字节数还是4个字节
这样说明int32_t 声明的变量按照8字节对齐,大小是4字节,这样就会有一个问题,这个变量不能定义数组:
int32_t array[2]; //这样定义编译器会报err
报错的原因是数组的存储在内存中是连续的,而int32_t 只有4字节确要8字节对齐,这样对齐和连续就不能同时保证,就会报错。
4. __attribute__((__aligned__(n)))修饰结构体
定义一个结构体:
typedef struct flag {
int a;
char b;
} FLG;
FLG test;
在32位系统下,自然对齐的情况下:sizeof(test) = 8;
注意__attribute__((__aligned__(n)))的位置。
typedef struct flag {
int a;
char b;
}__attribute__((__aligned__(32))) FLG;
FLG test;
sizeof(test) = 32;
__attribute__((__aligned__(32)))修饰struct flag,结构体按照32字节对齐
typedef struct flag {
int a;
char b;
}__attribute__((__aligned__(1))) FLG;
FLG test;
sizeof(test) = 8;
__attribute__((__aligned__(1)))修饰struct flag,结构体最小对齐值为1,系统默认4字节对齐了结构体成员,1 < 4,忽略1字节对齐。
typedef struct flag {
int a;
char b;
}FLG __attribute__((__aligned__(32)));
FLG test;
sizeof(test) = 8;
__attribute__((__aligned__(32)))修饰FLG变量,只影响对齐,不影响结构的大小,FLG的地址是32字节对齐。
五:#pragma pack(n)选项
正确理解 #pragma(pack(n))
#pragma(pack(n))定义的是最大对齐边界,它可以用来修饰structure也可以用来修饰structure成员变量,它可以增加/减少 structure或者structure成员变量的对齐值。
1,#pragma pack(n)的使用
#pragma pack需要成对使用,
#pragma pack(1)按照1字节对齐。
#pragma pack()取消定义的对齐规则恢复系统默认。
#pragma pack(1)
struct test
{
char a;
int b;
};
#pragma pack
根据我们之前的分析,这个结构体应该要占用8个字节,但是因为加了#pragma pack(1)导致整个结构体按照1字节来对齐,所以sizeof结果是5.
#pragma pack(2)
struct test
{
char a;
int b;
};
#pragma pack
使用#pragma pack(2)整个结构体按照2字节来对齐,所以sizeof结果是6.
六:#pragma pack(n)与__attribute__((aligned(x)))的区别
#pragma pack(n) 的作用范围是整个文件,也就是说结构体本身和结构体成员都需要遵守这个对齐规则;
__attribute__((aligned(x)))的作用范围如果是用在结构体变量上,则对齐规则只限于结构体地址本身,结构体大小不受影响,其内部的成员变量不遵循此规则。
__attribute__((aligned(x)))的作用范围如果是用在结构体上,则对齐规则只限于结构体本身,其内部的成员变量不遵循此规则。
__attribute__((aligned(x)))如果是修饰结构体中的成员变量,则此成员变量需要遵守对齐规则,最后结构体进行对齐时需要和成员变量的最大对齐值成倍数即可(不一定受此规则限制)。
测试样例1:
#pragma pack(push,2)
typedef struct test{
char a;
double b;
char c;
int d;
} __attribute__ ((aligned (32))) test ;
#pragma pack(pop)
① #pragma pack(push,2) 将编译器的最大对齐边界设置为 2
② 以 2 为最大对齐边界后,test 的内存对齐情况(先不考虑 aligned 属性):
typedef struct test{
char a; // 1B
double b; //因为最大对齐是2,所以只需要满足2的倍数,不需要满足8的倍数
//填充 1B ,b 本身占据 8B
char c; //对齐时我们要选择最小的对齐值,对于 c最小的对齐值是1(不是2)
//无填充,自身占据 1B
int d; //对齐值为2,故填充 1B,自身占据 4B
} test ; //整个结构体对齐值为 2(不是8,因为已经设置了最大对齐值是2)
//(1B)+(1B+8B)+(1B)+(1B+4B)+0B(已经满足2的倍数,无需对结构体进行填充)
也就是说当前(不考虑 aligned 属性)时,test 类型占据 16B,最大对齐值 2 。
③ 最后来看 aligned 属性,我们前面说了,aligned 属性设置的是最小对齐值,也就是说当前类型或变量的对齐值要大于或等于aligned 属性设置的对齐值,所以编译器重新设置 test类型的对齐值为32,则test struct需要填充16B。
✔结果就是32B 。
测试样例2:
#pragma pack(push,4)
typedef struct test{
char a;
double b;
char c;
int d;
} __attribute__ ((aligned (2))) test ;
#pragma pack(pop)
对应于 test类型的对齐值,大于 aligned 属性设置的最小对齐值的情况,这种情况下可以忽略aligned 属性。
✔结果就是20B 。
测试样例3:
typedef struct subtest{
char a;
double b;
char c;
int d;
}subtest;
#pragma pack(push,2)
typedef struct test{
char a;
subtest b;
} __attribute__ ((aligned (4))) test ;
#pragma pack(pop)
① test 的内存对齐情况(先不考虑 aligned 属性)
typedef struct subtest{
char a;
double b;
char c;
int d;
}subtest;
#pragma pack(push,2)
typedef struct test{
char a; // 1B
subtest b; //对于结构体subtest,原本以 8B做为对齐值
//但这里设置了最大对齐值2,所以 填充1B,自身占据 24B
} test ; // (1B) + (1B+24B) + 0B(已经是2的倍数,不需要再填充)
#pragma pack(pop)
② 最后来看 aligned 属性,设置的最小对齐值4 > 2,所以 test 结构体需要填充 2B,以构成4的倍数。
✔结果就是28B 。
七:含位域结构体的sizeof大小
前面已经说过,位域成员不能单独被取sizeof值,我们这里要讨论的是含有位域的结构体的sizeof,只是考虑到其特殊性而将其专门列了出来。C99规定int、unsigned int和bool可以作为位域类型,但编译器几乎都对此作了扩展,允许其它类型类型的存在。 使用位域的主要目的是压缩存储,
其大致规则为:
1) 如果相邻位域字段的类型相同,且其位宽之和小于类型的sizeof大小,则后面的字 段将紧邻前一个字段存储,直到不能容纳为止;
2) 如果相邻位域字段的类型相同,但其位宽之和大于类型的sizeof大小,则后面的字 段将从新的存储单元开始,其偏移量为其类型大小的整数倍;
3) 如果相邻的位域字段的类型不同,则各编译器的具体实现有差异,VC6采取不压缩方 式,Dev-C++采取压缩方式;
4) 如果位域字段之间穿插着非位域字段,则不进行压缩;
5) 整个结构体的总大小为最宽基本类型成员大小的整数倍。
所以使用位域时不同编译器平台下占用大小不同,尽量还是自己实际测试一下。
测试:
struct test
{
char a:1;
char b:2;
int c:3;
char d:2;
};
test t1;
int len=sizeof(t1); //len=12