《2022年对ObjectPascal编译器给类对象分配堆内存细节的一种大胆猜测 .pdf》由会员分享,可在线阅读,更多相关《2022年对ObjectPascal编译器给类对象分配堆内存细节的一种大胆猜测 .pdf(12页珍藏版)》请在taowenge.com淘文阁网|工程机械CAD图纸|机械工程制图|CAD装配图下载|SolidWorks_CaTia_CAD_UG_PROE_设计图分享下载上搜索。
1、对 Object Pascal编译器给类对象分配堆内存细节的一种大胆猜测读过我以前写的文章的网友,都知道我是一个喜欢“刨根问底”、“死钻牛角尖”的家伙。最近由于工作需要转学DELPHI,在接触 Object Pascal之后,果然领会到了它的整洁和优美,怪不得连程序设计语言:设计与实现一书的作者也称赞 pascal 是“一种极优美的语言”。但在学习过程中遇到了好多问题,特别是对于像我这样由C+转至 OP对 Object Pascal 的简称 学习的人,由于两种语言风格不同,问号就会更多了。其中,OP和 C+语言的一个很大的区别就是:类对象 或称之为类实例 的内存分配机制不同。其中有两方面要说:
2、一、什么时候分配?在 C+中,定义了对象,那么马上分配其内存,之后调用其构造函数,这个内存可能在堆中,也可能在栈内,也可能在全程数据区内。但OP却截然不同,定义一对象,如:obj:TObject;只是为其分配了4字节的一个指针空间,而真正的对象空间还没有分配,那怎么用?在使用前当然要给对象分配空间,不然就会造成访问内存出错,给对象分配空间的办法也很简单:obj:=Tobject.Create;就 OK,这个对象空间是分配在堆内的,大家知道,栈内空间可以在使用期过后自动回收,但堆内存需要程序员自己管理,所以在使用完类对象之后,别忘了 obj.Free 真正实现析构的是obj.Destroy,但
3、obj.free是一种更安全的方式。“什么时候分配”这个问题在OP和 C+上的答案确实不同,但还不至于让我“疑惑”。知道了 OP类对象是通过调用这样的语句(构造函数):obj:=Tobject.Create;来得到堆内存的,但在这个处理细节上,编译器在内部是如何实现分配堆内存的呢?请看下一个问题:二、OP编译器是如何分配的内存?首先要感谢Lippman 的Inside C+Ojbect Model,这是一本不可多得的好书,她告诉了你对于C+编译器实现的一些你最迷惑、也是最想关心的细节,但不知DELPHI业界内有没有这样一本书,可以让我清楚的了解到OP编译器具体 具体到每个细节 是如何给一个类对
4、象分配堆内存的 如果有这样的书,您一定要通知我:?我大胆的做了猜测!名师资料总结-精品资料欢迎下载-名师精心整理-第 1 页,共 12 页 -一些小动作都是在Tobject类内部事先已经定义好的!下面让我们来关注一下这几个 Tobject类方法(Tobject定义于 System.pas):TObject=class,constructor Create;procedure Free;class function InitInstance(Instance:Pointer):TObject;procedure CleanupInstance;class function InstanceSiz
5、e:Longint;class function NewInstance:TObject;virtual;procedure FreeInstance;virtual;destructor Destroy;virtual;end;从方法的名称上我们能隐约的感觉到:NewInstance 和 FreeInstance肯定和类对象的构造和析构有些关联!先来分析一下NewInstance:class function TObject.NewInstance:TObject;begin Result:=InitInstance(_GetMem(InstanceSize);end;只有一句代码,但却调用
6、了三个其它方法:1、class function TObject.InstanceSize:Longint;begin Result:=PInteger(Integer(Self)+vmtInstanceSize);end;这个方法是OP类实现 RTTI 的一个重要方法,它能返回类对象所需要占用堆内存的大小,注意它并非是类对象所占有内存大小,因为类对象是一指针,那么在32 位环境下,指针永远是4 字节!名师资料总结-精品资料欢迎下载-名师精心整理-第 2 页,共 12 页 -大家可能对这句代码比较疑惑Result:=PInteger(Integer(Self)+vmtInstanceSize)
7、;下面我定义一个OP类:TBase=class(TObject)x:Integer;y:Double;constructor Create;end;然后分配内存:b:Tbase;b:=TBase.Create;我设想分配后的内存布局应是这样的 按 C+对象的内存考虑联想的:再来看这句:Result:=PInteger(Integer(Self)+vmtInstanceSize);它的目的是取到 VMT中 Index=-40注意:常量vmtInstanceSize=-40的格子中的内容。大家看这里的Self 变量是什么值呢?是b 的值也就是VPTR的 ADDRESS 吗?绝对不是!因为程序在执行
8、到 TObject.InstanceSize时只是想通过调用它知道得划分多少堆内存,但还没有正式分配堆内存,也就是说,VPTR、X、Y还不存在 但 VMT是和类一同建立起来的,它包含了和类有关的一些信息,如类实例需要请求的堆内存的大小等等,当然这个 Self也就不能是b 的值了,我猜测它的内容是VMT中 index=0 的格子的 Address,只有这样,这里的代码和下面要讲的代码才能被正常解释,但,Self 是怎么被 Assigned 为这个值的,我想是编译器所做的处理吧。这样,Result:=PInteger(Integer(Self)+vmtInstanceSize)自然得到了类对象所需
9、要堆内存大小的信息!为了证明我上面的猜测是正确的,大家可以实验以下代码:var b:Tbase;名师资料总结-精品资料欢迎下载-名师精心整理-第 3 页,共 12 页 -size_b:Integer;begin b:=TBase.Create;ShowMessage(Format(InitanceSize of TBase:%d,b.InstanceSize);size_b:=PInteger(PInteger(b)-40);ShowMessage(Format(InitanceSize of TBase:%d,size_b);,end;大家可以看到,两种方法得到的是同一个值!好,现在我们回过
10、头来讲解TObject.NewInstance中要调用的第二个函数。2、function _GetMem(Size:Integer):Pointer;它在 System.pas 中的定义如下:function _GetMem(Size:Integer):Pointer;$IF Defined(DEBUG)and Defined(LINUX)var Signature:PLongInt;$IFEND begin if Size 0 then begin$IF Defined(DEBUG)and Defined(LINUX)Signature:=PLongInt(MemoryManager.Get
11、Mem(Size+4);if Signature=nil then Error(reOutOfMemory);Signature:=0;Result:=Pointer(LongInt(Signature)+4);$ELSE Result:=MemoryManager.GetMem(Size);if Result=nil then 名师资料总结-精品资料欢迎下载-名师精心整理-第 4 页,共 12 页 -Error(reOutOfMemory);$IFEND end else Result:=nil;end;具体代码就不分析了,但我们终于看到了OP中分配堆内存的具体函数,原来是OP是通过一个内存
12、管理器MemoryManager来管理类对象所取得的堆内存空间的!TObject.NewInstance中第三个调用的方法:3、class function TObject.InitInstance(Instance:Pointer):TObject;$IFDEF PUREPASCAL var IntfTable:PInterfaceTable;ClassPtr:TClass;I:Integer;begin FillChar(Instance,InstanceSize,0);PInteger(Instance):=Integer(Self);ClassPtr:=Self;while Class
13、Ptr nil do begin IntfTable:=ClassPtr.GetInterfaceTable;if IntfTable nil then for I:=0 to IntfTable.EntryCount-1 do with IntfTable.EntriesI do begin if VTable nil then PInteger(PChar(Instance)IOffset):=Integer(VTable);end;名师资料总结-精品资料欢迎下载-名师精心整理-第 5 页,共 12 页 -ClassPtr:=ClassPtr.ClassParent;end;Result:
14、=Instance;end;$ELSE asm PUSH EBX PUSH ESI PUSH EDI MOV EBX,EAX MOV EDI,EDX STOSD MOV ECX,EBX.vmtInstanceSize XOR EAX,EAX PUSH ECX SHR ECX,2 DEC ECX REP STOSD POP ECX AND ECX,3 REP STOSB MOV EAX,EDX MOV EDX,ESP 0:MOV ECX,EBX.vmtIntfTable TEST ECX,ECX JE 1 PUSH ECX 1:MOV EBX,EBX.vmtParent TEST EBX,EBX
15、 JE 2 名师资料总结-精品资料欢迎下载-名师精心整理-第 6 页,共 12 页 -MOV EBX,EBX JMP 0 2:CMP ESP,EDX JE 5 3:POP EBX MOV ECX,EBX.TInterfaceTable.EntryCount ADD EBX,4 4:MOV ESI,EBX.TInterfaceEntry.VTable TEST ESI,ESI JE 4a MOV EDI,EBX.TInterfaceEntry.IOffset MOV EAX+EDI,ESI 4a:ADD EBX,TYPE TInterfaceEntry DEC ECX JNE 4 CMP ESP
16、,EDX JNE 3 5:POP EDI POP ESI POP EBX end;$ENDIF 刚才知道 _GetMem 已经得到了堆内存空间,而我们现在要讨论的这个方法是进行一些必须的初始化。其它代码不管,只看这两句:FillChar(Instance,InstanceSize,0);PInteger(Instance):=Integer(Self);第一就是给类对象清零,现在我们知道为什么OP的类实例的字段会自动被初始化为零了吧 String就为空,指针就为nil!第二条语句,是让VTPR指针指向 VMT表的 0 号格子 读者请参考结构图自行分析,此处也证明上面我对Self 值的猜测的正确
17、性。名师资料总结-精品资料欢迎下载-名师精心整理-第 7 页,共 12 页 -到了这里,你也许会说,说了半天,都是猜测,或许,OP编译器根本就不会调用那个 TObject.NewInstance方法呢!问得好,再做实验!还是以上面的那个Tbase 类为例,重载TObject.NewInstance方法,如下:TBase=class(TObject)x:Integer;y:Double;class function NewInstance:TObject;override;procedure FreeInstance;override;constructor Create;end;实现 cons
18、tructor TBase.Create;begin self.x:=2;self.y:=3.14;end;procedure TBase.FreeInstance;begin inherited;ShowMessage(Format(Call%s.FreeInstance!,self.ClassName);end;class function TBase.NewInstance:TObject;begin ShowMessage(Format(call%s.NewInstance,self.ClassName);result:=inherited NewInstance;end;名师资料总结
19、-精品资料欢迎下载-名师精心整理-第 8 页,共 12 页 -之后进行简单的声明对象:var b:Tbase;begin b:=Tbase.Create;在这里设断点!b.Free;end;通过对代码进行跟踪果然在一进入Create 就马上调用NewInstance 方法。说明:一定要重载它才能跟踪到它,在断点处,观察CPU,从反汇编后的代码中可以发现,是先调用一个_ClassCreate,然后才调用NewInstance 用同样的方法可以分析出b.Free 会最终调用到FreeInstance;来释放对象。我想基本上大的问题已经说请了,Object Pascal为了实现分配堆内存,在你调用构
20、造器的时候:b:=Tbase.Create;在构造方法内你的代码前,安插了代码调用NewInstance 方法,析构时,则在析构函数中你的代码后,调用FreeInstance函数。那么,现在再来看这种情况:派生TBase=class(TObject)x:Integer;y:Double;class function NewInstance:TObject;override;procedure FreeInstance;override;constructor Create;end;TSub=class(TBase)m:Integer;名师资料总结-精品资料欢迎下载-名师精心整理-第 9 页,共
21、 12 页 -n:Double;constructor Create;end;实现 constructor TBase.Create;begin self.x:=2;self.y:=3.14;end;procedure TBase.FreeInstance;begin inherited;ShowMessage(Format(Call%s.FreeInstance!,self.ClassName);end;class function TBase.NewInstance:TObject;begin ShowMessage(Format(call%s.NewInstance,self.Class
22、Name);result:=inherited NewInstance;end;TSub constructor TSub.Create;begin inherited Create;注意这里!名师资料总结-精品资料欢迎下载-名师精心整理-第 10 页,共 12 页 -self.m:=4;self.n:=12.32;end;我们已经知道,var s:Tsub;s:=Tsub.Create;时,在进入 Tsub.Create内部马上得到了它想要的内存 这里是 32 字节,与内存对齐有关,类里含有最大数double:8 字节,所以总长度要8 的倍数 ,那么当:inherited Create;时,
23、在 Tbase.Create内部,还有内存分配的动作吗?我们可以通过三点证明:这里,Tbase.Create只是完成程序员给出的初始化代码,没有进行内存分配的动作。第一点,ReturnValue:=inherited Create;所得到的返回地址和调用Tsub.Create所得到的返回地址相同。第二点,如果在Tbase.Create内部又分配新的内存,那么self.x:=2;self.y:=3.14;只是针对新的内存操作,而原来的 S 对象中从 TBASE 中继承来的X,Y不会变,还是0,但我们发现,S 中的 X,Y已经改变,所以也可以证明Tbase.Create没有分配新的内存,只是对原有
24、内存中的X,Y 进行设置。第三点,跟踪。这是最简单,最一目了然的方法,看看inherited Create;到底有没有调用 NewInstance,实验证明,跟本没有调用。但是,如果把 Tsub.Create中的 inherited Create;改为 Tbase.Create;情况则大不同了,用上面三种方式发现,它又分配了新的堆内存,这样不但没有达到程序员初始化数据的目的,反而造成了内存泄漏,而这样的BUG 是很难找到的。也就是说,编译器发现如果是通过类来调用构造函数,就会当成是新的类对象进行构造、分配堆内存,如果是在构造器内部inherited Create;只是按常规的处理类方法 的方式进行处理。我想,对于Anders HejlsbergDELPHI设计者 ,想在编译器中实名师资料总结-精品资料欢迎下载-名师精心整理-第 11 页,共 12 页 -现这样的功能并非一件难事 实际上,我们通过查看汇编代码也能分析出个中原由,有兴趣者请注意其中的TEST d1,d1 指令和其下的跳转指令。PS:刚才被网友告知有本书叫delphi 的原子世界,我很想得到它,如果您手上有它的 E-BOOK 版,希望您能发给我: 名师资料总结-精品资料欢迎下载-名师精心整理-第 12 页,共 12 页 -