结构体内存分配分析.doc_第1页
结构体内存分配分析.doc_第2页
结构体内存分配分析.doc_第3页
结构体内存分配分析.doc_第4页
结构体内存分配分析.doc_第5页
已阅读5页,还剩10页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

结构体成员的内存分布与对齐马国峻 我们先看一道IBM和微软的笔试题:IBM笔试题:struct short a1;short a2; short a3; A; struct long a1; short a2; B; sizeof( A)=6, sizeof(B)=8,为什么? 注:sizeof(short)=2,sizeof(long)=4微软笔试题:struct example1 short a ;long b; ; struct example2 char c; example1 struct1; short e; ;int main(int argc, char* argv)example2 e2;int d=(unsigned int)&e2.struct1-(unsigned int)&e2.c;printf(%d,%d,%dn,sizeof(example1),sizeof(example2),d);return 0;输出结果?要能清除的分析上面的问题就要搞清楚结构体变量的成员在内存里是如何分布的、成员先后顺序是怎样的、成员之间是连续的还是分散的、还是其他的什么形式?其实这些问题既和软件相关又和硬件相关。所谓软件相关主要是指和具体的编程语言的编译器的特性相关,编译器为了优化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+”选项卡的“分类”中 “Code Generation ”的“Struct member alignment” 中设置,1byte、2byte、4byte、8byte、16byte等几种,默认为8byte也可以程序控制,采用指令:#pragma pack(xx)控制如#pragma pack(1),1字节对齐,#pragma pack(4),4字节对齐#pragma pack(16),16字节对齐接下来我们将分不同的情况来详细讨论结构体成员的分布情况,顺便提醒一下,常见类型的长度:Int 4byte,Short 2byte,Char 1byte,Double 8byte,Long 4byte让我们先看下例:struct Achar c;/1bytedoubled;/8byteshorts;/2byteinti;/4byte;int main(int argc, char* argv)A strua;printf(%len:dn,sizeof(A);printf(%d,%d,%d,%d,&strua.c,&strua.d,&strua.s,&strua.i);return 0;1)n设置为8byte时结果:len:24,1245032,1245040,1245048,1245052内存中成员分布如下:补0d补0cs4byte4byteStrua1245032i12450401245048strua.c分配在一个起始于8的整数倍的地址1245032(为什么是这样读者先自己思考,读完就会明白),接下来要在strua.c之后分配strua.d,由于double为8字节,取N=min(8,8),8字节来对齐,所以从strua.c向后找第一个能被8整除的地址,所以取1245032+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整除地址为1245048+4,所以在1245048+4的位置分配了strua.i,中间补空,同时由于所有成员的N值的最大值为8,所以整个结构长度为8byte的最小整数倍,即取24byte其余均补0.于是该结构体的对齐参数就是8byte。2)当对齐参数n设置为16byte时,结果同上,不再分析3)当对齐参数设置为4byte时上例结果为:Len:201245036,1245040,1245048,1245052内存中成员分布如下:cds补0补04byteStrua124503612450401245052i1245048Strua.c起始于一个4的整数倍的地址,接下来要在strua.c之后分配strua.d,由于strua.d长度为8byte,大于对齐参数4byte,所以N=min(8,4)取最小的4字节,所以向后找第一个能被4整除的地址来作为strua.d首地址,故取1245036+4,接着要在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整除的位置即1245048+4来分配和strua.i,同时N的最大值为4byte,所以整个结构的长度为4byte的最小整数倍16byte4)当对齐参数设置为2byte时上例结果为:Len:161245040,1245042,1245050,1245052补0dcsiStrua.c12450402byteStrua.c分配后,向后找一第一个能被2整除的位置来存放strua.d,依次类推5)1byte对齐时:上例结果为:Len:151245040,1245041,1245049,1245051此时,N=min(sizeof(成员),1),取N=1,由于1可以整除任何整数,所以各个成员依次分配,没有间空,如下图所示:cdsi1byte8byte2byte4byte6)当结构体成员为数组时,并不是将整个数组当成一个成员来对待,而是将数组的每个元素当一个成员来分配,其他分配规则不变,如将上例的结构体改为:struct Acharc;/1bytedoubled;/8byteshorts;/2bytechar szBuf5;对齐参数设置为8byte,则,运行结果如下:Len:241245032,1245040,1245048,1245050补0dszBufcs1补08byteStrua 的s分配后,接下来分配Strua 的数组szBuf5,这里要单独分配它的每个元素,由于是char类型,所以N=min(1,8),取N=1,所以数组szBuf5的元素依次分配没有间隙。7)当结构中有成员不是一个完整的类型单元,如int或short型,而是该类型的一段时,即位段时,如struct Ainta1:5;inta2:9;charc;intb:4;shorts;对于位段成员,存储是按其类型分配空间的,如int 型就分配4个连续的存储单元,如果是相邻的同类型的段位成员就连续存放,共用存储单元,此处如a1,a2将公用一个4字节的存储单元,当该类型的长度不够用时,就另起一个该类型长度的存储空间。有位段时的对齐规则是这样:同类型的、相邻的可连续在一个类型的存储空间中存放的位段成员作为一个该类型的成员变量来对待,不是同类型的、相邻的位段成员,分别当作一个单独得该类型的成员来对待,分配一个完整的类型空间,其长度为该类型的长度,其他成员的分配规则不变,仍然按照前述的对齐规则进行。对于 struct A,VC+6.0中n设置为8时,sizeof(A)=16,内存分布:a1a2cd空s又如:struct B int a:5; int b:7; int c:6; int d:9; char e:2; int x; ; Vc+6.0的对齐参数设置为8、16、4字节对齐时,sizeof(A)=12内存分布为:(灰色部分未使用)空xecdcba4byte当对齐参数设置为2字节时:(灰色部分未使用)sizeof(A)=10xeabd2byte又如intel的笔试题:#include “stdafx.h” #include struct bit int a:3; int b:2; int c:3; ; int main(int argc, char* argv) bit s; char *c = (char*)&s; *c = 0x99; cout s.a endl s.bendls.cendl; return 0; Output:? 运行的结果是 1 -1 -4 结构bit的成员在内存中由低地址到高地址顺序存放,执行 *c=0x99;后成员的内存分布情况为:&s10011001cabc8)当结构体成员是结构体类型时,那么该过程是个递归过程,且把该成员作为一个整体来对待,如(微软笔试题):struct example1 short a ;long b; ; struct example2 char c; example1 struct1; short e; ;int main(int argc, char* argv)example2 e2;int d=(unsigned int)&e2.struct1-(unsigned int)&e2.c;printf(%d,%d,%dn,sizeof(example1),sizeof(example2),d);return 0;8byte对齐时,结果为:8,16,4内存分布为:c补0a补0be补0example2example14byte因为example1的对齐参数为4,分配完c后要接着分配struct1,这时的对齐参数为min(struct1的对齐参数,指定对齐参数),开始分配struct1,在struct1的成员分配过程中又是按照前述的规则来分配的。关于结构体内存对齐内存对齐”应该是编译器的“管辖范围”。编译器为程序中的每个“数据单元”安排在适当的位置上。但是C语言的一个特点就是太灵活,太强大,它允许你干预“内存对齐”。如果你想了解更加底层的秘密,“内存对齐”对你就不应该再透明了。一、内存对齐的原因大部分的参考资料都是如是说的:1、平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。2、性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。二、对齐规则每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。程序员可以通过预编译命令#pragma pack(n),n=1,2,4,8,16来改变这一系数,其中的n就是你要指定的“对齐系数”。对齐步骤:1、数据成员对齐规则:结构(struct)(或联合(union)的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的对齐按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行。2、结构(或联合)的整体对齐规则:在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行。3、结合1、2颗推断:当#pragma pack的n值等于或超过所有数据成员长度的时候,这个n值的大小将不产生任何效果。备注:数组成员按长度按数组类型长度计算,如char t9,在第1步中数据自身长度按1算,累加结构体时长度为9;第2步中,找最大数据长度时,如果结构体T有复杂类型成员A的,该A成员的长度为该复杂类型成员A的最大成员长度。三、试验我们通过一系列例子的详细说明来证明这个规则吧!我试验用的编译器包括GCC 3.4.2和VC6.0的C编译器,平台为Windows XP + Sp2。我们将用典型的struct对齐来说明。首先我们定义一个struct:#pragma pack(n) /* n = 1, 2, 4, 8, 16 */struct test_t int a;char b;short c;char d;#pragma pack(n)首先我们首先确认在试验平台上的各个类型的size,经验证两个编译器的输出均为:sizeof(char) = 1sizeof(short) = 2sizeof(int) = 4我们的试验过程如下:通过#pragma pack(n)改变“对齐系数”,然后察看sizeof(struct test_t)的值。1、1字节对齐(#pragma pack(1)输出结果:sizeof(struct test_t) = 8 两个编译器输出一致分析过程:1) 成员数据对齐#pragma pack(1)struct test_t int a;/* 长度4 1 按1对齐;起始offset=5 5%1=0;存放位置区间5,6 */char d;/* 长度1 = 1 按1对齐;起始offset=7 7%1=0;存放位置区间7 */;#pragma pack()成员总大小=82) 整体对齐整体对齐系数 = min(max(int,short,char), 1) = 1整体大小(size)=$(成员总大小) 按 $(整体对齐系数) 圆整 = 8 /* 8%1=0 */ 注12、2字节对齐(#pragma pack(2)输出结果:sizeof(struct test_t) = 10 两个编译器输出一致分析过程:1) 成员数据对齐#pragma pack(2)struct test_t int a;/* 长度4 2 按2对齐;起始offset=0 0%2=0;存放位置区间0,3 */char b;/* 长度1 2 按1对齐;起始offset=4 4%1=0;存放位置区间4 */short c;/* 长度2 = 2 按2对齐;起始offset=6 6%2=0;存放位置区间6,7 */char d;/* 长度1 2 按1对齐;起始offset=8 8%1=0;存放位置区间8 */;#pragma pack()成员总大小=92) 整体对齐整体对齐系数 = min(max(int,short,char), 2) = 2整体大小(size)=$(成员总大小) 按 $(整体对齐系数) 圆整 = 10 /* 10%2=0 */3、4字节对齐(#pragma pack(4)输出结果:sizeof(struct test_t) = 12 两个编译器输出一致分析过程:1) 成员数据对齐#pragma pack(4)struct test_t int a;/* 长度4 = 4 按4对齐;起始offset=0 0%4=0;存放位置区间0,3 */char b;/* 长度1 4 按1对齐;起始offset=4 4%1=0;存放位置区间4 */short c;/* 长度2 4 按2对齐;起始offset=6 6%2=0;存放位置区间6,7 */char d;/* 长度1 4 按1对齐;起始offset=8 8%1=0;存放位置区间8 */;#pragma pack()成员总大小=92) 整体对齐整体对齐系数 = min(max(int,short,char), 4) = 4整体大小(size)=$(成员总大小) 按 $(整体对齐系数) 圆整 = 12 /* 12%4=0 */4、8字节对齐(#pragma pack(8)输出结果:sizeof(struct test_t) = 12 两个编译器输出一致分析过程:1) 成员数据对齐#pragma pack(8)struct test_t int a;/* 长度4 8 按4对齐;起始offset=0 0%4=0;存放位置区间0,3 */char b;/* 长度1 8 按1对齐;起始offset=4 4%1=0;存放位置区间4 */short c;/* 长度2 8 按2对齐;起始offset=6 6%2=0;存放位置区间6,7 */char d;/* 长度1 8 按1对齐;起始offset=8 8%1=0;存放位置区间8 */;#pragma pack()成员总大小=92) 整体对齐整体对齐系数 = min(max(int,short,char), 8) = 4整体大小(size)=$(成员总大小) 按 $(整体对齐系数) 圆整 = 12 /* 12%4=0 */5、16字节对齐(#pragma pack(16)输出结果:sizeof(struct test_t) = 12 两个编译器输出一致分析过程:1) 成员数据对齐#pragma pack(16)struct test_t int a;/* 长度4 16 按4对齐;起始offset=0 0%4=0;存放位置区间0,3 */char b;/* 长度1 16 按1对齐;起始offset=4 4%1=0;存放位置区间4 */short c;/* 长度2 16 按2对齐;起始offset=6 6%2=0;存放位置区间6,7 */char d;/* 长度1 16 按1对齐;起始offset=8 8%1=0;存放位置区间8 */;#pragma pack()成员总大小=92) 整体对齐整体对齐系数 = min(max(int,short,char), 16) = 4整体大小(size)=$(成员总大小) 按 $(整体对齐系数) 圆整 = 12 /* 12%4=0 */记录类型的内存分配! Packed Record和Record的不同之处! type MyRec=Record var1:integer; var2,var3,var4,var5,var6,var7,var8:shortint; var9:integer; var10:shortint; var11:integer; var12,var13:shortint; end; . ShowMessage(intTostr(SizeOf(MyRec); 结果显示为18,而按我想象应为16。请高手讲解一下Delphi5.0中变量内存空间分配机制,因为我有一个数组MyArray:Array1.1000000 of MyRec;需要考虑节省内存问题, 另外不要说我懒不爱看书,我手头所有关于Delphi的书都没有提到这个问题。 回答: 显示的结果应该为28,而不是18!按道理应该是22。用Packed的结果就是22。 拟定义的数组比较大,应该用packed record! 原因如下: 在Windows中内存的分配一次是4个字节的。而Packed按字节进行内存的申请和分配,这样速度要慢一些,因为需要额外的时间来进行指针的定位。因此如果不用Packed的话,Delphi将按一次4个字节的方式申请内存,因此如果一个变量没有4个字节宽的话也要占4个字节!这样就浪费了。按上面的例子来说: var1:integer;/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)(2001-3-2 17:45:00) 得0分 这是因为在32位的环境中,所有变量分配的内存都进行“边界对齐”造成的。这样做可以对速度有优化作用;但是单个定义的变量至少会占用32位,即4个字节。所以会有长度误差,你可以用packed关键字取消这种优化。 深入的分析,内存空间(不是内存地址)在计算机中划分为无数与总线宽度一致的单位,单位之间相接的地方称为“边界”;总线在对内存进行访问时,每次访问周期只能读写一个单位(32bit),如果一个变量横跨“边界”的话,则读或写这个变量就得用两个访问周期,而“边界对齐”时,只需一个访问周期,速度当然会有所优化。Record的数据各个字节都是对齐的,数据格式比较完整,所以这种格式相对packed占用的内存比较大,但是因为格式比较整齐,所以电脑读取这个类型的数据的时候速度比较快。而Packed Record对数据进行了压缩,节省了内存空间,当然他的速度也变的慢了。 type / Declare an unpacked record TDefaultRecord = Record name1 : string4; floater : single; name2 : char; int : Integer; end; / Declare a packed record TPackedRecord = Packed Record name1 : string4; floater : single; name2 : char; int : Integer; end; var defaultRec : TDefaultRecord; packedRec : TPackedRecord; begin ShowMessage(Default record size = +IntToStr(SizeOf(defaultRec); ShowMessage(Packed record size = +IntToStr(SizeOf(packedRec); end; Default record size = 20 Packed record size = 14 不过,对于现在的操作系统来,packed Record 节省的那些空间已不用考虑他了。除了做DLL(不用packed容易造成内存混乱)和做硬件编程时(比如串口)编程时必须用到packed Record,其它情况都可以用RecordC的结构体与Delphi中的记录类型Object Pascal的指针一、类型指针的定义。对于指向特定类型的指针,在C中是这样定义的:int *ptr;char *ptr;与之等价的Object Pascal是如何定义的呢? varptr : Integer;ptr : char; 其实也就是符号的差别而已。二、无类型指针的定义。C中有void *类型,也就是可以指向任何类型数据的指针。Object Pascal为其定义了一个专门的类型:Pointer。于是,ptr : Pointer;就与C中的void *ptr;等价了。三、指针的解除引用。要解除指针引用(即取出指针所指区域的值),C 的语法是 (*ptr),Object Pascal则是 ptr。四、取地址(指针赋值)。取某对象的地址并将其赋值给指针变量,C 的语法是ptr = &Object;Object Pascal 则是ptr := Object;也只是符号的差别而已。五、指针运算。在C中,可以对指针进行移动的运算,如:char a20;char *ptr=a;ptr+;ptr+=2;当执行ptr+;时,编译器会产生让ptr前进sizeof(char)步长的代码,之后,ptr将指向a1。ptr+=2;这句使得ptr前进两个sizeof(char)大小的步长。同样,我们来看一下Object Pascal中如何实现:vara : array 1.20 of Char;ptr : PChar; /PChar 可以看作 Charbeginptr := a;Inc(ptr); / 这句等价于 C 的 ptr+;Inc(ptr, 2); /这句等价于 C 的 ptr+=2;end;六、动态内存分配。C中,使用malloc()库函数分配内存,free()函数释放内存。如这样的代码:int *ptr, *ptr2;int i;ptr = (int*) malloc(sizeof(int) * 20);ptr2 = ptr;for (i=0; i20; i+)*ptr = i;ptr+;free(ptr2);Object Pascal中,动态分配内存的函数是GetMem(),与之对应的释放函数为FreeMem()(传统Pascal中获取内存的函数是New()和 Dispose(),但New()只能获得对象的单个实体的内存大小,无法取得连续的存放多个对象的内存块)。因此,与上面那段C的代码等价的Object Pascal的代码为:var ptr, ptr2 : integer;i : integer;beginGetMem(ptr, sizeof(integer) * 20); /这句等价于C的 ptr = (int*) malloc(sizeof(int) * 20);ptr2 := ptr; /保留原始指针位置for i := 0 to 19 dobeginptr := i;Inc(ptr);end;FreeMem(ptr2);end;对于以上这个例子(无论是C版本的,还是Object Pascal版本的),都要注意一个问题,就是分配内存的单位是字节(BYTE),因此在使用GetMem时,其第二个参数如果想当然的写成 20,那么就会出问题了(内存访问越界)。因为GetMem(ptr, 20);实际只分配了20个字节的内存空间,

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论