测试环境:ubuntu 64位
1、对齐原因
在设计不同cpu下的通信协议,或者编写硬件驱动程序时,可能需要按字节对齐。即使是看起来就是自然对齐的,也要设置字节对齐,以免不同编译器生成的代码不一样。
理论上来说,处理器可以访问内存的任何一个地址,但出于效率考虑,多会对数据存储进行特殊设计。比如有些平台每次读都是从偶地址开始,如果一个int型(假设为32位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出这32bit,而如果存放在奇地址开始的地方,就需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该32bit数据,显然在读取效率上下降很多;这是典型的以空间换时间。
2、对齐定义
对齐指的是:每个成员相对于这个结构体的偏移量是该成员类型有效对齐值的整数倍;结构体占用的字节数是其有效对齐值的整数倍。
3、对齐原则
(1) 数据类型自身的对齐值:64位系统(windows 和 Linux)下char、short、int、float、double自身对齐值分别是1、2、4、4、8字节。
(2) 结构体或者类的自身对齐值:其成员中自身对齐值最大的那个值。
(3) 指定对齐值:#pragma pack (value)时的指定对齐值value。
(4) 数据成员、结构体和类的有效对齐值:自身对齐值和指定对齐值中小的那个值。
4、示例
struct S
{
char a;
short b;
int c;
float d;
double e;
} ;
ubuntu 64位下默认对齐是8字节,即指定对齐值是8
char:自身对齐值是1,a的有效对齐值 = 1 = min(1, 8) ; a的偏移地址是0,0/1 = 0,所以不需要填充;
short:自身对齐值是2,b的有效对齐值 = 2 = min(2, 8); 未填充前b的偏移地址是1(a占一个字节),1/2 = 1,无法整除,因此需要在b前填充1个字节;填充后b的偏移地址是2;
int:自身对齐值是4,c的有效对齐值 = 4 = min(4,8);c的偏移地址是4(a占一个字节,填充一个字节,b占2个字节),4/4=0,不需要填充;
float:自身对齐值是4,d的有效对齐值 = 4 = min(4,8);d的偏移地址是8,8/4=0,不需要填充;
double:自身对齐值是8,e的有效对齐值 = 8 = min(8,8);e的偏移地址是12(1+1填充+2+4+4),12/8 = 4,需要填充4个字节;
因此 sizeof(s) = 1+1(填充)+2+4+4+4(填充)+8=24,实验结果也证实了这一点:
S:自身对齐值取 = 8 = max(char short int float double),S有效对齐值 = 8 = min(8,8),sizeof(s) = 24, 是8的整数倍,因此结构体不再需要填充。
下面这段代码最后需要填充个字节,sizeof(SS) = 4 ;
#define PACKED __attribute__((packed, aligned(4)))
typedef struct _SS
{
short a;
char b;
}PACKED SS;
5、如何动态修改对齐值
windows:
#pragma pack(push) //保存原来的值
#pragma pack(value) //value 是自己定义的值
struct s
{
int a;
double b;
};
#pragma pack(pop) //恢复原来的值
linux:
参考上一节代码
6、题外话
默认对齐大小为8字节,例子从其他文章摘写的
struct T1
{
char c;
int i;
double d ;
};
sizeof(T1) = 16;
struct T2
{
int i;
double d;
char c;
};
sizeof(T2) = 24
类和结构体都存在同样的问题,至于变量的存放顺序,结构体成员排列最好的方式,按类型占有字节的大小,按大到小排列(暂时还没有去证明)。但是如果一个类或者结构体需要生成大量的对象时,不妨考虑一下字节对齐问题。
7、什么时候需要考虑字节对齐
字节对齐是编译器自动实现的,对于程序员来说是透明的,因此在定义类或结构体时,如果需要其大小是一个固定值,则必须要考虑对齐问题。另外就是上一节中提到的情况。