编译器实现了基础数据类型(以及相关的强制转换机制),最小化的Delphi内核实现了可执行模块装载程序。因此,利用这样的最小化内核能够编译的应用只能做到“数据结构十内置运算符”。
在Delphi中使用数据结构,并不是要编译器简单地理解这些数据结构。更重要的是,必须有一套相关的内核函数和过程的支持。在源代码中,这些与基本数据结构相关的例程名通常是以“”开始的,它们虽然声明在Interface节中,却无法在用户代码中直接调用。这些例程包括:
- 标准Pascal的内置例程;
- 短字符串操作例程;
- 长字符串操作例程;
- 宽字符串操作例程;
- 未分类的字符串操作例程;
- 集合操作例程;
- 数组(含动态数组)操作例程;
- 记录操作例程。
2.4.1标准Pascal的内置例程
procedure _COS; procedure _EXP; procedure _INT; procedure _SIN; procedure _FRAC; procedure _ROUND; procedure _TRUNC; procedure _Pow10;
Delphi通过内嵌汇编,用浮点运算指令完成了上述例程。在这部分的(包括其后的)代码中,有一个有趣的注释:
{Procedures and functions that need compiler magic) //需要编译魔法的程序和函数
应该注意到,COS()的接口定义与Delphi帮助中并不相同。在帮助中,定义是:
function Cos(X:Extended):Extended;
这样的差异也表现在其他的例程中。因此,“compiler magic”表明了Delphi实际对这些内部例程并不进行强类型检测。在编译期,内部例程的参数入口、寄存器的使用、类型检测和返回值等,编译器中都是用专门的代码来处理的。
有些时候,对于同一个函数声明,在内部需要使用多个函数来实现。这时,也会用到“compiler magic”。例如function writeln(),在内核中,使用了超过25个函数来实现writeln(),以应付不同的入口参数和自Pascal以来形成的古怪的语法。
此外,“compiler magic”还表明可以用内置例程来替代语言的操作符和运算符。例如在后面将提到的“PChar类型字符串操作例程”和“集合操作例程”等例程中,就大量地使用了这种编译器支持的技术。
function Pi也是使用“compiler magic”的一个典型例子。在System.pas中,不可能找到PI(O的实现代码,因为这个函数是在编译期计算的。看起来,它更像是一个常量,而不是函数。下面的汇编码是将“function Pi”的结果赋给局部变量e:
Unitl. pas.29:e:=pi; 0044C878 C745F035C26821 mov [ ebp-$10],$2168c235 0044C87F C745F4A2DAOFC9 mov [ ebp-$0c], Sc90fdaa2 0044C886 66C745F80040 mov word ptr [ ebp-$08],$4000
在Delphi中,Pi的十六进制值是$4000C90FDAA22168C235。上面的代码,其实就是直接修改栈上的变量e,而不是通常理解的“将函数Pi()的结果返回到变量e”。
2.4.2字符串操作例程
■短字符串操作例程
//表达式、运算和运算符 procedure _PStrCat; procedure _PStrNCat; procedure PStrCpy(Dest:PShortstring; Source:PShortString); procedure _PStrNCpy(Dest:PShortstring; Source:PShortString; MaxLen:Byte); procedure _PStrcmp; //操作例程 procedure _Copy{(s:Shortstring; index,count:Integer):ShortString}; procedure _Delete{(vars:openstring; index,count:Integer)}; procedure _Insert{source:Shortstring;var s:openstring; index:Integer}; procedure _Pos{substr:ShortString;s:Shortstring}:Integer}; procedure _SetLength{s:PShortstring;newLength:Byte}; //类型转换 procedure _SetString(s:PShortString; buffer:PChar; len:Byte);
基本上,这些例程都是用来替代运算符或实现运算表达式的。下例对照地进行说明:
var s1,s2,s3:String[20]; begin s1:=s2i //call procedure _PStrNCpy() s2:=s1+s2: //call procedure _PStrCpy(),and more s1:=s1+s2; //call procedure _PStrNCat() end;
在上面的源码所对应的汇编码中,“s2:=s1+s2;”是很有代表性的:
Unit1.pas.36:s2:=s1+s2;//call procedure _pStrCpy() 0044C8D6 8D55DB 1ea edx,[ebp-$25] //edx暂存s1地址 0044C8D9 8D85B4FEFFFF lea eax,[ebp-$0000014c] //eax暂存中间字符串(temp)地址 0044C8DF E83061FBFF call epstrcpy //temp:=s1 0044C8E4 8D55C6 1ea edx,[ebp-$3a] 0044C8E7 8D85B4FEFFFF 1ea eax,[ebp-$0000014c] 0044C8ED B128 mov c1,$28 //最大长度为$28 0044C8EF E8FO60FBFF ca11 ePStrNCat //temp:=temp+s2 0044C8F4 8D95B4FEFFFF 1ea edx,[ebp-$0000014c] 0044C8FA 8D45C6 lea eax,[ebp-$3a] 0044C8FD B114 mov cl,$14/最大长度为$14 0044C8FF E81C61FBFF ca11 ePStrNCpy //从temp中复制值到s2
这里,[ebp-$0000014c]是暂存的一个中间字符串(temp)。三次内部例程调用都进行了复制操作,而且,从temp到s2的PStrNCpy()操作还表明:如果s2不够长,则在以前的代码中,“temp:=temp+s2”会有一些不必要的内存复制操作。
简而言之,在Delphi内部实现上,短字符串是一个低效率的字符串类型——不要以为在Delphi中,短字符串的效率会如同它在Pascal中一样高。
■长字符串操作例程
//表达式、运算和运算符 procedure _LStrAsg(var dest; const source); procedure _LStrLAsg(var dest; const source); procedure _LStrCat{var dest:Ansistring;source:AnsiString)}; procedure _LStrCat3{var dest:Ansistring;sourcel:AnsiString;source2:Ansistring}; procedure _LStrCatN{var dest:Ansistring;argcnt:Integer;…);}; procedure _LstrCmp{left:Ansistring;right:Ansistring}; //类型转换 procedure _IStrPromPCharLen(var Dest:Ansistring;Source:PAnsiChar;Length:Integer); procedure _LStrFromPWCharten(var Dest:Ansistring;Source:PWidechar;Length:Integer); procedure _LStrFromchar(var Dest:AnsiString;Source:Ansichar); procedure _LStrFromwchar(var Dest:Ansistring;Source:Widechar); procedure _LStrFrompChar (var Dest:Ansistring;Source:PAnsichar); procedure _LStrFrompwChar (var Dest:Ansistring;Source:pwidechar); procedure _LStrFromstring(var Dest:Ansistring;const Source:ShortString); procedure _LStrFromArray(var Dest:Ansistring;Source:PAnsichar;Length:Integer); procedure _LStrFromWArray(var Dest:Ansistring; Source:pWidechar; Length:Integer); procedure _LStrFromwstr(var Dest:Ansistring; const Source:Widestring); procedure _LStrTostring{(var Dest:Shortstring;const Source:Ansistring;MaxLen:Integer)}; function _LStrTopChar(const s:Ansistring):pChar; //长度计数和引用计数位的操作 function _LStrLen(const:Ansistring):Longint; function _LStrAddRef(var str):Pointer; procedure _LStrClr(var S); procedure _LStrArrayClr(var StrArray; cnt:longint); //操作例程 procedure _LStrCopy{const:Ansistring;index,count:Integer):Ansistring}; procedure _LStrpeletet{var s:Ansistring;index,count:Integer}; procedure _LStrInsert{const source:Ansistring;vars:Ansistring;index:Integer}; procedure _LStrPos{const substr:Ansistring;consts:Ansistring):Integer}; procedure _LStrSetLength{var str:Ansistring;newtength:Integer}; procedure _LStrofchar{c:Char;count:Integer):Ansistring)}; function _Newansistring(length:Longint):Pointer;
很大一部分内核字符串例程的作用,是对运算符和表达式的替换。如下例:
var GAStr1:AnsiString; procedure TestAnsiStr; var AStr1,AStr2,AStr3:AnsiString; begin AStr1:=; //call procedure _Lstrclrt) AStrl:='abcde'; //cal1 Procedure _LStrLAsg() GAStr1 :='abcde'; //call Procedure LStrAsg() AStr1:=AStr1+'abcd'; //call procedure _LStrcat() if AStrl=GAStrl then //call procedure _LStrCmp() AStr1:=AStrl+'abcd'+'gqq'; //call procedure _LStrCatN() end;
其中,向局部变量AStr1赋值调用的是LStrLAsg(),而向全局变量GAStr1赋值时,调用的却是LStrAsg(),这种差异是值得注意的。
此外,运算中的类型兼容和强制类型转换也会导致内部函数的调用。如下例:
var AStr:Ansistring; CharArr:array [0..10] of char; D:PChar; begin //类型兼容是通过内核例程来支持的 Astrl := CharArr;//call procedure _LStrFromArray() //(部分)类型强制转换也需要询用内核例程 p := PChar(AStr);// call procedure _LStrToPChar() end;
部分内核函数的代码是值得仔细研读的。例如长字符串的引用计数管理实际上由两个例程来实现:
function _LStrAddRef(var str):Pointer; procedure _LStrClr(var S);
PUREPASCAL版本的实现代码如下:
function _LStrAddRef(var str):Pointer; P:PStrRec; begin P:=Pointer(Integer(str)-sizeof(StrRec));//取负偏移 if P<>nil then if P.refcnt >=0 then InterlockedIncrement(P.refcnt); Result:=Pointer(str); end; procedure _LStrClr(var S); var P:PStrRec; begin if Pointer(S)<>nil then begin P:=Pointer(Integer(S)-Sizeof(StrRec));//取负偏移 Pointer(S):=nil;//字符串置空,相当于s:= if P.refCnt>0 then //减一,如果减一后P.refcnt=0,则 //在堆上释放p指向的内存块(即字符串的内存占用,含负偏移) if InterlockedDecrement(P.refCnt)=0 then FreeMem(P); end; end;
代码中涉及了PStrRec记录。PStrRec记录是长字符串的内存占用的负偏移上的前8个字节的记录定义。定义如下:
type PStrRec="StrRec; StrRec=packed record refant:Longint; 1ength:Longint; end;
因此,可以直接用P.refcnt来修正引用计数,也可以用P.1ength来修改字符串长度。
_LStrAddRef()通过调用API InterlockedIncrement)将引用计数加1。
InterlockedIncrement()是一个原子操作函数,可以保证不会有两个操作同时作用于P.refcnt。类似地,LStrC1r()中使用InterlockedDecrement()来将引用计数减1。
在procedure_LStrLAsg()中可以见到对它们的调用:
procedure _LStrLAsg(var dest;const source); var P:Pointer; begin P:=Pointer(source); L.StrAddRef (P)i//增加 source的索引计数 P:=Pointer(dest); Pointer(dest):=Pointer(source);//将 dest的值直接指向 source LStrClr(P);//减少dest的索引计数.如果dest的内存空间不 //再被其他变量引用,则释放 end;
可以看出,长字符串赋值时,是将dest的值直接指向source,并同时修正source和dest的引用计数。
■宽字符串操作例程
//表达式、运算和运算符 procedure _wStrAsg(var Dest:Widestring;const Source:Widestring); procedure _WStrLAsg(var Dest:WideString;const Source:Widestring); procedure _wStrcat(var Dest:WideString;const Source:WideString); procedure _wstrcat3(var Dest:Widestring;const Sourcel,Source2:WideString); procedure _wStrCatN{var dest:Widestring;argcnt:Integer;..}; procedure _wStrCmp(left:Widestring;right:widestring); //类型转换 procedure _wStrFromPCharLen(var Dest:Widestring;Source:PAnsiChar;Length:Integer); procedure _wStrFrompwCharLen(var Dest:Widestring;Source:PWideChar;CharLength:Integer); procedure _WStrFromchar(var Dest:Widestring;Source:Ansichar); procedure _WStrFromwChar(var Dest:Widestring;Source:WideChar); procedure _WStrFromPChar(var Dest:Widestring;Source:PAnsichar); procedure _WStrFrompWChar(var Dest:Widestring;Source:PwideChar); rocedure _wStrFromString(var Dest:Widestring;const Source:ShortString); procedure _WStrFromArray(var Dest:Widestring;Source:PAnsiChar; Length:Integer); procedure _WStrFromwArray(var Dest:Widestring;Source:PWidechar;Length:Integer); procedure _wStrFromtStr(var Dest:Widestring;const Source:Ansistring); procedure _wStrTostring(Dest:PShortstring;const Source:WideString;MaxLen:Integer); function _wStrToPWChar(const S:Widestring):PWideChar; //长度计数和引用计数位的操作 function _wStrLen(const s:Widestring):Integer; procedure _WStrClr(var S); procedure _wStrArrayClr(var StrArray;Count:Integer); function _WStrAddRef(var str:WideString):Pointer; //操作例程 function _wStrcopy (consts:Widestring;Index,Count:Integer):WideString; procedure _wStrDelete(var S:WideString;Index,Count:Integer); procedure _wStrInsert(const Source:Widestring;var Dest:Widestring;Index:Integer); procedure _wStrpos{ const substr:Widestring;const s:Widestring):Integer)}; procedure _wStrSetLength(var s:widestring;NewLength:Integer); function _wstrofwchar (Ch:widechar;Count:Integer):Widestring; function _Newidestring(Chartength:Longint):Pointer;
不难看出,宽字符串与长字符串的内核例程是一一对应的。
但是,前面提到过“宽字符串没有引用计数位”。那么,内部例程中_WStrAddRef()有什么意义呢?显然,必须要有新的procedure_WStrAsg()实现方法以及虚拟的引用计数管理机制,才能适应宽字符串的内存结构。
具体的实现代码如下:
procedure _wStrAsg(var Dest:Widestring;const Source:Widestring); asm {->BAxPointer to Widestring } {EDX Pointer to data} TEST EDX,EDX JE _WStrClr//如果Source是空串,则Dest置空事 MOV ECX,[EDX-4]//取长度计数位 SHR ECX,1//Length(Source)div2 JE _WStrC1r//如果结果为0,则Dest置空串 PUSH ECX PUSH EDX PUSH EAX //按Source的长度重分配Dest的空间(以字为单位),并将Source内容复制到Dest CALL SysReA1locStringLen TEST EAX,EAX //函数调用错。内存不够。弹出系统错误:reoutofMemory JE WStrError end; function _WStrAddRef(var str:Widestring):Pointer; asm MOV EDX,[EAX] TEST EDX,EDX JE @@1 //如果str是空串,则返回空串 PUSH EAX//pointer(str)入栽暂存 MOV ECX,[EDX-4] SHR ECX,1 PUSH ECX PUSH EDX //调SysA11ocStringten(str,1ength(str));返回新字符率地址在EAX CALL SysA1locStringLen POPE DX//将暂存的 pointer(str)出栈 TEST EAX,EAX JE WStrError MOV [EDX],EAX//使str指向SysA1locstringten()返回的新串地址 @@1: end;
procedure _wstrclr(var s); asm {->EAX Pointer to widestring} MOV EDX,[EAX] TEST EDX,EDX JE @@1//如果是空串,则退出.否则将释放串 MOV DWORD PTR[EAX],0//置字符串的#0结束符 PUSH EAX PUSH EDX CALL SysFreestring//释放串 POP EAX @@1: end;
可以看到:
- _WStrASg()总是将一个源字符串的拷贝赋给目标字符串,而从不执行引用计数例程_WStrAddRef()。
- _WStrAddRefC)调用后,将会有一个新的字符串产生,且入口变量Str与返回值Resu1t都指向新字符串。
- WStrC1r()总是直接释放一个非空的字符串。
因此,这些函数并没有与长字符串例程相对应的引用计数功能。Delphi保留这些例程,从而使编译器能够以近乎同样的流程来处理两种不同的字符串。
■未分类的字符串操作例程
//字符串类型转换 procedure _CToPasStr(Dest:PShortString;const Source:PChar); procedure CLenToPasstr(Dest:PShortString;const Source:PChar;MaxLen:Integer); procedure ArrayTopasstr(Dest:PShortString;const Source:PChar;Len:Integer); procedure PasTocStr(const Source:PShortString;const pest:PChar); //过程Str()和Val() procedure _Str2Ext; procedure _StrOExt; procedure _StrlExt; procedure _StrLong (val,width:Longint;s:PShortstring); procedure _StrOLong(val:Longint;s:PShortString); procedure _ValExt; function _ValLong(const s:String;var code:Integer):Longint; //字符串惟一化操作 procedure Uniquestring(var str:Ansistring);overload; procedure Uniquestring(var str:Widestring);overload; procedure _UniquestringA(var str:AnsiString); procedure _Uniquestringw(var str:WideString);
在CStr(C语言风格的字符串,即Nu11结尾字符串)与短字符串的相互操作中,会用到上面的内部例程。例如:
var str: string[20]; cstr: PChar =' abcd'; Arr: array [0..10] of Char; begin str:=CStr;//call procedure _CToPasstr(), and _CLenopasstr() Str:=Arr;//call procedure CLenToPasStr() end;
过程Str()与Val()是Delphi中用来在字符串与数字之间进行转换的两个例程。根据入口参数的不同,编译器会将调用的实际代码指向上面的内部例程。例如:
var s:string[200]; e:Extended; I,L:Integer; begin Str(12:5,s);//call procedure _StrLong() Str(15,s);//call procedure _StrOLong() Str(PI,s);// call procedure _StrOExt() Val(s,I,L);// call function _ValLong() Val(s,e,i);// call procedure _ValExt() end;
字符串惟一化,是指创建一个引用计数为1的字符串的拷贝。四个例程的Linux版本都直接调用InternalUniqueString()函数。如果是Windows版本,则两个宽字符串的惟一化例程是无意义的:因为Windows上的宽字符串没有引用计数位,这在前面已经一再讲过。
function InternalUniqueString()的实现过程,实际上是调用NewAnsiString()分配新字符串并返回值。然后,将原字符串的引用减1,如果减1后为0,则释放字符串。根据system.pas中的 InternalUniqueString()改写的PUREPASCAL版本如下:
function InternalUniqueString(var str):Pointer; function _Ref:Integer; begin Result:=PInteger(Integer(str)-8)^ end; function _Len:Integer; begin Result:=PInteger(Integer(str)-4)^ end; var V:String; begin if pChar(str)"=then Exit; if _Ref=1 then Exit; //模拟_NewAnsiString()例程 if _Len>0 then begin SetLength(V,_Len); move(Pointer(str)",V[1],_Len); end; string(str):=V;/使得入口时的str引用计数减1,且变量V的引用计数加1 Result:=Pointer(Str); end;//过程结束时,局部变量v的引用计数会减1,使得返回时的str的引用计数为1
function_UniqueStringAC)是内部例程中几个不多的提供了用户调用接口的函数。
用户可以调用procedure UniqueString(),来完成相同的功能。
■集合操作例程
procedure _SetElem; procedure _SetRange; procedure _SetEq; procedure _SetLe; procedure _SetIntersect; procedure _SetIntersect3;{BEG only} procedure _SetUnion; procedure SetUnion3;{BEG only} procedure _SetSub; procedure _SetSub3;{BEG only} procedure _SetExpand;
集合的内部例程用于实现集合运算,参见下例:
var aset,bset,cSet:TDemoSet; begin aset:=aset+bset;//call procedure _SetUnion() aSet:=aSet *bset;//call procedure _SetIntersect() if(aset =bset)and //call procedure _SetEg() (aSet<>cSet)and //call procedure _SetEq() (cSet<= aset)and // call procedure _SetLe() (bSet>=aset)then // call procedure Sette() cSet:=aSet-bSet;//call procedure _SetSub() end:
并非每个对应的集合操作都会调用这些内部例程。通常情况下,Delphi试图以字节、字和双字类型的位运算来实现集合运算。并且集合运算总是优先享有使用寄存器的权利。但是,如果一个集合类型超过了32个元素,则编译器使用上述的内部例程来替代位运算。这意味着超过32个元素以后,集合运算的效率将大打折扣。
■数组(含动态数组)操作例程
//表达式、运算和运算符 procedure DynArrayHigh; procedure _DynArrayclear (var a:Pointer;typeInfo:Pointer); procedure _DynArrayLength; procedure _DynArraySetLength; procedure _DynArrayCopy(a:Pointer;typeInfo:Pointer;var Result:Pointer); procedure _DynArrayCopyRange(a:Pointer;typeInfo:Pointer;index,count:Integer;var Result:Pointer); procedure _DynArrayAsg; procedure _DynArrayAddRef; //可外部调用的动态数组操作例程 procedure DynArrayClear(var a:Pointer;typeInfo:Pointer); procedure DynArraySetLength(var a:Pointer;typeInfo:Pointer;dimCnt:Longint;lengthVec:PLongint); function DynArrayDim(typeInfo:PDynArrayTypeInfo):Integer; //可用于静态数组的例程 procedure _AddRefArray; procedure CopyArray;
Delphi的动态数组采用跟字符串类同的内存结构。亦即是说,动态数组也具有引用计数位和长度计数位,也类似地采用了引用计数的机制来提高系统效率。下面的例子详细演示了内部例程的调用情况:
procedure DynArrayTest; type TDynArr=array of char; var a,al:TDynArr; procedure TestDynArrayRef(constt:TDynArr); var c:array of char; begin //call procedure _ynArrayAddRef() TDynArr(c):=copy(t);// call procedure _DynArrayCopy() end;//call procedure _DynArrayClear() begin if Length(a)>0 then //call procedure _DynArrayLength() al:=a//call procedure _DynArrayAsg() else begin SetLength(a,100);//call procedure _DynArraySetLength() a1:=copy(a,1,3);//call procedure _DynArrayCopyRange() end; a:=nil;//call procedure _DynArrayClear() TestDynArrayRef(al); end;
动态数组的引用计数并非基于元素的,而是基于变量的,这与长字符串的引用计数不一样:在长字符串中,访问串中的一个字符,会导致引用计数发生变化;而在动态数组中,读写一个元素,并不会使引用关系发生变化一—这与大多数人的设想并不一致。下例说明这一点:
type DynArr=array of char; procedure Testwritecopy; var al,a2:TDynArr; s1,s2:String; begin SetLength(a1,3); fillchar(al[0],3,ORD('a)); s1:='aaa'; a2:=al; al[1]:='b'; writeln(pChar(al));//'aba' writeln(pChar(a2));//'aba' 82:=s1; 81[2]:='b'; writeln(s1);//'aba' writeln(s2);//'aaa' end;
如果要避免修改数组元素而影响到被引用的动态数组,可以使用function Copy()来创建一个惟一的拷贝。这与在长字符串中调用procedure UniqueString()的效果一致。
编译器会使function Copy()直接调用内存例程_DynArraycopy()。
procedure _DynArrayCopy(a:Pointer;typeInfo:Pointer;var Result:Pointer); begin if a<>nil then _DynArrayCopyRange(a,typeInfo,0,PLongint(PChar(a)-4)",Result) else _DynArrayClear(Result,typernfo); end;
procedure_DynArrayCopy()做一个简单的检查:如果试图把nil赋给数组,则将结果数组Result清空。否则,将从动态数组负偏移上取出数组长度,并调用
_DynArrayCopyRange()。这使得下面两行代码的效果是一致的:
a1:=copy(a2);//call procedure DynArraycopy() al:=copy(a2,0,Length(a2));// call procedure _DynArrayCopyRange()
但是真正的复制代码仍然不在例程_DynArrayCopyRange()里。因为procedure _DynArrayCopyRange()的实现代码是这样的:
procedure _DynArrayCopyRange(a:Pointer;typeInfo:Pointer;index,count:Integer;var Result:Pointer); begin //1.修正数组边界,使参数index和count有效 //2.从类型信息中取每个元素的长度到elsize局部变量 //3.在堆上分配数组空间count*elsize+Sizeof(Longint)*2个字节,并 //置引用计数位为1,长度计数位为count if typeInf <>nil then begin FillChar(p^,count*elSize,0); CopyArray(p,a,typeInf,count) end else Move (a^,p^,count*elSize); end;
例程_DynArrayCopyRange()构造了一个新的数组。如果原数组有类型信息,则调用CopyArray()复制数组,否则,直接使用move()操作复制数组的内容,CopyArray()调用内部例程_CopyArray()。
所有动态和静态数组,如果具有类型信息,那么它的复制操作最终都将调用到_CopyArray()。
数组的类型信息保存有一个引用列表,CopyArray()根据这个列表创建对旧数组的相应元素的引用。关于这个操作的细节,请参见“引用一计数一写复制与类型信息”小节。
Delphi中,Copy函数有三种声明:
function copy(s;Index,Count:Integer):string; function Copy(S;Index,Count:Integer):array; function Copy(S:array):array;
针对不同的数据类型,编译器会使Copy()函数调用该类型的内部例程。如下表:
类型 | 调用函数 |
短字符串 | procedure _Copy{s:ShortString; |
长字符串 | procedure _LStrCopy (const s:Ansistring; |
宽字符串 | function _wStrCopy(const S:WideString; |
动态数组 | procedure _DynArrayCopyRange(a:Pointer;typeInfo:Pointer; |
同样地,Delphi对如下字符串例程也使用相同的技巧:
procedure SetLength(var S;NewLength:Integer); procedure Delete(var S:string;Index,Count:Integer); procedure Delete(var S:string;Index,Count:Integer); procedure Insert(Source:string;var S:string;Index:Integer); function Pos(Substr:string;s:string):Integer;
这些都可以在各自的字符串内部例程中看到对应的版本。SetLength()可以同时作用于字符串和动态数组。
■记录操作例程
procedure AddRefRecord; procedure _CopyRecord;
Delphi的赋值运算符可以用于记录操作,编译器使该运算符指向内部例程CopyRecord()。因此,这样的语句是可以被编译的:
type TRec=record I:Integer; S:String; end; var R1,R2:TRec; begin R1:=R2;//记录赋值,call procedure _CopyRecord()
同_CopyArray()一样,例程_CopyRecord()也是通过类型信息来实现的。实现的效果也类同。
有关类型信息在记录和数组中更多的应用,以及AddRefRecord()例程的具体实现,在
“引用一计数一写复制与类型信息”小节中会有更进一步的描述。
■其他
//实数类型转换 procedure _Rea12Ext; procedure _Ext2Real; //添加引用 procedure _AddRef; //浮点数除运算 procedure _FSafepivide; procedure _FSafeDivideR; //Conversion utility routines for Ct+ convenience.Not for Delphi code. function CompToDouble(Value:Comp):Double;cdecl; procedure DoubleTocomp(Value:Double;var Result:Comp);cdecl; function CompToCurrency(Value:Comp):Currency;cdec1; procedure CurrencyTocomp (Value:Currency;var Result:Comp);cdec1; {wide character support procedures and functions for C++} function WidecharToString(Source:PWidechar):string; function wideCharLenrostring(Source:PWidechar;SourceLen:Integer):string; procedure widecharrostrVar(Source:PWideChar;var Dest:string); procedure WidecharLenrostrVar(Source:PWidechar;SourceLen:Integer;var Dest:string); function stringrowidechar(const Source;string;Dest:Pwidechar;DestSize:Integer):Pwidechar;
上面例程中,AddRef()用于对任意一个变量添加引用。看起来,它试图将变量看作一个只有一个元素的数组处理,而并不关心该元素是什么类型或者有没有类型信息。因为这个例程总是直接调用AddRefArray()。然而事实上,这段仅仅两行代码的过程:
procedure AddRef{p:Pointer;typeInfo:Pointer);} asm MOV ECX,1 JMP AddRefArray end;
充分利用了数组、元素和类型信息的特性,利用AddRefArray()代码轻松地实现了对所有7种支持引用计数机制的数据类型的处理,并完成对其他数据类型的甄别。
有关浮点数运算的两个例程_FSafeDivide()和_FSafeDivideR(),可以参阅参考书目的《Delphi技术手册》中,有关SSafeDivide编译指令和TestFDTV变量的描述。
Real2Ext()和_Ext2Real()两个例程,用于编译器在Rea1和Extened两种浮点数类型间相互赋值时隐含地进行类型转换。