pragma pack & align()
看一下这两个指令是如何影响变量在内存的存储的。
1、pack pragma
pack pragma设置了struct、union或class中各成员的对齐方式,结构成员对齐指的是成员相对于起始地址的偏移量。该指令基本用法如下:
#pragma pack(n)
它指定了结构成员按n(1,2,4,8,16)字节对齐,如果未指定n,则恢复成默认值。需要注意的是,它并不是指结构体中的每个成员都要按n对齐,而是按照每个成员的大小和n相比较小的值对齐。下面引用MSDN中C++ Preprocessor Reference部分关于pack指令的说明:
n (optional)
Specifies the value, in bytes, to be used for packing. The default value for n is 8. Valid values are 1, 2, 4, 8, and 16. The alignment of a member will be on a boundary that is either a multiple of n or a multiple of the size of the member, whichever is smaller.
即成员member的对齐值 align of member = min( pack setting value, sizeof(member) )
请看下面示例代码:
#include <iostream>
using namespace std;
#pragma pack(show) //显示当前结构成员对齐设置
#pragma pack(8)
struct A
...{
int n;
char c;
short s;
};
struct B
...{
char c;
int n;
short s;
};
#pragma pack()
int _tmain(int argc, _TCHAR* argv[])
...{
A a;
B b;
memset( &a, 0, sizeof(A) );
memset( &b, 0, sizeof(B) );
a.c = '1';
a.n = 2;
a.s = 3;
b.c = '1';
b.n = 2;
b.s = 3;
cout << sizeof(A) << endl;
cout << sizeof(B) << endl;
return 0;
}
笔者的测试环境为x86体系32位计算机 win2000操作系统,VS2003编译器。
编译器默认的成员对齐值是8字节,通过#pragma pack(show)指令,编译的时候在输出栏会限制默认对齐值。以上程序运行完通过调试的内存察看功能得到a和b的内存存储区域如下:
a的存储区域:0x0012FED0 02 00 00 00 31 00 03 00
b的存储区域:0x0012FEBC 31 00 00 00 02 00 00 00 03 00 00 00
最前面的4字节整数是变量的起始地址,后面是变量的整个存储区域。
现在我们按照 align of member = min( pack setting value, sizeof(member) )的公式分析一下a和b的存储。
a 的第一个成员n为int,大小为4,align of a.n = min(8,sizeof(int) ),对齐值为4。第一个成员相对于结构体起始地址从0偏移开始,前四个字节02 00 00 00即为n的存储区域,因为x86是Little Endian(低字节在前)的字节顺序,所以第一字节是2,后面三个字节0,我们通常写成0x00000002;
a的第二个成员c为char,大小为1,align of a.c=min(8,sizeof(char)),对齐值为1。c紧接着a后面存储从偏移4开始,满足1字节对齐的要求。它的值为'1',ASCII码为0x31,共一个字节31;
a的第三个成员为short,大小为2,align of a.s=min(8,sizeof(short)),对齐值为2。如果紧接第二个成员从偏移5开始存储就不满足2字节对齐,因此跳过1个字节,从偏移6字节的地方开始存储,即最后两个字节03 00;
b的第一个成员c为char,大小为1,align of a.c=min(8,sizeof(char)),对齐值为1。第一个成员从偏移起始地址0字节开始存储,它的值为'1',ASCII码为0x31,共一个字节31;
b 的第二个成员n为int,大小为4,align of a.n = min(8,sizeof(int) ),对齐值为4。如果紧接第二个成员后面从偏移1开始存储就不能4字节对齐,因此跳过3个字节,从偏移4字节的地方开始存储,即第5-8的四个字节02 00 00 00;
b的第三个成员为short,大小为2,align of a.s=min(8,sizeof(short)),对齐值为2。紧接第二个成员从偏移8字节的地方开始存储,即9-10两个字节03 00;
这时有人可能要问,b为什么最后多了两个字节00 00呢?这就是我们下面要讲的,整个结构体的对齐。
2、align指令
align指令可以用于设置各种内置类型、自定义类型如struct、union或class的的对齐方式。指令格式为: __declspec(align( # )) ,#是对齐值,取值为2的1次方至2的8192次方。在声明自定义类型或内置变量时,如果指定了对齐值,则对应变量的起始地址必须是该值的整数倍。除此外,它还会影响结构体的大小。下面引用两段MSDN关于align的描述:
Without __declspec(align( # )) , Visual C++ aligns data on natural boundaries based on the size of the data, for example 4-byte integers on 4-byte boundaries and 8-byte doubles on 8-byte boundaries. Data in classes or structures is aligned within the class or structure at the minimum of its natural alignment and the current packing setting (from #pragma pack or the /Zp compiler option).
从这段可以看出,如果没有设置align(#)值,变量x按照sizeof(x)来对齐起始地址。类或结构体内的成员在类或结构体内部按照min( pack setting value,sizeof(member))来对齐。这个我们在pack指令部分已经分析过。
The sizeof
value for any structure is the offset of the final member, plus that member's size, rounded up to the nearest multiple of the largest member alignment value or the whole structure alignment value, whichever is greater.
从这段可以看出,align(#)指令会影响结构体或类的大小。总结公式为:
sizeof(structure) = (结构体最后一个成员的偏移 + sizeof(结构体最后一个成员) ) 上取整 ( n* max( 结构体各成员的对齐值,align(#)设置的值 ) ); 其中n为正整数 。
根据该公式我们分析一下b为什么后面会多两个填充字节0。
b的最后一个成s偏移为8,大小为2,b中各成员对齐值最大的为4,因为未设置align(#),所以上取整的数值为4n。8+2按4的倍数上取整为12。因此后面需要填充两个字节,这样才能使sizeof(b) == 12。
下面以一代码来说明align(#)指令的用法:
#include <iostream>
using namespace std;
#define CACHE_LINE 32
#define CACHE_ALIGN __declspec(align(CACHE_LINE))
#pragma pack(8)
struct CACHE_ALIGN S1
...{
int a, b, c, d;
};
struct S3
...{
struct S1 s1;
int a;
};
#pragma pack()
int _tmain(int argc, _TCHAR* argv[])
...{
CACHE_ALIGN int i = 2;
cout << sizeof(S1) << endl;
cout << sizeof(S3) << endl;
return 0;
}
运行程序输出32和64,按公式sizeof(structure) = (结构体最后一个成员的偏移 + sizeof(结构体最后一个成员) ) 上取整 ( n* max( 结构体各成员的对齐值,align(#)设置的值 ) )分析:
sizeof(S1) = (12+4) 上取整 ( n * max( 4, 32 ) )
sizeof(S1) = (16) 上取整 ( 32 )
sizeof(S1) = 32
S3的大小留待大家练练手。