一个结构体变量定义完之后,其在内存中的存储并不等于其所包含元素的宽度之和。
例一:
#include
using namespace std;
struct X
{
char
a;
int b;
double
c;
}S1;
void
main()
{
cout << sizeof(S1) <<
endl;
cout << sizeof(S1.a) <<
endl;
cout << sizeof(S1.b) << endl;
cout << sizeof(S1.c) <<
endl;
}
比如例一中的结构体变量S1定义之后,经测试,会发现sizeof(S1)= 16,其值不等于sizeof(S1.a) =
1、sizeof(S1.b) = 4和 sizeof(S1.c) = 8三者之和,这里面就存在存储对齐问题。
原则一:结构体中元素是按照定义顺序一个一个放到内存中去的,但并不是紧密排列的。从结构体存储的首地址开始,每一个元素放置到内存中时,它都会认为内存是以它自己的大小来划分的,因此元素放置的位置一定会在自己宽度的整数倍上开始(以结构体变量首地址为0计算)。
比如此例,首先系统会将字符型变量a存入第0个字节(相对地址,指内存开辟的首地址);然后在存放整形变量b时,会以4个字节为单位进行存储,由于第一个四字节模块已有数据,因此它会存入第二个四字节模块,也就是存入到4~8字节;同理,存放双精度实型变量c时,由于其宽度为8,其存放时会以8个字节为单位存储,也就是会找到第一个空的且是8的整数倍的位置开始存储,此例中,此例中,由于头一个8字节模块已被占用,所以将c存入第二个8字节模块。整体存储示意图如图1所示。
考虑另外一个实例。
例二:
struct X
{
char a;
double b;
int c;
}S2;
在例二中仅仅是将double型的变量和int型的变量互换了位置。测试程序不变,测试结果却截然不同,sizeof(S2)=24,不同于我们按照原则一计算出的8+8+4=20,这就引出了我们的第二原则。
原则二:在经过第一原则分析后,检查计算出的存储单元是否为所有元素中最宽的元素的长度的整数倍,是,则结束;若不是,则补齐为它的整数倍。
例二中,我们分析完后的存储长度为20字节,不是最宽元素长度8的整数倍,因此将它补齐到8的整数倍,也就是24。这样就没问题了。其存储示意图如图2所示。
掌握了这两个原则,就能够分析所有数据存储对齐问题了。再来看几个例子,应用以上两个原则来判断。
例三:
struct X
{
double a;
char
b;
int
c;
}S3;
首先根据原则一来分析。按照定义的顺序,先存储double型的a,存储在第0~7个字节;其次是char型的b,存储在第8个字节;接下来是int型的c,顺序检查后发现前面三个四字节模块都被占用,因此存储在第4个四字节模块,也就是第12~15字节。按照第一原则分析得到16个字节,16正好是最宽元素a的宽度8的整数倍,因此结构体变量S3所占存储空间就是16个字节。存储结构如图3所示。
例四:
struct X
{
double a;
char b;
int
c;
char
d;
}S4;
仍然首先按照第一原则分析,得到的字节数为8+4+4+1=17;再按照第二原则补齐,则结构体变量S4所占存储空间为24。存储结构如图4所示:
例五:
struct X
{
double a;
char b;
int c;
char d;
int e;
}S5;
同样结合原则一和原则二分析,可知在S4的基础上在结构体内部变量定义最后加入一个int型变量后,结构体所占空间并未增加,仍为24。存储结构示意图如图5所示。
例六:
如果将例五中加入的变量e放到第一个定义的位置,则情况就不同了。结构体所占存储空间会变为32。其存储结构示意图如图6所示。
struct X
{
int e;
double a;
char
b;
int c;
char
d;
}S6;
补充:前面所介绍的都是元素为基本数据类型的结构体,那么含有指针、数组或是其它结构体变量或联合体变量时该如何呢?
1.包含指针类型的情况。只要记住指针本身所占的存储空间是4个字节就行了,而不必看它是指向什么类型的指针。
例七:
struct
X
struct
Y
struct Z
{ { {
char
*a;
int
*b; double
*c;
};
}; };
经测试,可知sizeof(X)、sizeof(Y)和sizeof(Z)的值都为4。
2.含有构造数据类型(数组、结构体和联合体)的情况。首先要明确的是计算存储空间时要把构造体看作一个整体来为其开辟存储空间;其次要明确的是在最后补齐时是按照所有元素中的基本数据类型元素的最长宽度来补齐的,也就是说虽然要把构造体看作整体,但在补齐的时候并不会按照所含结构体所占存储空间的长度来补齐的(即使它可能是最长的)。
例八:
struct X
{
char
a;
int
b;
double c;
};
struct Y
{
char a;
X
b;
};
经测试,可知sizeof(X)为16,sizeof(Y)为24。即计算Y的存储长度时,在存放第二个元素b时的初始位置是在double型的长度8的整数倍处,而非16的整数倍处,即系统为b所分配的存储空间是第8~23个字节。
如果将Y的两个元素char型的a和X型的b调换定义顺序,则系统为b分配的存储位置是第0~15个字节,为a分配的是第16个字节,加起来一共17个字节,不是最长基本类型double所占宽度8的整数倍,因此要补齐到8的整数倍,即24。测试后可得sizeof(Y)的值为24。
由于结构体所占空间与其内部元素的类型有关,而且与不同类型元素的排列有关,因此在定义结构体时,在元素类型及数量确定之后,我们还应该注意一下其内部元素的定义顺序。
Q:关于结构体的对齐,到底遵循什么原则?
A:首先先不讨论结构体按多少字节对齐,先看看只以1字节对齐的情况:
复制代码
#include
#include
#define
PRINT_D(intValue)
printf(#intValue" is %d\n", (intValue));
#define
OFFSET(struct,member) ((char *)&((struct
*)0)->member - (char *)0)
#pragma
pack(1)
typedef
struct
{
char
sex;
short
score;
int
age;
}student;
int
main()
{
PRINT_D(sizeof(student))
PRINT_D(OFFSET(student,sex))
PRINT_D(OFFSET(student,score))
PRINT_D(OFFSET(student,age))
return 0;
}
复制代码
输出:
sizeof(student) is
7
OFFSET(student,sex)
is 0
OFFSET(student,score) is 1
OFFSET(student,age)
is 3
可以看到,如果按1字节对齐,那么结构体内部的成员紧密排列,sizeof(char) ==
1, sizeof(short) == 2, sizeof(int) == 4.
修改上面的代码, 去掉#pragma
pack语句,代码如下:
复制代码
#include
#include
#define
PRINT_D(intValue)
printf(#intValue" is %d\n", (intValue));
#define
OFFSET(struct,member) ((char *)&((struct
*)0)->member - (char *)0)
typedef
struct
{
char
sex;
short
score;
int
age;
}student;
int
main()
{
PRINT_D(sizeof(student))
PRINT_D(OFFSET(student,sex))
PRINT_D(OFFSET(student,score))
PRINT_D(OFFSET(student,age))
return 0;
}
复制代码
运行结果:
sizeof(student) is
8
OFFSET(student,sex)
is 0
OFFSET(student,score) is 2
OFFSET(student,age)
is 4
此时,各个成员之间就不像之前那样紧密排列了,而是有一些缝隙。这里需要介绍下对齐原则:
此原则是在没有#pragma
pack语句作用时的原则(不同平台可能会有不同):
原则A:struct或者union的成员,第一个成员在偏移0的位置,之后的每个成员的起始位置必须是当前成员大小的整数倍;
原则B:如果结构体A含有结构体成员B,那么B的起始位置必须是B中最大元素大小整数倍地址;
原则C:结构体的总大小,必须是内部最大成员的整数倍;
依据上面3个原则,我们来具体分析下:
sex在偏移0处,占1字节;score是short类型,占2字节,score必须以2的整数倍为起始位置,所以它的起始位置为2;
age为int类型,大小为4字节,它必须以4的整数倍为起始位置,因为前面有sex占1字节,填充的1字节和score占2字节,地址4已经是4的整数倍,所以age的位置为4.最后,总大小为4的倍数,不用继续填充。
继续修改上面的代码,增加#pragma
pack语句:
复制代码
#include
#include
#define
PRINT_D(intValue)
printf(#intValue" is %d\n", (intValue));
#define
OFFSET(struct, member) ((char *)&((struct
*)0)->member - (char *)0)
#pragma
pack(4)
typedef
struct
{
char
sex;
short
score;
int
age;
}student;
int
main()
{
PRINT_D(sizeof(student))
PRINT_D(OFFSET(student,sex))
PRINT_D(OFFSET(student,score))
PRINT_D(OFFSET(student,age))
return 0;
}
复制代码
运行结果:
sizeof(student) is
8
OFFSET(student,sex)
is 0
OFFSET(student,score) is 2
OFFSET(student,age)
is 4
具体分析下:
有了#pragma
pack(4)语句后,之前说的原则A和C就不适用了。实际对齐原则是自身对齐值(成员sizeof大小)和指定对齐值(#pragma
pack指定的对齐大小)的较小者。依次原则,sex依然偏移为0, 自身对齐值为1,指定对齐值为4,所以实际对齐为1;
score成员自身对齐值为2,指定对齐值为4,实际对齐为2;所以前面的sex后面将填充一个1字节,然后是score的位置,它的偏移为2;age自身对齐值为4,指定对齐为4,所以实际对齐值为4;前面的sex和score正好占用4字节,所以age接着存放;它的偏移为4.
Q:关于位域的问题,空域到底表示什么?
A:它表示之后的位域从新空间开始。
复制代码
#include
#include
#define
PRINT_D(intValue)
printf(#intValue" is %d\n", (intValue));
#define
OFFSET(struct, member) ((char *)&((struct
*)0)->member - (char *)0)
typedef
struct
{
int a : 1;
int b : 3;
int : 0;
int d : 2;
}bit_info;
int
main()
{
PRINT_D(sizeof(bit_info))
return 0;
}
复制代码
运行结果:
sizeof(bit_info) is
8
bit_info中的a,
b占用4个字节的前4位,到int:0; 时表示此时将填充余下所有没有填充的位,即刚刚的4个字节的余下28位;int d:2;
将从第四个字节开始填充,又会占用4个字节,所以总大小为8.
看一下这两个指令是如何影响变量在内存的存储的。
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
using namespace std;
struct A
{
int n;
char c;
short s;
};
struct B
{
char c;
int n;
short s;
};
#pragma pack()
int main(int argc, char* 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
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的大小留待大家练练手。
///////////////////////////////////////////////
///////////////////////////////////////////////
http://ooclewice.spaces.live.com/blog/cns!28DE78078A5A20F3!149.entry
#pragma pack
结构体的sizeof
一个结构体:
struct S1
{
char c;
int i;
};
问sizeof(s1)等于多少?理论上,char占1个字节,int占4个字节,那么加起来就应该是5。实际了,在机器上试试看,vC6中按默认设置得到的结果为8。
琢磨一下sizeof的定义——sizeof的结果等于对象或者类型所占的内存字节数,好吧,那就让我们来看看S1的内存分配情况:
S1 s1 = { 'a', 0xFFFFFFFF };
定义上面的变量后,加上断点,运行程序,观察s1所在的内存,以VC6.0为例,其数据内容如下:
61 CC CC CC FF FF FF FF
发现了什么?怎么中间夹杂了3个字节的CC?看看MSDN上的说明:
When applied to a structure type or variable, sizeof returns the
actual size, which may include padding bytes inserted for
alignment.
原来如此,这就是传说中的字节对齐啊!一个重要的话题出现了。
为什么需要字节对齐?计算机组成原理教导我们这样有助于加快计算机的取数速度,否则就得多花指令周期了。为此,编译器默认会对结构体进行处理(实际上其它地方的数据变量也是如此),让宽度为2的基本数据类型(short等)都位于能被2整除的地址上,让宽度为4的基本数据类型(int等)都位于能被4整除的地址上,以此类推。这样,两个数中间就可能需要加入填充字节,所以整个结构体的sizeof值就增长了。
让我们交换一下S1中char与int的位置:
struct S2
{
int i;
char c;
};
看看sizeof(S2)的结果为多少,怎么还是8?再看看内存,原来成员c后面仍然有3个填充字节,这又是为什么啊?别着急,下面总结规律。
字节对齐的细节和编译器实现相关,但一般而言,满足三个准则:
1) 结构体变量的首地址能够被其最宽基本类型成员的大小所整除;
2)
结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal
adding);
3) 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节(trailing
padding)。
对于上面的准则,有几点需要说明:
1)
前面不是说结构体成员的地址是其大小的整数倍,怎么又说到偏移量了呢?因为有了第1点存在,所以我们就可以只考虑成员的偏移量,这样思考起来简单。想想为什么。
结构体某个成员相对于结构体首地址的偏移量可以通过宏offsetof()来获得,这个宏也在stddef.h中定义,如下:
#define offsetof(s,m) (size_t)&(((s *)0)->m)
例如,想要获得S2中c的偏移量,方法为
size_t pos = offsetof(S2, c);// pos等于4
2)
基本类型是指前面提到的像char、short、int、float、double这样的内置数据类型,这里所说的“数据宽度”就是指其sizeof的大小。由于结构体的成员可以是复合类型,比如另外一个结构体,所以在寻找最宽基本类型成员时,应当包括复合类型成员的子成员,而不是把复合成员看成是一个整体。但在确定复合类型成员的偏移位置时则是将复合类型作为整体看待。
这里叙述起来有点拗口,思考起来也有点挠头,还是让我们看看例子吧(具体数值仍以VC6为例,以后不再说明):
struct S3
{
char c1;
S1 s;
char c2
};
S1的最宽简单成员的类型为int,S3在考虑最宽简单类型成员时是将S1“打散”看的,所以S3的最宽简单类型为int,这样,通过S3定义的变量,其存储空间首地址需要被4整除,整个sizeof(S3)的值也应该被4整除。
c1的偏移量为0,s的偏移量呢?这时s是一个整体,它作为结构体变量也满足前面三个准则,所以其大小为8,偏移量为4,c1与s之间便需要3个填充字节,而c2与s之间就不需要了,所以c2的偏移量为12,算上c2的大小为13,13是不能被4整除的,这样末尾还得补上3个填充字节。最后得到sizeof(S3)的值为16。
通过上面的叙述,我们可以得到一个公式:
结构体的大小等于最后一个成员的偏移量加上其大小再加上末尾的填充字节数目,即:
sizeof( struct ) = offsetof( last item ) + sizeof( last item ) +
sizeof( trailing padding )
到这里,朋友们应该对结构体的sizeof有了一个全新的认识,但不要高兴得太早,有一个影响sizeof的重要参量还未被提及,那便是编译器的pack指令。它是用来调整结构体对齐方式的,不同编译器名称和用法略有不同,VC6中通过#pragma
pack实现,也可以直接修改/Zp编译开关。#pragma pack的基本用法为:#pragma pack( n
),n为字节对齐数,其取值为1、2、4、8、16,默认是8,如果这个值比结构体成员的sizeof值小,那么该成员的偏移量应该以此值为准,即是说,结构体成员的偏移量应该取二者的最小值,公式如下:
offsetof( item ) = min( n, sizeof( item ) )
再看示例:
#pragma pack(push) // 将当前pack设置压栈保存
#pragma pack(2)// 必须在结构体定义之前使用
//这两句也可写成一句 #pragma pack(push,2)
struct S1
{
char c;
int i;
};
struct S3
{
char c1;
S1 s;
char c2
};
#pragma pack(pop) // 恢复先前的pack设置
计算sizeof(S1)时,min(2,
sizeof(i))的值为2,所以i的偏移量为2,加上sizeof(i)等于6,能够被2整除,所以整个S1的大小为6。
同样,对于sizeof(S3),s的偏移量为2,c2的偏移量为8,加上sizeof(c2)等于9,不能被2整除,添加一个填充字节,所以sizeof(S3)等于10。
还有一点要注意,“空结构体”(不含数据成员)的大小不为0,而是1。试想一个“不占空间”的变量如何被取地址、两个不同的“空结构体”变量又如何得以区分呢?于是,“空结构体”变量也得被存储,这样编译器也就只能为其分配一个字节的空间用于占位了。如下:
struct S5 { };
sizeof( S5 ); // 结果为1
///////////////////////////////////////////////
///////////////////////////////////////////////
关于pragma pack的用法 C++中的内存对齐问题
2009-02-19 12:36
首先请大家先看下面代码:
typedef
struct
{
UINT32
NumElements;
union
{
UINT32
ObjectHandle;
}Entry;
}STR_ARRAY,
*PSTR_ARRAY;
还有这两句#pragma
pack(push, 1)
#pragma
pack(pop)
#pragma
pack( [ n ] )
该指令指定结构和联合成员的紧凑对齐。而一个完整的转换单元的结构和联合的紧凑对齐由/
Z p 选项设置。紧凑对齐用p a c e
编译指示在数据说明层设置。该编译指示在其出现后的第一个结构或联合说明处生效。该编译指示对定义无效。当你使用#pragma pack (
n ) 时, 这里n 为1 、2 、4 、8 或1 6 。第一个结构成员之后的每个结构成员都被存储在更小的成员类型或n
字节界限内。如果你使用无参量的#pragma pack , 结构成员被紧凑为以/ Z p 指定的值。该缺省/ Z p 紧凑值为/ Z
p 8 。
编译器也支持以下增强型语法:
#pragma
pack( [ [ { p u s h | p o p } , ] [ 标识符, ] ] [ n] )若不同的组件使用p a c k
编译指示指定不同的紧凑对齐, 这个语法允许你把程序组件组合为一个单独的转换单元。带p u s h 参量的p a c k
编译指示的每次出现将当前的紧凑对齐存储到一个内部编译器堆栈中。编译指示的参量表从左到右读取。如果你使用p u s h ,
则当前紧凑值被存储起来; 如果你给出一个n 的值, 该值将成为新的紧凑值。若你指定一个
标识符, 即你选定一个名称, 则该标识符将和这个新的的紧凑值联系起来。带一个p o p 参量的p a c k
编译指示的每次出现都会检索内部编译器堆栈顶的值,并且使该值为新的紧凑对齐值。如果你使用p o p
参量且内部编译器堆栈是空的,则紧凑值为命令行给定的值, 并且将产生一个警告信息。若你使用p o p 且指定一
个n 的值, 该值将成为新的紧凑值。若你使用p o p 且指定一个标识符, 所有存储在堆栈中的值将从栈中删除,
直到找到一个匹配的标识符, 这个与标识符相关的紧凑值也从栈中移出,
并且这个仅在标识符入栈之前存在的紧凑值成为新的紧凑值。如果未找到匹配的标识符, 将使用命令行设置的紧凑值,
并且将产生一个一级警告。缺省紧凑对齐为8 。p a c k 编译指示的新的增强功能让你编写头文件,
确保在遇到该头文件的前后的紧凑值是一样的。
什么是内存对齐
考虑下面的结构:
struct
foo
{
char
c1;
short
s;
char
c2;
int
i;
};
假设这个结构的成员在内存中是紧凑排列的,假设c1的地址是0,那么s的地址就应该是1,c2的地址就是3,i的地址就是4。也就是
c1
00000000, s 00000001, c2 00000003, i 00000004。
可是,我们在Visual
c/c++ 6中写一个简单的程序:
struct
foo a;
printf("c1
%p, s %p, c2 %p, i %p\n",
(unsigned
int)(void*)&a.c1 - (unsigned int)(void*)&a,
(unsigned
int)(void*)&a.s - (unsigned int)(void*)&a,
(unsigned
int)(void*)&a.c2 - (unsigned int)(void*)&a,
(unsigned
int)(void*)&a.i - (unsigned int)(void*)&a);
运行,输出:
c1
00000000, s 00000002, c2 00000004, i 00000008。
为什么会这样?这就是内存对齐而导致的问题。
为什么会有内存对齐
以下内容节选自《Intel
Architecture 32 Manual》。
字,双字,和四字在自然边界上不需要在内存中对齐。(对字,双字,和四字来说,自然边界分别是偶数地址,可以被4整除的地址,和可以被8整除的地址。)
无论如何,为了提高程序的性能,数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;然而,对齐的内存访问仅需要一次访问。
一个字或双字操作数跨越了4字节边界,或者一个四字操作数跨越了8字节边界,被认为是未对齐的,从而需要两次总线周期来访问内存。一个字起始地址是奇数但却没有跨越字边界被认为是对齐的,能够在一个总线周期中被访问。
某些操作双四字的指令需要内存操作数在自然边界上对齐。如果操作数没有对齐,这些指令将会产生一个通用保护异常(#GP)。双四字的自然边界是能够被16
整除的地址。其他的操作双四字的指令允许未对齐的访问(不会产生通用保护异常),然而,需要额外的内存总线周期来访问内存中未对齐的数据。
编译器对内存对齐的处理
缺省情况下,c/c++编译器默认将结构、栈中的成员数据进行内存对齐。因此,上面的程序输出就变成了:
c1 00000000, s 00000002, c2 00000004, i 00000008。
编译器将未对齐的成员向后移,将每一个都成员对齐到自然边界上,从而也导致了整个结构的尺寸变大。尽管会牺牲一点空间(成员之间有空洞),但提高了性能。
也正是这个原因,我们不可以断言sizeof(foo) == 8。在这个例子中,sizeof(foo) == 12。
如何避免内存对齐的影响
那么,能不能既达到提高性能的目的,又能节约一点空间呢?有一点小技巧可以使用。比如我们可以将上面的结构改成:
struct
bar
{
char
c1;
char
c2;
short
s;
int
i;
};
这样一来,每个成员都对齐在其自然边界上,从而避免了编译器自动对齐。在这个例子中,sizeof(bar)
== 8。
这个技巧有一个重要的作用,尤其是这个结构作为API的一部分提供给第三方开发使用的时候。第三方开发者可能将编译器的默认对齐选项改变,从而造成这个结构在你的发行的DLL中使用某种对齐方式,而在第三方开发者哪里却使用另外一种对齐方式。这将会导致重大问题。
比如,foo结构,我们的DLL使用默认对齐选项,对齐为
c1 00000000, s 00000002, c2 00000004, i 00000008,同时sizeof(foo) ==
12。
而第三方将对齐选项关闭,导致
c1
00000000, s 00000001, c2 00000003, i 00000004,同时sizeof(foo) ==
8。
如何使用c/c++中的对齐选项
vc6中的编译选项有
/Zp[1|2|4|8|16]
,/Zp1表示以1字节边界对齐,相应的,/Zpn表示以n字节边界对齐。n字节边界对齐的意思是说,一个成员的地址必须安排在成员的尺寸的整数倍地址上或者是n的整数倍地址上,取它们中的最小值。也就是:
min
( sizeof ( member ), n)
实际上,1字节边界对齐也就表示了结构成员之间没有空洞。
/Zpn选项是应用于整个工程的,影响所有的参与编译的结构。
要使用这个选项,可以在vc6中打开工程属性页,c/c++页,选择Code
Generation分类,在Struct member alignment可以选择。
要专门针对某些结构定义使用对齐选项,可以使用#pragma
pack编译指令。指令语法如下:
#pragma pack( [ show ] | [ push | pop ] [, identifier ] , n )
意义和/Zpn选项相同。比如:
#pragma
pack(1)
struct
foo_pack
{
char
c1;
short
s;
char
c2;
int
i;
};
#pragma
pack()
栈内存对齐
我们可以观察到,在vc6中栈的对齐方式不受结构成员对齐选项的影响。(本来就是两码事)。它总是保持对齐,而且对齐在4字节边界上。
验证代码
#include
struct
foo
{
char
c1;
short
s;
char
c2;
int
i;
};
struct
bar
{
char
c1;
char
c2;
short
s;
int
i;
};
#pragma
pack(1)
struct
foo_pack
{
char
c1;
short
s;
char
c2;
int
i;
};
#pragma
pack()
int
main(int argc, char* argv[])
{
char
c1;
short
s;
char
c2;
int
i;
struct
foo a;
struct
bar b;
struct
foo_pack p;
printf("stack
c1 %p, s %p, c2 %p, i %p\n",
(unsigned
int)(void*)&c1 - (unsigned int)(void*)&i,
(unsigned
int)(void*)&s - (unsigned int)(void*)&i,
(unsigned
int)(void*)&c2 - (unsigned int)(void*)&i,
(unsigned
int)(void*)&i - (unsigned int)(void*)&i);
printf("struct
foo c1 %p, s %p, c2 %p, i %p\n",
(unsigned
int)(void*)&a.c1 - (unsigned int)(void*)&a,
(unsigned
int)(void*)&a.s - (unsigned int)(void*)&a,
(unsigned
int)(void*)&a.c2 - (unsigned int)(void*)&a,
(unsigned
int)(void*)&a.i - (unsigned int)(void*)&a);
printf("struct
bar c1 %p, c2 %p, s %p, i %p\n",
(unsigned
int)(void*)&b.c1 - (unsigned int)(void*)&b,
(unsigned
int)(void*)&b.c2 - (unsigned int)(void*)&b,
(unsigned
int)(void*)&b.s - (unsigned int)(void*)&b,
(unsigned
int)(void*)&b.i - (unsigned int)(void*)&b);
printf("struct
foo_pack c1 %p, s %p, c2 %p, i %p\n",
(unsigned
int)(void*)&p.c1 - (unsigned int)(void*)&p,
(unsigned
int)(void*)&p.s - (unsigned int)(void*)&p,
(unsigned
int)(void*)&p.c2 - (unsigned int)(void*)&p,
(unsigned
int)(void*)&p.i - (unsigned int)(void*)&p);
printf("sizeof
foo is %d\n", sizeof(foo));
printf("sizeof
bar is %d\n", sizeof(bar));
printf("sizeof
foo_pack is %d\n", sizeof(foo_pack));
return
0;
}
//////////////////////////////////////////////////
//////////////////////////////////////////////////
细说 #pragma pack(n)
2008-11-27 00:25
在C语言中,结构是一种复合数据类型,其构成元素既可以是基本数据类型(如int、long、float等)的变量,也可以是一些复合数据类型(如数组、结构、联合等)的数据单元。在结构中,编译器为结构的每个成员按其自然对界(alignment)条件分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同。
例如,下面的结构各成员空间分配情况:
struct test
{
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编译器的缺省字节对齐方式 在缺省情况下,C编译器为每一个变量或是数据单元按其自然对界条件分配空间。一般地,可以通过下面的方法来改变缺省的对界条件:
·
使用伪指令#pragma pack (n),C编译器将按照n个字节对齐。
·
使用伪指令#pragma pack (),取消自定义字节对齐方式。
另外,还有如下的一种方式:
·
__attribute((aligned (n))),让所作用的结构成员对齐在n字节自然边界上。如果结构中有成员的长度大于n,则按照最大成员的长度来对齐。
·
__attribute__ ((packed)),取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐。
以上的n
= 1, 2, 4, 8, 16... 第一种方式较为常见。
(
via http://blog.csdn.net/wenddy112/articles/300583.aspx )
下面有一道在 CSDN论坛 上讨论火热的题:
Intel和微软和本公司同时出现的面试题
#pragma pack(8)
struct s1{
short a;
long b;
};
struct s2{
char c;
s1 d;
long long e;
};
#pragma pack()
问
1.sizeof(s2) = ?
2.s2的c后面空了几个字节接着是d?
感谢 redleaves(ID最吊的网友) 的解答,结果如下:
sizeof(S2)结果为24.
成员对齐有一个重要的条件,即每个成员分别对齐.即每个成员按自己的方式对齐.
也就是说上面虽然指定了按8字节对齐,但并不是所有的成员都是以8字节对齐.其对齐的规则是,每个成员按其类型的对齐参数(通常是这个类型的大小)和指定对齐参数(这里是8字节)中较小的一个对齐.并且结构的长度必须为所用过的所有对齐参数的整数倍,不够就补空字节.
S1中,成员a是1字节默认按1字节对齐,指定对齐参数为8,这两个值中取1,a按1字节对齐;成员b是4个字节,默认是按4字节对齐,这时就按4字节对齐,所以sizeof(S1)应该为8;
S2中,c和S1中的a一样,按1字节对齐,而d
是个结构,它是8个字节,它按什么对齐呢?对于结构来说,它的默认对齐方式就是它的所有成员使用的对齐参数中最大的一个,S1的就是4.所以,成员d就是按4字节对齐.成员e是8个字节,它是默认按8字节对齐,和指定的一样,所以它对到8字节的边界上,这时,已经使用了12个字节了,所以又添加了4个字节的空,从第16个字节开始放置成员e.这时,长度为24,已经可以被8(成员e按8字节对齐)整除.这样,一共使用了24个字节.
a b
S1的内存布局:11**,1111,
c S1.a
S1.b d
S2的内存布局:1***,11**,1111,****11111111
这里有三点很重要:
1.每个成员分别按自己的方式对齐,并能最小化长度
2.复杂类型(如结构)的默认对齐方式是它最长的成员的对齐方式,这样在成员是复杂类型时,可以最小化长度
3.对齐后的长度必须是成员中最大的对齐参数的整数倍,这样在处理数组时可以保证每一项都边界对齐
补充一下,对于数组,比如:
char a[3];这种,它的对齐方式和分别写3个char是一样的.也就是说它还是按1个字节对齐.
struct A
{
int
n;
char
s[9];
char
c;
double
p;
};
sizeof(A) ==24
如果写: typedef char Array3[3];
Array3这种类型的对齐方式还是按1个字节对齐,而不是按它的长度.
不论类型是什么,对齐的边界一定是1,2,4,8,16,32,64....中的一个.
|
|