对ObjectPascal编译器给类对象分配堆内存细节的一种大胆猜测_第1页
对ObjectPascal编译器给类对象分配堆内存细节的一种大胆猜测_第2页
对ObjectPascal编译器给类对象分配堆内存细节的一种大胆猜测_第3页
对ObjectPascal编译器给类对象分配堆内存细节的一种大胆猜测_第4页
对ObjectPascal编译器给类对象分配堆内存细节的一种大胆猜测_第5页
已阅读5页,还剩7页未读 继续免费阅读

下载本文档

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

文档简介

1、对Object Pascal编译器给类对象分配堆内存细节的一种大胆猜测读过我以前写的文章的网友,都知道我是一个喜欢“刨根问底”、“死钻牛角尖”的家伙。最近由于工作需要转学DELPHI,在接触Object Pascal之后,果然领会到了它的整洁和优美,怪不得连程序设计语言:设计与实现一书的作者也称赞pascal是“一种极优美的语言”。但在学习过程中遇到了好多问题,特别是对于像我这样由C+转至OP对Object Pascal的简称学习的人,由于两种语言风格不同,问号就会更多了。其中,OP和C+语言的一个很大的区别就是:类对象或称之为类实例的内存分配机制不同。其中有两方面要说:一、什么时候分配?在C

2、+中,定义了对象,那么马上分配其内存,之后调用其构造函数,这个内存可能在堆中,也可能在栈内,也可能在全程数据区内。但OP却截然不同,定义一对象,如:obj : TObject;只是为其分配了4字节的一个指针空间,而真正的对象空间还没有分配,那怎么用?在使用前当然要给对象分配空间,不然就会造成访问内存出错,给对象分配空间的办法也很简单:obj := Tobject.Create;就OK,这个对象空间是分配在堆内的,大家知道,栈内空间可以在使用期过后自动回收,但堆内存需要程序员自己管理,所以在使用完类对象之后,别忘了 obj.Free 真正实现析构的是obj.Destroy,但obj.free是一

3、种更安全的方式。“什么时候分配”这个问题在OP和C+上的答案确实不同,但还不至于让我“疑惑”。知道了OP类对象是通过调用这样的语句(构造函数):obj := Tobject.Create;来得到堆内存的,但在这个处理细节上,编译器在内部是如何实现分配堆内存的呢?请看下一个问题:二、OP编译器是如何分配的内存?首先要感谢Lippman的Inside C+ Ojbect Model,这是一本不可多得的好书,她告诉了你对于C+编译器实现的一些你最迷惑、也是最想关心的细节,但不知DELPHI业界内有没有这样一本书,可以让我清楚的了解到OP编译器具体具体到每个细节是如何给一个类对象分配堆内存的 如果有这

4、样的书,您一定要通知我: ?我大胆的做了猜测!一些小动作都是在Tobject类内部事先已经定义好的!下面让我们来关注一下这几个Tobject类方法(Tobject定义于System.pas): TObject = class constructor Create; procedure Free; class function InitInstance(Instance: Pointer): TObject; procedure CleanupInstance; class function InstanceSize: Longint; class functi

5、on NewInstance: TObject; virtual; procedure FreeInstance; virtual; destructor Destroy; virtual; end;从方法的名称上我们能隐约的感觉到:NewInstance和FreeInstance肯定和类对象的构造和析构有些关联!先来分析一下NewInstance:class function TObject.NewInstance: TObject;begin Result := InitInstance(_GetMem(InstanceSize);end;只有一句代码,但却调用了三个其它方法:1、clas

6、s function TObject.InstanceSize: Longint;begin Result := PInteger(Integer(Self) + vmtInstanceSize);end;这个方法是OP类实现RTTI的一个重要方法,它能返回类对象所需要占用堆内存的大小,注意它并非是类对象所占有内存大小,因为类对象是一指针,那么在32位环境下,指针永远是4字节!大家可能对这句代码比较疑惑Result := PInteger(Integer(Self) + vmtInstanceSize);下面我定义一个OP类:TBase = class(TObject) x : Integer

7、; 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吗?绝对不是!因为程序在执行到TObject.InstanceSize时只是想通过调用它知道

8、得划分多少堆内存,但还没有正式分配堆内存,也就是说,VPTR、X、Y还不存在但VMT是和类一同建立起来的,它包含了和类有关的一些信息,如类实例需要请求的堆内存的大小等等,当然这个Self也就不能是b的值了,我猜测它的内容是VMT中index = 0的格子的Address,只有这样,这里的代码和下面要讲的代码才能被正常解释,但,Self是怎么被Assigned为这个值的,我想是编译器所做的处理吧。这样,Result := PInteger(Integer(Self) + vmtInstanceSize)自然得到了类对象所需要堆内存大小的信息!为了证明我上面的猜测是正确的,大家可以实验以下代码:v

9、ar b :Tbase;size_b : Integer;beginb := 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;大家可以看到,两种方法得到的是同一个值!好,现在我们回过头来讲解TObject.NewInstance中要调用的第二个函数。2、function _GetMem(Size: Inte

10、ger): Pointer;它在System.pas中的定义如下:function _GetMem(Size: Integer): Pointer;$IF Defined(DEBUG) and Defined(LINUX)var Signature: PLongInt;$IFENDbegin if Size 0 then begin$IF Defined(DEBUG) and Defined(LINUX) Signature := PLongInt(MemoryManager.GetMem(Size + 4); if Signature = nil then Error(reOutOfMemo

11、ry); Signature := 0; Result := Pointer(LongInt(Signature) + 4);$ELSE Result := MemoryManager.GetMem(Size); if Result = nil then Error(reOutOfMemory);$IFEND end else Result := nil;end;具体代码就不分析了,但我们终于看到了OP中分配堆内存的具体函数,原来是OP是通过一个内存管理器MemoryManager来管理类对象所取得的堆内存空间的!TObject.NewInstance中第三个调用的方法:3、class fun

12、ction TObject.InitInstance(Instance: Pointer): TObject;$IFDEF PUREPASCALvar IntfTable: PInterfaceTable; ClassPtr: TClass; I: Integer;begin FillChar(Instance, InstanceSize, 0); PInteger(Instance) := Integer(Self); ClassPtr := Self; while ClassPtr nil do begin IntfTable := ClassPtr.GetInterfaceTable;

13、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; ClassPtr := ClassPtr.ClassParent; end; Result := Instance;end;$ELSEasm PUSH EBX PUSH ESI PUSH EDI MOV EBX,EAX MOV EDI,EDX STOSD

14、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,ESP0: MOV ECX,EBX.vmtIntfTable TEST ECX,ECX JE 1 PUSH ECX1: MOV EBX,EBX.vmtParent TEST EBX,EBX JE 2 MOV EBX,EBX JMP 02: CMP ESP,EDX JE 53: POP EBX MOV ECX,EBX.TInterfaceTable.

15、EntryCount ADD EBX,44: MOV ESI,EBX.TInterfaceEntry.VTable TEST ESI,ESI JE 4a MOV EDI,EBX.TInterfaceEntry.IOffset MOV EAX+EDI,ESI4a: ADD EBX,TYPE TInterfaceEntry DEC ECX JNE 4 CMP ESP,EDX JNE 35: POP EDI POP ESI POP EBXend;$ENDIF刚才知道_GetMem已经得到了堆内存空间,而我们现在要讨论的这个方法是进行一些必须的初始化。其它代码不管,只看这两句:FillChar(Ins

16、tance, InstanceSize, 0);PInteger(Instance) := Integer(Self);第一就是给类对象清零,现在我们知道为什么OP的类实例的字段会自动被初始化为零了吧String就为空,指针就为nil!第二条语句,是让VTPR指针指向VMT表的0号格子读者请参考结构图自行分析,此处也证明上面我对Self值的猜测的正确性。到了这里,你也许会说,说了半天,都是猜测,或许,OP编译器根本就不会调用那个TObject.NewInstance方法呢!问得好,再做实验!还是以上面的那个Tbase类为例,重载TObject.NewInstance方法,如下:TBase =

17、class(TObject) x : Integer; y : Double; class function NewInstance: TObject; override; procedure FreeInstance; override; 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!,sel

18、f.ClassName);end;class function TBase.NewInstance: TObject;begin ShowMessage(Format(call %s.NewInstance,self.ClassName); result := inherited NewInstance;end;之后进行简单的声明对象:varb : Tbase;beginb := Tbase.Create; 在这里设断点!b.Free;end;通过对代码进行跟踪果然在一进入Create就马上调用NewInstance方法。说明:一定要重载它才能跟踪到它,在断点处,观察CPU,从反汇编后的代码中

19、可以发现,是先调用一个_ClassCreate,然后才调用NewInstance 用同样的方法可以分析出b.Free会最终调用到FreeInstance;来释放对象。我想基本上大的问题已经说请了,Object Pascal为了实现分配堆内存,在你调用构造器的时候:b := Tbase.Create;在构造方法内你的代码前,安插了代码调用NewInstance方法,析构时,则在析构函数中你的代码后,调用FreeInstance函数。那么,现在再来看这种情况:派生TBase = class(TObject) x : Integer; y : Double; class function NewIn

20、stance: TObject; override; procedure FreeInstance; override; constructor Create; end; TSub = class (TBase) m : Integer; 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.FreeIn

21、stance!,self.ClassName);end;class function TBase.NewInstance: TObject;begin ShowMessage(Format(call %s.NewInstance,self.ClassName); result := inherited NewInstance;end; TSub constructor TSub.Create;begin inherited Create; 注意这里! self.m := 4; self.n := 12.32;end;我们已经知道,vars : Tsub;s := Tsub.Create;时,在进入Tsub.Create内部马上得到了它想要的内存这里是32字节,与内存对齐有关,类里含有最大数double:8字节,所以总长度要8的倍数,那么当:inherited Create;时,在Tbase.Create内部,还有内存分配的动作吗?我们可以通过三点证明:这里,Tbase.Create只是完成程序员给

温馨提示

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

评论

0/150

提交评论