定义
现代计算机中,内存空间按照字节划分,理论上可以从任何起始地址访问任意类型的变量。但实际中在访问特定类型变量时经常在特定的内存地址访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺序一个接一个地存放,这就是对齐。
原因和作用
根本原因在于CPU访问数据的效率问题。某些平台对特定类型的数据只能从特定地址开始存取,而不允许其在内存中任意存放。例如Motorola 68000 处理器不允许16位的字存放在奇地址,否则会触发异常,因此在这种架构下编程必须保证字节对齐。此外,合理利用字节对齐还可以有效地节省存储空间。但要注意,在32位机中使用1字节或2字节对齐,反而会降低变量访问速度。因此需要考虑处理器类型。还应考虑编译器的类型。在VC/C++和GNU GCC中都是默认是4字节对齐。
对齐准则
1) 结构体变量的首地址能够被其最宽基本类型成员的大小所整除
2) 结构体每个成员相对结构体首地址的偏移量都是成员大小的整数倍,如有需要编译器会在成员间加上填充字节(internal adding);
3) 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员后加上填充字节{trailing padding}。
更改缺省的对齐方式
在缺省情况下,C编译器为每一个变量或是数据单元按其自然对界条件分配空间。一般地,可以通过下面的方法来改变缺省的对界条件:
· 使用伪指令#pragma pack (n),C编译器将按照n个字节对齐。
· 使用伪指令#pragma pack (),取消自定义字节对齐方式。
另外,还有如下的一种方式:
· __attribute((aligned (n))),让所作用的结构成员对齐在n字节自然边界上。如果结构中有成员的长度大于n,则按照最大成员的长度来对齐。
· __attribute__ ((packed)),取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐。
====================================
32位系统:
struct str=
{
char i:3,
char j:4,
unsigned short k:5,
unsigned long m:9,
};
sizeof(str)=?
分析:
i占3位,j占4位,总共才7位,所以共计1byte
k虽然只占5位,但short类型只有一个k位域,所以自动补齐,占2byte。
m自然点4byte,共7byte.
但由于默认4byte对齐,所以总计8byte。
====================================================================
看下面的例子:
struct A{
char c1;
int i;
short s;
int j;
}a;
struct B{
int i;
int j;
short s;
char c1;
}b;
结构A没有遵守字节对齐原则(为了区分,我将它叫做对齐声明原则),结构B遵守了。我们来看看在x86上会出现什么结果。
先打印出a和b的各个成员的地址。会看到a中,各个成员间的间距是4个字节。b中,i和j,j和s都间距4个字节,但是s和c1间距2个字节。
所以: sizeof(a) = 16 sizeof(b) = 12
为什么会有这样的结果呢?这就是x86上字节对齐的作用。为了加快程序执行的速度,一些体系结构以对齐的方式设计,通常以字长作为对齐边界。对于一些结构体变量,整个结构要对齐在内部成员变量最大的对齐边界,如B,整个结构以4为对齐边界,所以sizeof(b)为12,而不是11。
对于A来讲,虽然声明的时候没有对齐,但是根据打印出的地址来看,编译器已经自动为其对齐了,所以每个成员的间距是4。
在x86下,声明A与B唯一的差别,仅在于A多浪费了4个字节内存。(是不是某些特定情况下,B比A执行更快,这个还需要讨论。
比如紧挨的两条分别取s和c1的指令)
======================================================
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()
{
AA a;
cout<<sizeof(a)<<" "<<sizeof(BB)<<endl;
return 0;
}
结果是: 48 24
=============================================================
设对齐字节数为n(n = 4或8,区别于32位或者64位操作系统),每个成员内存长度为Li, Max(Li)为最大的成员内存长度,字节对齐规则是:
1. 结构体对象的起始地址能够被Max(Li)所整除;(一般情况下这条规则满足)
2. 结构体中每个成员相对于起始地址的偏移量,即对齐值应是min(n,Li)的倍数.若不满足对齐值的要求,编译器会在成员之间填充若干个字节;(这里总是容易出错)
3. 结构体的总长度值应是min(n,Max(Li))的倍数,若不满足总长度值的要求,编译器在为最后一个成员分配空间后,会在其后填充若干个字节.
//第一个范例
int main()
{
struct s {
int i;
char ch;
short c;
};
printf("%d", sizeof( struct s ));
return 0;
}
//结果为:4+1+X+2=8 (X=1,满足于对齐规则第2条)
//第二个范例
int main()
{
struct s {
char ch;
int i;
short c;
};
printf("%d", sizeof( struct s ));
return 0;
}
//结果为:1+X+4+2+Y=12 (X=3,Y=2,分别满足于对齐规则第2条和第3条)
《使用pragma pack(push,n)和pragma pop组合》
//第三个范例
#pragma pack(push,2) //此处n=2,(相当于将VC默认的对齐字节数设定为n=2)
int main()
{
struct s {
int i;
char ch;
short c;
};
#pragma pack(pop)
printf("%d", sizeof( struct s ));
return 0;
}
//结果为:4+1+X+2=8(X=1,满足于对齐规则第条)
//第四个范例
#pragma pack(push,4) //此处n=4,(相当于将VC默认的对齐字节数设定为n=4)
int main()
{
struct s {
int i;
char ch;
short c;
};
#pragma pack(pop)
printf("%d", sizeof( struct s ));
return 0;
}
//结果为:4+1+X+2=8(X=1,满足于对齐规则第2条)
//第五个范例
#pragma pack(push,8) //此处n=8,(相当于将VC默认的对齐字节数设定为n=8)
int main()
{
struct s {
int i;
int y;
char ch;
short c;
};
#pragma pack(pop)
printf("%d", sizeof( struct s ));
return 0;
}
//结果为:4+4+1+X+2=12(X=1,满足于对齐规则第2条)
=============================================================
struct S1 {
char c; // 1个字节
int i; // 前面空3个字节,占用4个字节
}; // 刚好8个字节,是4的倍数
struct S2 {
char c1; // 1个字节
S1 s; // 前面空3个字节,而不是空7个字节,占用8个字节
char c2; // 占用1个字节
}; // 一共13个字节,要成为4的倍数,后面增加3个字节,成为16个字节
======================================
struct{
int a1:8;
int a2:8;
char a3[2];
char a4[2];
}SA;
sizeof(sA) = 8; //第一个位域占8位,第二个位域占8位,加一起16位,没超过4个字节,补全4个字节。后面两个正常的字符串,一起8个字节
=================================================
struct st1{ struct st2{ struct st3{ struct st4{ struct st5{ struct st6{
char c1; char c1; char arr[5]; char arr[5]; char c1; char c1;
char c2; int a; int b; short c; char c2; double a;
int a; char c2; short c; int b; double a; char c2;
}; }; }; }; }; };
st1\st2分别为8,12
1 1:1+3
1:1+3 4:必须是4的倍数
4:必须为4的倍数 1:整体书4的倍数
st3\st4分别为16,12
5:5+3 5:5+1
4:4 2:必须是2的倍数
2:4,必须为4的倍数 4:必须是4的倍数
st5\st6分别为16,24
1 1:1+7
1 8:必须为8的倍数
8:前面的必须为8的倍数 1:为保证整个结构体的 大小为8的倍数
============================================================
大端模式:认为第一个字节是最高位字节,也就说按照从低地址到高地址的顺序存放数据的高位字节到低位字节。
小端模式:认为第一个字节是最低位字节,也就是说按照从低地址到高地址的顺序存放数据的低位字节到高位字节。
可以理解为:
大端:较高的有效字节存放在较低的存储器地址,较低的有效字节存放在较高的存储器地址。
小端:较高的有效字节存放在较高的的存储器地址,较低的有效字节存放在较低的存储器地址。
假设从内存地址0x0000开始有以下数据:
内存地址:0x0000 0x0001 0x0002 0x0003
对应数据:0x12 0x34 0x56 0x78
如果我们去读取一个地址为0x0000的4字节变量
若字节序位为小端模式,读出为:0x78563412
若字节序位为大端模式,读出为:0x12345678
一般来说:X86系列的CPU都是小端字节序
powerPC通常是大端字节序。
运行结果为:37363534
分析:这里是小端字节序
地址从0x0000开始,那么sz在内存中的存储为:
内存地址: 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09
对应的值: 0 1 2 3 4 5 6 7 8 9
对应的值: 48 49 50 51 52 53 54 55 56 57
对应的16进制:0x30 0x31 0x32 0x33 0x34 0x35 0x36 0x37 0x38 0x39
sz ++p
所以读取为:0x37363534