部分数据类型的变量是有初始化与结束化过程的。在什么时候调用这些初始化和结束化过程,取决于所定义变量的生命周期。对于局部变量来说,其生命周期自进入例程开始,到结束例程时终出。也就是说,局部变量的初始化与结束化过程,在例程的初始化与结束化过程中发:。
具有初始化与结束化过程的数据类型如表4-1所示。
4.1.1初始化的必要性
为什么要初始化?或者更进一步地提出问题:为什么仅有这些类型要被初始化呢?
应该注意到,上表与“表2-3发生引用计数的数据类型”和“表2-4有‘增加引用’行为的数据类型”有相似之处。事实上,正是“引用计数机制”使得这些数据类型的变量必须被初始化。
以长字符串为例,这样的代码:
var Str:String; begin Str:='abc'; end;
在执行“Str:='abc';”时,系统将调用_LStrAsg()来赋值。而_LStrAsg()中,如果“Pointer(Str)<oNi1”,则需要将Str的引用计数减1。这时,如果局部变量Str未经初始化就被使用,则Pointer(Str)就是一个指向随机地址的指针,这将导致_LStrAsg()中的引用计数操作可能访问非法地址。
在这种情况下,必须将变量初值置为Nil,以使得其他内部例程可以确认该变量并没有在堆上分配内存。
4.1.2如何初始化
初始化的目的就是将变量初值置为Nil。
Delphi中,对于单独定义的长字符串、宽字符串、动态数组、变体以及接口变量,会直接操作栈来完成初始化。关于这一点,可以参见第3章中“完全汇编例程与内嵌汇编例程”的有关描述。
记录和数组的初始化如同它们的“增加引用”一样,也是操作其中的一些子域。所使用的
“初始化表”与“增加引用”中使用的“引用列表(数组)”是同一个表。
记录的“引用列表”中记录了所有具有引用计数特征的域。数组的引用列表要相对简单一些,只有数组基于长字符串、宽字符串、动态数组、变体以及接口变量定义时,才会有一个记录来保存基类型的信息。
以数组为例,其初始化代码如下:
procedure _InitializeArray(p: Pointer; typeInfo: Pointer;elemCount: Cardinal); var FT: PFieldTable; //… case PTypeInfo (typeInfo). Kind of tkArray: begin FT:=PFieldTable(Integer(typeInfo)+ Byte(PTypeInfo(typeInfo). Name[0])); while elemCount>0 do begin Initializearray(P, FT. Fields[0]. TypeInfo", FT. Count); Inc(Integer(P), FT. Size); Dec(elemCount); end; end; end;
这里使用FT变量来定义初始化记录的字段表。由于最多只有一个记录,所以FT.Fields[0].TypeInfo代表了所有数组元素的数据类型。考虑到多维数组的情况,这里递归调用_InitializeArray()来初始化每一个数组元素。
4.1.3如何结束化
结束化过程要相对简单得多。局部变量在栈上占用的空间通过栈操作即可释放。记录和数组中的元素可以通过初始化表来结束化。对于不在初始化表中的域或数组元素,在释放记录和数组占用的栈空间的同时就被释放了。
如果变量、域(记录)或元素(数组)具有引用计数特征,那么结束化过程中,都必将调用到相应数据类型中减少引用计数的例程—例如长字符的内部例程_LStrC1r()和_LStrArrayClr()。
Newl与Initialize0如果是全局变量,则记录变量的空间被分配在数据区,这会发生两种情况:
- 操作系统的文件装载程序会自动将BSS节初始化为0,因此无初值的记录变量的初始化可以自动完成;
- 有初值的记录变量不存在初始化的问题。由于它被放在DATA节中,因而也不会被自动初始化。
如果记录(而非记录指针)是局部变量,那么会有整个记录大小的空间直接在栈上分配,这时编译器会在例程初始化代码中加入_InitializeRecord()来初始化这个变量。例如:
type TRec=record i:integer; s:String; end; procedure Initializerest; var r:TRec; begin r.i:=3; end;
这段代码在CPU窗体的汇编代码是这样的:
Project2. dpr.16: procedure Initializerest; Project2. dpr.17: begin 00407D4055push ebp 00407D41 8BEC mov ebp, esp 00407D43 83C4F8 add esp,-$08//在栈上分配Sizeof(TRec)大小的空间 00407D46 8D45F8 lea eax,[ebp-$08]//取变量r的地址 00407D49 8B15247D4000 mov edx,[$00407d24]// TypeInfo(TRec) 00407D4F E8D8BCFFFF call @InitializeRecord//记录初始化 00407D54 33C0 xor eax,eax//以下代码向栈上填入一个异常帧 00407D56 55 push ebp 00407D57 68857D4000 push $00407d85 00407D5C 64FF30 push dword ptr fs:[eax] 00407D5F 648920 mov fs:[eax],esp Project2.dpr.18:r.i:=3;
编译器在栈上填入异常帧,这使得无论例程InitializeTest()中的代码运行情况如何,例程结束化时的_FinalizeRecord()调用都必然被执行到。这相当于如下的代码:
InitializeRecord(r,TypeInfo(TRec)); try //1.以上代码被编译在begin关键字的实现代码中 //在InitializeTest()中的代码 //2.以下代码被编译在end关键字的实现代码中 finally _FinalizeRecord() end;
但如果是记录指针,那么开发人员通常会调用New()例程来为它分配空间。为此编译器在New()例程中调用了_Initialize()来初始化:
procedure _Initialize(p:Pointer;typeInfo:Pointer); begin _InitializeArray(p,typeInfo,1); end; function _New(size:Longint;typeInfo:Pointer):Pointer; begin GetMem(Result,size); if Result <>nil then _Initialize(Result,typeInfo); end;
接下来的代码,看起来像是一个怪圈:_InitializeRecord()将再调用例程InitializeArray()。_InitializeRecord()的实现代码如下:
procedure _InitializeRecord(p: Pointer; typeInfo: Pointer); var FT: PFieldTable; I: Cardinal; begin FT:=pFieldTable(Integer(typeInfo)+Byte(PTypeInfo(typeInfo). Name[0])); for I:=FT. Count-1 downto 0 do _InitializeArray(Pointer(Cardinal(P)+FT. Fields[I]. Offset), FT. Fields[I]. TypeInfo^,1); end;
_InitializeRecord()从类型信息记录(FieldTable)中列举每一个使用了引用机制的域,并将每个域作为“一个元素的数组”,再次调用_InitializeArray()来初始化该域的值。
可以这样来说明_InitializeRecord()与_Initialize()之间的差异:
_Initialize()中将记录作为“一个元素的数组”传入_InitializeArray();而在_InitializeRecord()中,将通过typeInfo列举记录中每一个需要初始化的域,并将该域(地址)作为“一个元素的数组”传入 _InitializeArray()。