在结构中,编译器为结构的每个成员按其自然对界(alignment)条件分配空间;各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同。在缺省情况下,C编译器为每一个变量或是数据单元按其自然对界条件分配空间。
例如,下面的结构各成员空间分配情况。
struct tagTest {
char x1;
short x2;
float x3;
char x4;
};
结构的第一个成员x1,其偏移地址为0,占据了第1个字节。第二个成员x2为short类型,其起始地址必须2字节对界,因此,编译器在x2和x1之间填充了一个空字节。结构的第三个成员x3和第四个成员x4恰好落在其自然对界地址上,在它们前面不需要额外的填充字节。在test结构中,成员x3要求4字节对界,是该结构所有成员中要求的最大对界单元,因而test结构的自然对界条件为4字节,编译器在成员x4后面填充了3个空字节。整个结构所占据空间为12字节。
现在你知道怎么回事了吧?
更改C编译器的缺省分配策略
一般地,可以通过下面的方法改变缺省的对界条件:
1. 使用伪指令#pragma pack ([n])
#pragma pack ([n])伪指令允许你选择编译器为数据分配空间所采取的对界策略。
例如,在使用了#pragma pack (1)伪指令后,test结构各成员的空间分配情况就是按照一个字节对齐了,格式如下:
#pragma pack(push) //保存对齐状态
#pragma pack(1)
//定义你的结构
//…………
#pragma pack(pop)
在Visual stdio开发环境中,编译选项中可以设置:菜单中“工程”-->“设置”, 弹出一个“project setting”对话框。然后选择“C/C++”标签--->在“Y分类”中选择“Code Generation”--->在“Struct member alignment”中选择“1 Byte”。
重新编译。ok了!!
如:
#pragma pack(push,1)
typedef struct tagSocketData {
BYTE nSize;
BYTE nType;
DWORD nDataSize;
DWORD nIndex;
SOCKET socket;
DWORD nDataLength;
} SOCKETDATA, *LPSOCKETDATA;
#progma pack(pop,1)
就可以了~
#include <iostream.h>
#pragma pack(8)
struct example1
{
short a;
long b;
};
struct example2
{
char c;
example1 struct1;
short e;
};
#pragma pack()
int main(int argc, char* argv[])
{
example2 struct2;
cout << sizeof(example1) << endl;
cout << sizeof(example2) << endl;
cout << (unsigned int)(&struct2.struct1) - (unsigned int)(&struct2)
<< endl;
return 0;
}
//问程序的输入结果是什么?答案是:
8
16
4
//不明白?还是不明白?下面一一道来:
2.1 自然对界
struct是一种复合数据类型,其构成元素既可以是基本数据类型(如int、long、float等)的变量,也可以是一些复合数据类型(如array、struct、union等)的数据单元。对于结构体,编译器会自动进行成员变量的对齐,以提高运算效率。缺省情况下,编译器为结构体的每个成员按其自然对界(natural alignment)条件分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同。
自然对界(natural alignment)即默认对齐方式,是指按结构体的成员中size最大的成员对齐。
例如:
struct naturalalign
{
char a;
short b;
char c;
};
在上述结构体中,size最大的是short,其长度为2字节,因而结构体中的char成员a、c都以2为单位对齐,sizeof(naturalalign)的结果等于6;
如果改为:
struct naturalalign
{
char a;
int b;
char c;
};
其结果显然为12。
2.2指定对界
一般地,可以通过下面的方法来改变缺省的对界条件:
使用伪指令#pragma pack (n),编译器将按照n个字节对齐;
使用伪指令#pragma pack (),取消自定义字节对齐方式。
注意:如果#pragma pack (n)中指定的n大于结构体中最大成员的size,则其不起作用,结构体仍然按照size最大的成员进行对界。
例如:
#pragma pack (n)
struct naturalalign
{
char a;
int b;
char c;
};
#pragma pack ()
当n为4、8、16时,其对齐方式均一样,sizeof(naturalalign)的结果都等于12。而当n为2时,其发挥了作用,使得sizeof(naturalalign)的结果为8。
在VC++ 6.0编译器中,我们可以指定其对界方式, 其操作方式为依次选择projetct > setting > C/C++菜单,在struct member alignment中指定你要的对界方式。
另外,通过__attribute((aligned (n)))也可以让所作用的结构体成员对齐在n字节边界上,但是它较少被使用,因而不作详细讲解。
2.3 面试题的解答
至此,我们可以对Intel、微软的面试题进行全面的解答。
程序中第2行#pragma pack (8)虽然指定了对界为8,但是由于struct example1中的成员最大size为4(long变量size为4),故struct example1仍然按4字节对界,struct example1的size为8,即第18行的输出结果;
struct example2中包含了struct example1,其本身包含的简单数据成员的最大size为2(short变量e),但是因为其包含了struct example1,而struct example1中的最大成员size为4,struct example2也应以4对界,#pragma pack (8)中指定的对界对struct example2也不起作用,故19行的输出结果为16;
由于struct example2中的成员以4为单位对界,故其char变量c后应补充3个空,其后才是成员struct1的内存空间,20行的输出结果为4。
struct Size2{
char c1;
int i1; //占4个字节
float f;
double d1; //占8个字节
};
上边的好像有点问题,这个sizeof()的结果是24;
目前可以这样算,1+4+4+8 = 17;求大于17的8的最小倍数,那么就是24了。
(看了下边的,知道怎么回事了,上边我说的错了)
struct Size2{
char c1; //下一个大小为4,偏移应该可以整除4,所以会填充3个字节
int i1; //下一个大小为4,上边的4加上int的大小4,所以这个偏移可以。占4
float f; //下一个大小为8,当前正常偏移为12,不可以整除8,所以填充4位,偏移为16
double d1; //占8个字节
}; //最后整体偏移为24,可以整除8。
下边这个讲的比较好
1 从union的sizeof问题看cpu的对界
考虑下面问题:(默认对齐方式)
union u
{
double a;
int b;
};
union u2
{
char a[13];
int b;
};
union u3
{
char a[13];
char b;
};
都知道union的大小取决于它所有的成员中,占用空间最大的一个成员的大小。所以对于u来说,
大小就是最大的double类型成员a了,所以sizeof(u)=sizeof(double)=8。但是对于u2和u3,最
大的空间都是char[13]类型的数组,为什么u3的大小是13,而u2是16呢?关键在于u2中的成员
int b。由于int类型成员的存在,使u2的对齐方式变成4,也就是说,u2的大小必须在4的对界
上,所以占用的空间变成了16(最接近13的对界)。
结论:复合数据类型,如union,struct,class的对齐方式为成员中对齐方式最大的成员的对
齐方式.顺便提一下CPU对界问题,32的C++采用8位对界来提高运行速度,所以编译器会尽量把
数据放在它的对界上以提高内存命中率。对界是可以更改的,使用#pragma pack(x)宏可以改
变编译器的对界方式,默认是8。C++固有类型的对界取编译器对界方式与自身大小中较小的一
个。例如,指定编译器按2对界,int类型的大小是4,则int的对界为2和4中较小的2。在默认的
对界方式下,因为几乎所有的数据类型都不大于默认的对界方式8(除了long double),所以
所有的固有类型的对界方式可以认为就是类型自身的大小。更改一下上面的程序:
#pragma pack(2)
union u2
{
char a[13];
int b;
};
union u3
{
char a[13];
char b;
};
#pragma pack(8)
由于手动更改对界方式为2,所以int的对界也变成了2,u2的对界取成员中最大的对界,也是2了,所以此时sizeof(u2)=14。
结论:C++固有类型的对界取编译器对界方式与自身大小中较小的一个。
9、struct的sizeof问题
因为对齐问题使结构体的sizeof变得比较复杂,看下面的例子:(默认对齐方式下)
struct s1
{
char a;
double b;
int c;
char d;
};
struct s2
{
char a;
char b;
int c;
double d;
};
同样是两个char类型,一个int类型,一个double类型,但是因为对界问题,导致他们的大小不同。
计算结构体大小可以采用元素摆放法,我举例子说明一下:首先,CPU判断结构体的对界,根据上
一节的结论,s1和s2的对界都取最大的元素类型,也就是double类型的对界8。然后开始摆放每个
元素。
对于s1,首先把a放到8的对界,假定是0,此时下一个空闲的地址是1,但是下一个元素d是double
类型,要放到8的对界上,离1最接近的地址是8了,所以d被放在了8,此时下一个空闲地址变成了
16,下一个元素c的对界是4,16可以满足,所以c放在了16,此时下一个空闲地址变成了20,下一
个元素d需要对界1,也正好落在对界上,所以d放在了20,结构体在地址21处结束。由于s1的大小
需要是8的倍数,所以21-23的空间被保留,s1的大小变成了24。
对于s2,首先把a放到8的对界,假定是0,此时下一个空闲地址是1,下一个元素的对界也是1,所
以b摆放在1,下一个空闲地址变成了2;下一个元素c的对界是4,所以取离2最近的地址4摆放c,
下一个空闲地址变成了8,下一个元素d的对界是8,所以d摆放在8,所有元素摆放完毕,结构体在
15处结束,占用总空间为16,正好是8的倍数。
这里有个陷阱,对于结构体中的结构体成员,不要认为它的对齐方式就是他的大小,看下面的例子:
struct s1
{
char a[8];
};
struct s2
{
double d;
};
struct s3
{
s1 s;
char a;
};
struct s4
{
s2 s;
char a;
};
s1和s2大小虽然都是8,但是s1的对齐方式是1,s2是8(double),所以在s3和s4中才有这样的差异。
所以,在自己定义结构体的时候,如果空间紧张的话,最好考虑对齐因素来排列结构体里的元素。
10、不要让double干扰你的位域
在结构体和类中,可以使用位域来规定某个成员所能占用的空间,所以使用位域能在一定程度上节
省结构体占用的空间。不过考虑下面的代码:
struct s1
{
int i: 8;
int j: 4;
double b;
int a:3;
};
struct s2
{
int i;
int j;
double b;
int a;
};
struct s3
{
int i;
int j;
int a;
double b;
};
struct s4
{
int i: 8;
int j: 4;
int a:3;
double b;
};
可以看到,有double存在会干涉到位域(sizeof的算法参考上一节),所以使用位域的的时候,最好
把float类型和double类型放在程序的开始或者最后。
第一次写东西,发现自己的表达能力太差了,知道的东西讲不出来,讲出来的东西别人也看不懂,呵呵
。另外,C99标准的sizeof已经可以工作在运行时了,打算最近找个支持C99的编译器研究一下。
首先拉来看个例子:
struct test
{
char a;
int b;
};
int main()
{
int nSize = sizeof(test);
return 0;
}
在使用vc编译器默认设置下得到的nsize是8,而不是5.为什么要这样呢?
原来在cpu寻址是是8的倍数的地址是最容易找到的,所以在vc默认编译情况下,都会把结构体按8字节对齐,这样就可以提高了编译出来效率。
vc在project->setting->c/c++中的struct member alignment中可以设置结构体补齐方式,默认情况下是8,如果改成1的话,上面的sizeof得到的结果就是5了。
那么现在我们还是用默认的8字节方式,把int b改成short b,得到的结果是多少呢?哈,是4.
原来struct的对齐机制里面还有另外一个规则就是首先计算结构体内所有基础类型的长度,最大的长度如果小于vc设置的对齐长度的话,就按最大长度作为结构体的对齐方式。如上面改成short以后,结构体内最大对齐长度是2(short),所以最终的结构体是按2字节来补齐的,而不是默认的8字节。
typedef struct tagSocketData {
BYTE nSize;
BYTE nType;
DWORD nDataSize;
DWORD nIndex;
SOCKET socket;
DWORD nDataLength;
} SOCKETDATA, *LPSOCKETDATA;
SOCKETDATA sd;
sd.nSize = sizeof(SOCKETDATA); //本来应该是18, 可却是20
sizeof(SOCKETDATA) = 20, 本来是18字节的大小
实际大小却是20
可存为文件的时候却是按照18字节的大小存储