结构体各成员共用同一段内存单元里又有结构体各成员共用同一段内存单元成员,那么,如何进行内存对齐

结构体成员的内存分布与对齐
我们先看一道IBM和微软的笔试题:
IBM笔试题:
sizeof( A)=6,
sizeof(B)=8,为什么?
注:sizeof(short)=2,sizeof(long)=4
微软笔试题:
struct example1
struct example2
example1 struct1;
int main(int argc, char*argv[])
example2 e2;
int d=(unsigned int)&e2.struct1-(unsigned int)&e2.c;
printf("%d,%d,%d\n",sizeof(example1),sizeof(example2),d);
输出结果?
要能清除的分析上面的问题就要搞清楚结构体变量的成员在内存里是如何分布的、成员先后顺序是怎样的、成员之间是连续的还是分散的、还是其他的什么形式?其实这些问题既和软件相关又和硬件相关。所谓软件相关主要是指和具体的编程语言的编译器的特性相关,编译器为了优化CPU访问内存的效率,在生成结构体成员的起始地址时遵循着某种特定的规则,这就是所谓的
结构体成员“对齐”;所谓硬件相关主要是指CPU的“字节序”问题,也就是大于一个字节类型的数据如int类型、short类型等,在内存中的存放顺序,即单个字节与高低地址的对应关系。字节序分为两类:Big-Endian和Little-Endian,有的文章上称之为“大端”和“小端”,他们是这样定义的:
Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端;Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。
Intel、VAX和Unisys处理器的计算机中的数据的字节顺序是Little-Endian,IBM 大型机和大多数Unix平台的计算机中字节顺序是Big –Endian。
关与Big-Endian和Little-Endian问题本文暂不做详细讨论,本文将以小端机(此处为intel x86架构的计算机)、OS:WindowsXp和VC++6.0编译器来详细讨论结构体成员的“对齐”问题。
前面说了,为了优化CPU访问内存的效率,程序语言的编译器在做变量的存储分配时就进行了分配优化处理,优化规则大致原则是这样:
对于n字节的元素(n=2,4,8,...),它的首地址能被n整除,这种原则称为“对齐”,如WORD(2字节)的值应该能被2整除的位置,DWORD(4字节)应该在能被4整除的位置。
对于结构体来说,结构体的成员在内存中顺序存放,所占内存地址依次 增高,第一个成员处于低地址处,最后一个成员处于最高地址处,但结构体成员的内存分配不一定是连续的,编译器会对其成员变量依据前面介绍的 “对齐”原则进行处理。对待每个成员类似于对待单个n字节的元素一样,依次为每个元素找一个适合的首地址,使得其符合上述的“对齐”原则。通常编译器中可以设置一个对齐参数n,但这个n并不是结构体成员实际的对齐参数,VC++6.0中结构体的每个成员实际对齐参数N通常是这样计算得到的N=min(sizeof(该成员类型),n)(n为VC++6.0中可设置的值)。
成员的内存分配规律是这样的:从结构体的首地址开始向后依次为每个成员寻找第一个满足条件的首地址x,该条件是x % N = 0,并且整个结构的长度必须为各个成员所使用的对齐参数中最大的那个值的最小整数倍,不够就补空字节。
结构体中所有成员的对齐参数N的最大值称为结构体的对齐参数。
VC++6.0中n默认是8个字节,可以修改这个设定的对齐参数,方法为在菜单“工程”的“设置”中的“C/C++”选项卡的“分类”中 “CodeGeneration ”的“Struct member alignment” 中设置,1byte、2byte、4byte、8byte、16byte等几种,默认为8byte
也可以程序控制,采用指令:#pragma
pack(xx)控制
pack(1),1字节对齐,#pragma
pack(4),4字节对齐
pack(16),16字节对齐
接下来我们将分不同的情况来详细讨论结构体成员的分布情况,顺便提醒一下,
常见类型的长度:
Int 4byte,
Short 2byte,
Char 1byte,
Double 8byte,
Long 4byte
让我们先看下例:
int main(int argc, char*argv[])
printf("%len:d\n",sizeof(A));
printf("%d,%d,%d,%d",&strua.c,&strua.d,&strua.s,&strua.i);
1)n设置为8byte时
结果:len:24,
内存中成员分布如下:
strua.c分配在一个起始于8的整数倍的地址1245032(为什么是这样读者先自己思考,读完就会明白),接下来要在strua.c之后分配strua.d,由于double为8字节,取N=min(8,8),8字节来对齐,所以从strua.c向后找第一个能被8整除的地址,所以取得1245040, strua.s 为2byte小于参数n,所以N=min(2,8),即N=2,取2字节长度对齐,所以要从strua.d后面寻找第一个能被2整除的地址来存储strua.s,由于strua.d后面的地址为1245048可以被2整除,所以strua.s紧接着分配,现在来分配strua.i,int为4byte,小于指定对齐参数8byte,所以N=min(4,8)取N=4byte对齐,strua.s后面第一个能被4整除地址为,所以在的位置分配了strua.i,中间补空,同时由于所有成员的N值的最大值为8,所以整个结构长度为8byte的最小整数倍,即取24byte其余均补0.
于是该结构体的对齐参数就是8byte。
2)当对齐参数n设置为16byte时,结果同上,不再分析
3)当对齐参数设置为4byte时
上例结果为:Len:20
内存中成员分布如下:
Strua.c起始于一个4的整数倍的地址,接下来要在strua.c之后分配strua.d,由于strua.d长度为8byte,大于对齐参数4byte,所以N=min(8,4)取最小的4字节,所以向后找第一个能被4整除的地址来作为strua.d首地址,故取,接着要在strua.d后分配strua.s,strua.s长度为2byte小于4byte,取N=min(2,4)2byte对齐,由于strua.d后的地址为1245048可以被2
整除,所以直接在strua.d后面分配,strua.i的长度为4byte,所以取N=min(4,4)4byte对齐,所以从strua.s向后找第一个能被4整除的位置即来分配和strua.i,同时N的最大值为4byte,所以整个结构的长度为4byte的最小整数倍16byte
4)当对齐参数设置为2byte时
上例结果为:Len:16
Strua.c分配后,向后找一第一个能被2整除的位置来存放strua.d,依次类推
5)1byte对齐时:
上例结果为:Len:15
此时,N=min(sizeof(成员),1),取N=1,由于1可以整除任何整数,所以各个成员依次分配,没有间空,如下图所示:
6)当结构体成员为数组时,并不是将整个数组当成一个成员来对待,而是将数组的每个元素当一个成员来分配,其他分配规则不变,如将上例的结构体改为:
对齐参数设置为8byte,则,运行结果如下:
Strua 的s分配后,接下来分配Strua 的数组szBuf[5],这里要单独分配它的每个元素,由于是char类型,所以N=min(1,8),取N=1,所以数组szBuf[5]的元素依次分配没有间隙。
7)当结构中有成员不是一个完整的类型单元,如int或short型,而是该类型的一段时,即位段时,如
对于位段成员,存储是按其类型分配空间的,如int 型就分配4个连续的存储单元,如果是相邻的同类型的段位成员就连续存放,共用存储单元,此处如a1,a2将公用一个4字节的存储单元,当该类型的长度不够用时,就另起一个该类型长度的存储空间。有位段时的对齐规则是这样:同类型的、相邻的可连续在一个类型的存储空间中存放的位段成员作为一个该类型的成员变量来对待,不是同类型的、相邻的位段成员,分别当作一个单独得该类型的成员来对待,分配一个完整的类型空间,其长度为该类型的长度,其他成员的分配规则不变,仍然按照前述的对齐规则进行。
对于 struct A,VC++6.0中n设置为8时,sizeof(A)=16,内存分布:
Vc++6.0的对齐参数设置为8、16、4字节对齐时,sizeof(A)=12内存分布为:
(灰色部分未使用)
当对齐参数设置为2字节时:(灰色部分未使用)sizeof(A)=10
又如intel的笔试题:
“stdafx.h”
&iostream.h&
(char*)&s;
cout&&s.a&&endl&&s.b&&endl&&s.c&&
运行的结果是
结构bit的成员在内存中由低地址到高地址顺序存放,执行*c=0x99;后成员的内存分布情况为:
8)当结构体成员是结构体类型时,那么该过程是个递归过程,且把该成员作为一个整体来对待,如(微软笔试题):
struct example1
struct example2
example1 struct1;
int main(int argc, char*argv[])
example2 e2;
int d=(unsigned int)&e2.struct1-(unsigned int)&e2.c;
printf("%d,%d,%d\n",sizeof(example1),sizeof(example2),d);
8byte对齐时,结果为:
内存分布为:
因为example1的对齐参数为4,分配完c后要接着分配struct1,这时的对齐参数为min(struct1的对齐参数,指定对齐参数),开始分配struct1,在struct1的成员分配过程中又是按照前述的规则来分配的。
内存对齐”应该是编译器的“管辖范围”。编译器为程序中的每个“数据单元”安排在适当的位置上。但是C语言的一个特点就是太灵活,太强大,它允许你干预“内存对齐”。如果你想了解更加底层的秘密,“内存对齐”对你就不应该再透明了。
一、内存对齐的原因
大部分的参考资料都是如是说的:
1、平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2、性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
二、对齐规则
每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。程序员可以通过预编译命令#pragmapack(n),n=1,2,4,8,16来改变这一系数,其中的n就是你要指定的“对齐系数”。
对齐步骤:
1、数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的对齐按照#pragmapack指定的数值和这个数据成员自身长度中,比较小的那个进行。
2、结构(或联合)的整体对齐规则:在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行。
3、结合1、2颗推断:当#pragma pack的n值等于或超过所有数据成员长度的时候,这个n值的大小将不产生任何效果。
备注:数组成员按长度按数组类型长度计算,如char t[9],在第1步中数据自身长度按1算,累加结构体时长度为9;第2步中,找最大数据长度时,如果结构体T有复杂类型成员A的,该A成员的长度为该复杂类型成员A的最大成员长度。
我们通过一系列例子的详细说明来证明这个规则吧!
我试验用的编译器包括GCC3.4.2和VC6.0的C编译器,平台为Windows XP + Sp2。
我们将用典型的struct对齐来说明。首先我们定义一个struct:
#pragma pack(n) /* n = 1, 2, 4, 8, 16 */
struct test_t {
#pragma pack(n)
首先我们首先确认在试验平台上的各个类型的size,经验证两个编译器的输出均为:
sizeof(char) = 1
sizeof(short) = 2
sizeof(int) = 4
我们的试验过程如下:通过#pragmapack(n)改变“对齐系数”,然后察看sizeof(structtest_t)的值。
1、1字节对齐(#pragma pack(1))
输出结果:sizeof(structtest_t) = 8 [两个编译器输出一致]
分析过程:
1) 成员数据对齐
#pragma pack(1)
struct test_t {
/* 长度4& 1 按1对齐;起始offset=0 0%1=0;存放位置区间[0,3] */
/* 长度1= 1 按1对齐;起始offset=4 4%1=0;存放位置区间[4] */
/* 长度2& 1 按1对齐;起始offset=5 5%1=0;存放位置区间[5,6] */
/* 长度1= 1 按1对齐;起始offset=7 7%1=0;存放位置区间[7] */
#pragma pack()
成员总大小=8
2) 整体对齐
整体对齐系数 =min((max(int,short,char), 1) = 1
整体大小(size)=$(成员总大小) 按 $(整体对齐系数) 圆整 = 8 /* 8%1=0 */ [注1]
2、2字节对齐(#pragma pack(2))
输出结果:sizeof(structtest_t) = 10 [两个编译器输出一致]
分析过程:
1) 成员数据对齐
#pragma pack(2)
struct test_t {
/* 长度4& 2 按2对齐;起始offset=0 0%2=0;存放位置区间[0,3] */
/* 长度1& 2 按1对齐;起始offset=4 4%1=0;存放位置区间[4] */
/* 长度2= 2 按2对齐;起始offset=6 6%2=0;存放位置区间[6,7] */
/* 长度1& 2 按1对齐;起始offset=8 8%1=0;存放位置区间[8] */
#pragma pack()
成员总大小=9
2) 整体对齐
整体对齐系数 =min((max(int,short,char), 2) = 2
整体大小(size)=$(成员总大小) 按 $(整体对齐系数) 圆整 = 10 /* 10%2=0 */
3、4字节对齐(#pragma pack(4))
输出结果:sizeof(structtest_t) = 12 [两个编译器输出一致]
分析过程:
1) 成员数据对齐
#pragma pack(4)
struct test_t {
/* 长度4= 4 按4对齐;起始offset=0 0%4=0;存放位置区间[0,3] */
/* 长度1& 4 按1对齐;起始offset=4 4%1=0;存放位置区间[4] */
/* 长度2& 4 按2对齐;起始offset=6 6%2=0;存放位置区间[6,7] */
/* 长度1& 4 按1对齐;起始offset=8 8%1=0;存放位置区间[8] */
#pragma pack()
成员总大小=9
2) 整体对齐
整体对齐系数 =min((max(int,short,char), 4) = 4
整体大小(size)=$(成员总大小) 按 $(整体对齐系数) 圆整 = 12 /* 12%4=0 */
4、8字节对齐(#pragma pack(8))
输出结果:sizeof(structtest_t) = 12 [两个编译器输出一致]
分析过程:
1) 成员数据对齐
#pragma pack(8)
struct test_t {
/* 长度4& 8 按4对齐;起始offset=0 0%4=0;存放位置区间[0,3] */
/* 长度1& 8 按1对齐;起始offset=4 4%1=0;存放位置区间[4] */
/* 长度2& 8 按2对齐;起始offset=6 6%2=0;存放位置区间[6,7] */
/* 长度1& 8 按1对齐;起始offset=8 8%1=0;存放位置区间[8] */
#pragma pack()
成员总大小=9
2) 整体对齐
整体对齐系数 =min((max(int,short,char), 8) = 4
整体大小(size)=$(成员总大小) 按 $(整体对齐系数) 圆整 = 12 /* 12%4=0 */
5、16字节对齐(#pragma pack(16))
输出结果:sizeof(structtest_t) = 12 [两个编译器输出一致]
分析过程:
1) 成员数据对齐
#pragma pack(16)
struct test_t {
/* 长度4& 16 按4对齐;起始offset=0 0%4=0;存放位置区间[0,3] */
/* 长度1& 16 按1对齐;起始offset=4 4%1=0;存放位置区间[4] */
/* 长度2& 16 按2对齐;起始offset=6 6%2=0;存放位置区间[6,7] */
/* 长度1& 16 按1对齐;起始offset=8 8%1=0;存放位置区间[8] */
#pragma pack()
成员总大小=9
2) 整体对齐
整体对齐系数 =min((max(int,short,char), 16) = 4
整体大小(size)=$(成员总大小) 按 $(整体对齐系数) 圆整 = 12 /* 12%4=0 */
记录类型的内存分配!
Packed Record和Record的不同之处!
MyRec=Record
var2,var3,var4,var5,var6,var7,var8:
var12,var13:
ShowMessage(intTostr(SizeOf(MyRec)));
结果显示为18,而按我想象应为16。请高手讲解一下Delphi5.0中变量内存空间分配机制,因为我有一个数组MyArray:Array[1..1000000]of MyR需要考虑节省内存问题,
另外不要说我懒不爱看书,我手头所有关于Delphi的书都没有提到这个问题。
显示的结果应该为28,而不是18!按道理应该是22。用Packed的结果就是22。
拟定义的数组比较大,应该用packedrecord!
原因如下:
在Windows中内存的分配一次是4个字节的。而Packed按字节进行内存的申请和分配,这样速度要慢一些,因为需要额外的时间来进行指针的定位。因此如果不用Packed的话,Delphi将按一次4个字节的方式申请内存,因此如果一个变量没有4个字节宽的话也要占4个字节!这样就浪费了。按上面的例子来说:
var1://integer刚好4个字节!
var2-var5占用4个字节,Var6-Var8占用4个字节,浪费了一个字节。
var9:integer//占用4个字节;
var10:占用4个字节;浪费3个字节
var11:占用4个字节;
var12,var13占用4个字节;浪费2个字节
所以,如果不用packed的话,那么一共浪费6个字节!所以原来22个字节的记录需要28个字节的内存空间!
****************
回复人:eDRIVE(eDRIVE) ( 17:45:00) 得0分
这是因为在32位的环境中,所有变量分配的内存都进行“边界对齐”造成的。这样做可以对速度有优化作用;但是单个定义的变量至少会占用32位,即4个字节。所以会有长度误差,你可以用packed关键字取消这种优化。
深入的分析,内存空间(不是内存地址)在计算机中划分为无数与总线宽度一致的单位,单位之间相接的地方称为“边界”;总线在对内存进行访问时,每次访问周期只能读写一个单位(32bit),如果一个变量横跨“边界”的话,则读或写这个变量就得用两个访问周期,而“边界对齐”时,只需一个访问周期,速度当然会有所优化。
Record的数据各个字节都是对齐的,数据格式比较完整,所以这种格式相对packed占用的内存比较大,
但是因为格式比较整齐,所以电脑读取这个类型的数据的时候速度比较快。
而PackedRecord对数据进行了压缩,节省了内存空间,当然他的速度也变的慢了。
TDefaultRecord
string[4];
TPackedRecord
string[4];
defaultRec
ShowMessage('Default
'+IntToStr(SizeOf(defaultRec)));
ShowMessage('Packed
'+IntToStr(SizeOf(packedRec)));
不过,对于现在的操作系统来,packedRecord 节省的那些空间已不用考虑他了。除了做DLL(不用packed容易造成内存混乱)和做硬件编程时(比如串口)编程时必须用到packedRecord,其它情况都可以用Record
C的结构体与Delphi中的记录类型
Object Pascal的指针
一、类型指针的定义。对于指向特定类型的指针,在C中是这样定义的:
与之等价的Object Pascal是如何定义的呢?
其实也就是符号的差别而已。
二、无类型指针的定义。C中有void *类型,也就是可以指向任何类型数据的指针。Object Pascal为其定义了一个专门的类型:Pointer。于是,
三、指针的解除引用。要解除指针引用(即取出指针所指区域的值),C 的语法是 (*ptr),Object Pascal则是 ptr^。
四、取地址(指针赋值)。取某对象的地址并将其赋值给指针变量,C 的语法是
Object Pascal 则是
也只是符号的差别而已。
五、指针运算。在C中,可以对指针进行移动的运算,如:
char a[20];
char *ptr=a;
当执行ptr++;时,编译器会产生让ptr前进sizeof(char)步长的代码,之后,ptr将指向a[1]。ptr+=2;这句使得ptr前进两个sizeof(char)大小的步长。同样,我们来看一下Object Pascal中如何实现:
a : array [1..20] of C
ptr : PC //PChar 可以看作 ^Char
ptr := @a;
Inc(ptr); // 这句等价于 C 的 ptr++;
Inc(ptr, 2); //这句等价于 C 的 ptr+=2;
六、动态内存分配。C中,使用malloc()库函数分配内存,free()函数释放内存。如这样的代码:
int *ptr, *ptr2;
ptr = (int*) malloc(sizeof(int) * 20);
for (i=0; i&20; i++){
free(ptr2);
Object Pascal中,动态分配内存的函数是GetMem(),与之对应的释放函数为FreeMem()(传统Pascal中获取内存的函数是New()和 Dispose(),但New()只能获得对象的单个实体的内存大小,无法取得连续的存放多个对象的内存块)。因此,与上面那段C的代码等价的Object Pascal的代码为:
var ptr, ptr2 : ^
GetMem(ptr, sizeof(integer) * 20);
//这句等价于C的 ptr = (int*) malloc(sizeof(int) * 20);
ptr2 := //保留原始指针位置
for i := 0 to 19 do
FreeMem(ptr2);
对于以上这个例子(无论是C版本的,还是Object Pascal版本的),都要注意一个问题,就是分配内存的单位是字节(BYTE),因此在使用GetMem时,其第二个参数如果想当然的写成 20,那么就会出问题了(内存访问越界)。因为GetMem(ptr, 20);实际只分配了20个字节的内存空间,而一个整形的大小是四个字节,那么访问第五个之后的所有元素都是非法的了(对于malloc()的参数同样)。
七、字符数组的运算。C语言中,是没有字符串类型的,因此,字符串都是用字符数组来实现,于是也有一套str打头的库函数以进行字符数组的运算,如以下代码:
char str[15];
strcpy(str, "teststr");
strcat(str, "_testok");
pstr = (char*) malloc(sizeof(char) * 15);
strcpy(pstr, str);
printf(pstr);
free(pstr);
而在Object Pascal中,有了String类型,因此可以很方便的对字符串进行各种运算。但是,有时我们的Pascal代码需要与C的代码交互(比如:用Object Pascal的代码调用C写的DLL或者用Object Pascal写的DLL准备允许用C写客户端的代码)的话,就不能使用String类型了,而必须使用两种语言通用的字符数组。其实,Object Pascal提供了完全相似C的一整套字符数组的运算函数,以上那段代码的Object Pascal版本是这样的:
var str : array [1..15]
pstr : PC //Pchar 也就是 ^Char
StrCopy(@str, 'teststr'); //在C中,数组的名称可以直接作为数组首地址指针来用
//但Pascal不是这样的,因此 str前要加上取地址的运算符
StrCat(@str, '_testok');
GetMem(pstr, sizeof(char) * 15);
StrCopy(pstr, @str);
Write(pstr);
FreeMem(pstr);
八、函数指针。在动态调用DLL中的函数时,就会用到函数指针。假设用C写的一段代码如下:
typedef int (*PVFN)(int); //定义函数指针类型
int main()
HMODULE hModule = LoadLibrary("test.dll");
PVFN pvfn = NULL;
pvfn = (PVFN) GetProcAddress(hModule, "Function1");
FreeLibrary(hModule);
就我个人感觉来说,C语言中定义函数指针类型的typedef代码的语法有些晦涩,而同样的代码在Object Pascal中却非常易懂:
type PVFN = Function (para : Integer) : I
fn : PVFN;
//也可以直接在此处定义,如:fn : function (para:Integer):I
hm : HMODULE;
hm := LoadLibrary('test.dll');
fn := GetProcAddress(hm, 'Function1');
FreeLibrary(hm);
以上是一位Delphi高手给我回的贴!
定义结构体与分配内存
初学者对于结构体内存对齐与补齐的理解
结构体(内存对齐)和共用体—C语言
结构体的内存分配
结构体在内存中的对齐规则
结构体在内存中的对其规则
共用体和结构体所占内存大小的计算方法二
结构体在内存中是如何存储的
C语言结构体占用空间内存大小解析
结构体成员的对齐方式
没有更多推荐了,详解结构体、类等内存字节对齐
&先说个题外话:早些年我学C程序设计时,写过一段解释硬盘MBR分区表的代码,对着磁盘编辑器怎么看,怎么对,可一执行,结果就错了。当时调试也不太会,又根本没听过结构体对齐这一说,所以,问题解决不了,好几天都十分纠结。后来万般无奈请教一个朋友,才获悉可能是结构体对齐的事,一查、一改,果真如此。
&&& 问题是解决了,可网上的资料多数只提到内存对齐是如何做的,却鲜有提及为什么这样做(即使提,也相当简单)。笔者是个超级健忘者,很难机械式的记住这些破规则,于是仔细想了想,总算明白了原因,这样,这些对齐的规则也就不会再轻易忘记了。&&&&
&&& 不光结构体存在内存对齐一说,类(对象)也如此,甚至于所有变量在内存中的存储也有对齐一说(只是这些对程序员是透明的,不需要关心)。实际上,这种对齐是为了在空间与复杂度上达到平衡的一种技术手段,简单的讲,是为了在可接受的空间浪费的前提下,尽可能的提高对相同运算过程的最少(快)处理。先举个例子:
&&& 假设机器字长是32位的(即4字节,下面示例均按此字长),也就是说处理任何内存中的数据,其实都是按32位的单位进行的。现在有2个变量:&&&
&&&& 假设这2个变量是从内存地址0开始分配的,如果不考虑对齐,应该是这样存储的(见下图,以intel上的little endian为例,为了形象,每16个字节分做一行,后同):
&&& 因为计算机的字长是4字节的,所以在处理变量A与B时的过程可能大致为:
&&& A:将0x00-0x03共32位读入寄存器,再通过左移24位再右移24位运算得到a的值(或与0x000000FF做与运算)
&&& B:将0x00-0x03这32位读入寄存器,通过位运算得到低24位的值;再将0x04-0x07这32位读入寄存器,通过位运算得到高8位的值;再与最先得到的24位做位运算,才可得到整个32位的值。
&&& 上面叙述可知,对a的处理是最简处理,可对b的处理,本身是个32位数,处理的时候却得折成2部分,之后再合并,效率上就有些低了。
&&& 想解决这个问题,就需要付出几个字节浪费的代价,改为下图的分配方式:
&&& 按上面的分配方式,A的处理过程不变;B却简单得多了:只需将0x04-0x07这32位读入寄存器就OK了。
&&& 我们可以具体谈结构体或类成员的对齐了:
&&& 结构体在编译成机器代码后,其实就没有本身的集合概念了,而类,实际上是个加强版的结构体,类的对象在实例化时,内存中申请的就是一些变量的空间集合(类似于结构体,同时也不包含函数指针)。这些集合中的每个变量,在使用中,都需要涉及上述的加工原则,自然也就需要在效率与空间之间做出权衡。
&&& 为了便捷加工连续多个相同类型原始变量,同时简化原始变量寻址,再汇总上述最少处理原则,通常可以将原始变量的长度做为针对此变量的分配单位,比如内存可用64个单元,如果某原始变量长度为8字节,即使机器字长为4字节,分配的时候也以8字节对齐(看似IO次数是相同的),这样,寻址、分配时,均可以按每8字节为单位进行,简化了操作,也可以更高效。
&&& 默认的对齐规则,追求的至少两点:1、变量的最高效加工 2、达到目的1的最少空间
&&& 举个例子,一个结构体如下:
typedef struct T
&&& //本身长度1字节
&&& __int64& //本身长度8字节
&&&& //本身长度4字节
&&&& //本身长度2字节
&&&& //本身长度1字节
&&&& //本身长度2字节
&&& 假设定义了一个结构体变量C,在内存中分配到了0x00的位置,显然:
&&& 对于成员C.c& 无论如何,也是一次寄存器读入,所以先占一个字节。
&&& 对于成员C.d& 是个64位的变量,如果紧跟着C.c存储,则读入寄存器至少需要3次,为了实现最少的2次读入,至少需要以4字节对齐;同时对于8字节的原始变量,为了在寻址单位上统一,则需要按8字节对齐,所以,应该分配到0x08-0xF的位置。
&&& 对于成员C.e& 是个32位的变量,自然只需满足分配起始为整数个32位即可,所以分配至0x10-0x13。
&&& 对于成员C.f& 是个16位的变量,直接分配在0x14-0x16上,这样,反正只需一次读入寄存器后加工,边界也与16位对齐。
&&& 对于成员C.g& 是个8位的变量,本身也得一次读入寄存器后加工,同时对于1个字节的变量,存储在任何字节开始都是对齐,所以,分配到0x17的位置。
&&& 对于成员C.h& 是个16位的变量,为了保证与16位边界对齐,所以,分配到0x18-0x1A的位置。
&&& 分配图如下(还不正确,耐心读下去):
&&& 结构体C的占用空间到h结束就可以了吗?我们找个示例:如果定义一个结构体数组 CA[2],按变量分配的原则,这2个结构体应该是在内存中连续存储的,分配应该如下图:
&&& 分析一下上图,明显可知,CA[1]的很多成员都不再对齐了,究其原因,是结构体的开始边界不对齐。
&&& 那结构体的开始偏移满足什么条件才可以使其成员全部对齐呢。想一想就明白了:很简单,保证结构体长度是原始成员最长分配的整数倍即可。
&&& 上述结构体应该按最长的.d成员对齐,即与8字节对齐,这样正确的分配图如下:
&&& 当然结构体T的长度:sizeof(T)==0x20;
&&&& 再举个例子,看看在默认对齐规则下,各结构体成员的对齐规则:
typedef struct A
&&&& //1个字节
&&&& //4个字节,要与4字节对齐,所以分配至第4个字节处
&&&& //2个字节, 上述两个成员过后,本身就是与2对齐的,所以之前无填充
&}; //整个结构体,最长的成员为4个字节,需要总长度与4字节对齐,所以, sizeof(A)==12
typedef struct B
&&&& //1个字节
&&& __int64& //8个字节,位置要与8字节对齐,所以分配到第8个字节处
&&&& //4个字节,成员d结束于15字节,紧跟的16字节对齐于4字节,所以分配到16-19
&&&& //2个字节,成员e结束于19字节,紧跟的20字节对齐于2字节,所以分配到20-21
&&& A& //结构体长为12字节,最长成员为4字节,需按4字节对齐,所以前面跳过2个字节,
//到24-35字节处
&&&&& //1个字节,分配到36字节处
&&&&& //4个字节,要对齐4字节,跳过3字节,分配到40-43 字节
}; //整个结构体的最大分配成员为8字节,所以结构体后面加5字节填充,被到48字节。故:
//sizeof(B)==48;
&&& 具体的分配图如下:
&上述全部测试代码如下:
#include &stdio.h&
typedef struct A
typedef struct B
&&& __int64
typedef struct C
&&& __int64
typedef struct D
int main()
&&& B *b=new B;
&&& void *s[32];
&&& s[0]=b;
&&& s[1]=&b-&c;
&&& s[2]=&b-&d;
&&& s[3]=&b-&e;
&&& s[4]=&b-&f;
&&& s[5]=&b-&g;
&&& s[6]=&b-&h;
&&& s[7]=&b-&g.c;
&&& s[8]=&b-&g.d;
&&& s[9]=&b-&g.e;
&&& s[10]=&b-&i;
&&& b-&c= 0x11;
&&& b-&d= 0x2222;
&&& b-&e= 0x;
&&& b-&f=0x4444;
&&& b-&g.c=0x50;
&&& b-&g.d=0x;
&&& b-&g.e=0x5252;
&&& b-&h=0x66;
&&& int i1=sizeof(A);
&&& int i2=sizeof(B);
&&& int i3=sizeof(C);
&&& int i4=sizeof(D);
&&& printf(&i1:%d\ni2:%d\ni3:%d\ni4:%d\n&,i1,i2,i3,i4);//12 48 32 6
运行时的内存情况如下图:
最后,简单加工一下转载过来的内存对齐正式原则:
& 先介绍四个概念:
1)数据类型自身的对齐值:基本数据类型的自身对齐值,等于sizeof(基本数据类型)。
2)指定对齐值:#pragma pack (value)时的指定对齐值value。
3)结构体或者类的自身对齐值:其成员中自身对齐值最大的那个值。
4)数据成员、结构体和类的有效对齐值:自身对齐值和指定对齐值中较小的那个值。
& 有效对齐值N是最终用来决定数据存放地址方式的值,最重要。有效对齐N,就是表示&对齐在N上&,也就是说该数据的&存放起始地址%N=0&.而数据结构中的数据变量都是按定义的先后顺序来排放的。第一个数据变量的起始地址就是 数据结构的起始地址。结构体的成员变量要对齐排放,结构体本身也要根据自身的有效对齐值圆整(就是结构体成员变量占用总长度需要是对结构体有效对齐值的整 数倍)
#pragma pack (value)来告诉编译器,使用我们指定的对齐值来取代缺省的。
如#pragma pack (1)& /*指定按2字节对齐*/
#pragma pack () /*取消指定对齐,恢复缺省对齐*/
作者&张宇(数据恢复)&}

我要回帖

更多关于 结构体的内存对齐 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信