上C++实验课的时候,坐在我旁边的同学问了我一个问题:
struct
{
char a;
int b;
double c;
} sa;
其中sizeof(char) =1; sizeof(int) = 4; sizeof(double) = 8; 所以sizeof(sa)应该等于1+4+8=13才对,但是当她编程输出sizeof(sa)的值却是16,她问我为什么?
我把我所知道的告诉了她:为了加快CPU的存取速度,C++编译器在处理数据时经常把结构变量中的成员的大小按照4或8的倍数计算,这就叫数据对齐(data alignment)。sizeof(sa)=16就是因为这个原因,并不是程序的错误,而是编译器为了使数据对齐而在结构中加上了3个字节的空位造成的。这样做可能会浪费一些内存,但理论上速度快了。
回答完她的问题后,我又给自己提出了一个问题,数据究竟是怎么对其的呢?这个问题,我也不知道,所以回到寝室我做了几个实验,并且查了相关的资料,最终得出了结论。
首先,应该说明的我的开发环境是WinXP+VC6,因为不同的操作系统和不同的编译器,得出的结果可能不一样。但是不同系统的原理应该是一样的,所以希望我所做的这些努力也能对使用其它开发环境的朋友有所帮助。
struct
{
int a;
char b;
} sa;
struct
{
int a;
char b;
double c;
} sb;
struct
{
int a;
double c;
char b;
} sc;
输出结果:sizeof(sa)=8, sizeof(sb)=16, sizeof(sc)=24;
只看sizeof(sa)的结果,我当时认为,结构体内部的数据是按4个字节一组存放的,所以尽管sizeof(char)=1但是,sa中的char b还是占用了4个字节,使得sizeof(sa)=8,为了验证我的“猜想”,我针对sa又做了一个实验:
struct
{
int a;
char b, c, d, e,;
} sa1;
struct
{
int a;
char b;
int c;
} sa2;
输出结果:sizeof(sa1)=8, sizeof(sa2)=12;
看来输出结果验证了我的刚才的“猜想”,于是,我试着用我推测出的结论解释sizeof(sb)=16, sizeof(sc)=24;
sb中的int a 占4个字节,紧接着的char b占用1个字节,但是char b随后的double c需要占用8个字节,所以char b和double c不能挤在一个4个字节的单位里面。于是在char b和double c之间填了3个字节的空位,实现与其后的数据的对齐,这样就不难理解为什么sizeof(sa1)=8了。
我试图按着同样的思路理解sizeof(sc)=24,但是却遇到了问题。sc中的int a占用4个字节,紧接着的double c占用8个字节,最后char b占用一个字节,但是为了填满4个字节的单位,char b后面应该填补3个字节的空位。这样下来sizeof(sc)应该等于4+8+4=16才对,可是输出的结果却是sizeof(sc)=24。看来我刚才的“猜想”是有问题的。问题究竟出在哪里呢?为什么根据我的“猜想”分析sa和和sb都没有问题,到sc就出现问题了呢?
sb和sc的唯一区别就是char b和double c的定义次序颠倒了,结果使得sizeof(sb)和sizeof(sc)产生了差别。能想到的唯一解释是sb和sc的对齐的最小单位不一样了。于是我又设计了一组对比:
struct
{
int a;
int d;
double c;
char b;
} sc1;
struct
{
int a;
double c;
} sc2;
输出结果:sizeof(sc1)=24, sizeof(sc2)=16;
sc1在sc的基础上在int a后面紧接着加上了int d;但是结果sizeof(sc1)=24= sizeof(sc),这说明在int a的后面存在4个字节的空位。看来这次系统给结构体内的变量分配空间的时候,不再像sa那样以4个字节为一组了。
再看sc2,只是去掉了一个char b,结果使sizeof(sc2)=16,这说明char b后面存在着7个字节的空位。由此可见这次系统是以8个字节为一组给结构体内的变量分配空间的。
综合分析以上的试验结果,sa以4个字节为一组给结构体内的变量分配空间,从而达到数据对齐;sb和sc以8个字节为一组给结构体内的变量分配空间,从而达到数据对齐。为什么会有差别呢?仔细观察不难发现:sa中占用空间最多的类型是int型,而sizeof(int)=4;sb和sc中占用空间最多的类型是double型,而sizeof(double)=8;由此可间,系统是根据结构体内所包含的类型制定分配空间的单位的。
之后我又针对这个问题做了几组试验,得到的结果与以上的分析结论相同。
这样,以后再遇到sizeof的时候,我就不用等到输出才能看到sizeof的值了,只要用看到struct内部的定义格式就能计算出sizeof的实际值了。
得出结论后,我又查阅了与sizeof相关的资料,发现可以分别在程序内部和用编译指令指定结构体内部对齐的具体方式:
1、在程序内部用#pragma pack(n)指定
#pragma pack(1)可以使编译器不在struct内留空位。
2、 用编译指令
使用CL的/Zp开关,例如:CL /Zp8 指定结构体内以8个字节为单位包装结构体内的数据。
例子:
CL /Zp8
#include <iostream>
using namespace std;
struct // packing is 8
{
int a;
char b;
} sa;
#pragma pack(push,1) // packing is now 1
struct
{
int a;
char b;
double c;
} sb;
#pragma pack(pop) // packing is 8
struct
{
int a;
double c;
char b;
} sc;
int main(void)
{
cout << "sizeof(sa) =" << sizeof(sa) << endl;
cout << "sizeof(sb) =" << sizeof(sb) << endl;
cout << "sizeof(sc) =" << sizeof(sc) << endl;
return 0;
}
输出结果:sizeof(sa)=8,sizeof(sb)=13,sizeof(sc)=24
还有一点必须指出:由#pragma pack(n)设定packing后,编译器实际排列结构体内部成员的时候,并不一定是按照#pragma pack(n)中的n指定长度为单位的,还取决于结构体内部占用空间最大的类型的长度。这么做可能是为了节省空间。例如:
struct // packing is 8
{
int a;
char b;
} sa;
在排列成员int a和char b的时候是以4个字节为单位的。因为此时sizeof(int)=4<8,所以排列的时候以4为单位。但是当packing值小于4的时候则以packing值为单位排列。所以:
struct // packing is 8
{
int a;
int c;
char b;
} sa;
输出结果为:sizeof(sa)=12;(以4个字节为单位)
当packing值小于结构体内占用空间最多成员占用的字节数的时候,以packing值为准。
#pragma pack(push,1) // packing is now 1
struct
{
int a;
char b;
double c;
} sb;
输出结果为:sizeof(sb)=13;(以1个字节为单位)
C++Builder可以在Options对话框中修改Advanced compiler页中的Data alignment为按字节对齐。
附:MSDN中关于pack的说明
#pragma pack( show | [ n] )
Specifies packing alignment for structure, union, and class members. Whereas the packing alignment of structures, unions, and classes is set for an entire translation unit by the /Zp option, the packing alignment is set at the data-declaration level by the pack pragma. The pragma takes effect at the first structure, union, or class declaration after the pragma is seen; the pragma has no effect on definitions.
When you use #pragma pack(n), where n is 1, 2, 4, 8, or 16, each structure member after the first is stored on the smaller member type or n-byte boundaries. If you use #pragma pack without an argument, structure members are packed to the value specified by /Zp. The default /Zp packing size is /Zp8.
The show option causes the C4810 warning to issue, displaying the current pack value.
#pragma pack(show)
#pragma pack(4)
#pragma pack(show)
void main(){}
The compiler also supports the following enhanced syntax:
#pragma pack( [ [ { push | pop}, ] [ identifier, ] ] [ n ] )
This syntax allows you to combine program components into a single translation unit if the different components use pack pragmas to specify different packing alignments.
Each occurrence of a pack pragma with a push argument stores the current packing alignment on an internal compiler stack. The pragma’s argument list is read from left to right. If you use push, the current packing value is stored. If you provide a value for n, that value becomes the new packing value. If you specify an identifier, a name of your choosing, the identifier is associated with the new packing value.
Each occurrence of a pack pragma with a pop argument retrieves the value at the top of an internal compiler stack and makes that value the new packing alignment. If you use pop and the internal compiler stack is empty, the alignment value is that set from the command-line and a warning is issued. If you use pop and specify a value for n, that value becomes the new packing value. If you use pop and specify an identifier, all values stored on the stack are removed from the stack until a matching identifier is found. The packing value associated with the identifier is also removed from the stack and the packing value that existed just before the identifier was pushed becomes the new packing value. If no matching identifier is found, the packing value set from the command line is used and a level-one warning is issued. The default packing alignment is 8.
The new, enhanced functionality of the pack pragma allows you to write header files that ensure that packing values are the same before and after the header file is encountered:
/* File name: include1.h
*/
#pragma pack( push, enter_include1 )
/* Your include-file code ... */
#pragma pack( pop, enter_include1 )
/* End of include1.h */
In the previous example, the current pack value is associated with the identifier enter_include1 and pushed, remembered, on entry to the header file. The pack pragma at the end of the header file removes all intervening pack values that may have occurred in the header file and removes the pack value associated with enter_include1. The header file thus ensures that the pack value is the same before and after the header file.
The new functionality also allows you to use code, such as header files, that uses pack pragmas to set packing alignments that differ from the packing value set in your code:
#pragma pack( push, before_include1 )
#include "include1.h"
#pragma pack( pop, before_include1 )
In the previous example, your code is protected from any changes to the packing value that might occur in include.h.