2.6引用-计数-写复制与类型信息

本文详细介绍了Delphi编程语言中的引用计数机制,包括增加引用的触发条件、依赖类型信息实现的方式、写复制技术及其与值参数备份的区别等核心内容。通过对Delphi内部处理机制的深入解析,帮助读者理解长字符串、动态数组等数据类型在操作时的底层行为。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

2.6.1引用计数与增加引用

  Delphi内部对“引用(Reference)”的处理稍显混乱。赋值运算符“=”、函数的值参数入口和一些内部例程都可能导致引用。但是相同的规则并不能应用于所有支持计数的数据类型,并且,对于一些看似类同的数据类型,规则的作用范围也不尽相同。
  从内部来看,Delphi已经尽可能地将不同类型的“增加引用”和“减少引用”例程分离开来。例如长字符串的“增加引用”调用_LStrAddRef(),动态数组则调用DynArrayAddRef()。但是一些例程名字却着实让人误解,例如AddRefArray(O并非增加数组的引用,而是成组地增加一批变量的引用。

  支持“引用计数”的只有少数的几个数据类型,如长字符串、动态数组、接口和变体类型。但是由于一些复杂的数据类型中的子域会声明成上述这些类型,因此,操作复杂数据类型的时候,也必须逐一考查其子域,对支持“引用计数”的数据进行计数修正。因此,虽然这些复杂数据类型也有“增加引用”的行为,但发生计数修改的是它们的子域,而非其自身。表2-3、表2-4说明了这些类型的不同。

 

2.6.2“增加引用”何时发生

  Delphi中,例程中的代码可以对例程入口的值参数进行写操作。为了应付这种情况,Delphi须在进入例程之前,为一个值参数创建一个备份。例程中对值参数的操作都实际发生在这个备份上,而不会对原来的值参数造成影响。
  理论上讲,这样的备份应该是对值参数的一个完全复制。但是,Delphi实际上只是对简单类型进行这样的处理,例如将整型值参数的值复制到堆栈,或者将集合放到寄存器。对于复杂的数据类型(例如数组、记录),为了节约内存开销和提高系统性能,编译器对数据类型的每一个部分进行考查,并确认哪些采用“值复制”,那些采用“引用计数”,并将采用“引用计数”的域的信息附加在类型信息中的一张表里。在运行期根据这张表进行的“引用计数”操作,就是“增加引用(AddRef)”的过程。
  因此,通常会看到记录和数组“增加引用”的过程发生在例程的入口处。下面的例子会在例程的begin中调用_AddRefRecord();

//Sizeof(TRec)=8,例程将在栈上开8bytes,用于生成一个入口值参数备份
type
TRec =record
I:Integer;
s:String;
end;
procedure TestRecordAddRef(R:TRec);
begin
    R.S:= '';
end;
//.…以下反汇编TestRecordAddRef()的begin处理
Unitl.pas.31procedure TestRecordAddRef(R:TRec);
Unit1.pas.32begin
0044C9BC 55              push ebp
0044C9BD 8BEC            mov ebp,esp
//在栈上开8字节的空间,设为_Buf,地址为[ebp-$08]
0044C9BF 83C4F8         add esp,-$08
//以下6条指令,用于完成入口参数R到_Buf的复制
//现在相当于完成了调用move(R,Buf,sizeof(TRec))
0044C9C256              push esi
0044C9C357              push edi
0044C9C4 8BF0           mov esi,eax
0044C9C6 8D7DF8         1ea edi,[ebp-$08]
0044C9C9 A5             movsd
0044C9CA A5             movsd
//调用_AddRefRecord(),为记录的域增加引用
0044C9CB 8D45F8         1ea eax,[ebp-$08]//_Buf地址到eax,开始操作备份
0044C9CE 8B15A0C94400   mov edx,[$0044c9a0]
0044C9D4 E8CB7FFBFF     call BAddRefRecord
//以下开始在栈上填入异常帧(参见相关章节)
0044C9D9 33C0           xor eax,eax
0044C9DB55              push ebp
0044C9DC 680ACA4400     push $0044ca0a
0044C9E1 64FF30         push dword ptr fs:[eax]
0044C9E4648920          mov fs:[eaxl,esp
Unit1.pas.33://..…

  此后,编译器会将每个对入口参数R的操作,定位到[ebp-$08]上。例如代码:

 R.S:= '';

  会被编译为:

Unit1. pas.33:R.S := '';
0044C9EE 8D45FC 1ea eax,[ ebp-$04]//ebp-$08+Sizeof(P.I)
0044C9F1 E81A74FBFF ca11 QLStrclr

 

2.6.3增加引用的操作是依赖类型信息来实现的

  只有使用类型信息,才能在运行期分析数据类型。因此在System,pas中,数组和记录操作是为数不多的几个使用运行期类型信息(RTTI)的地方。
  可以通过function TypeInfo()来取得一个指向类型信息的指针,这包括全部的基本数据类型和一些构造类型(如果它包含一些“具有引用计数特性的数据类型”的元素、字段或域,则该构造类型也具有类型信息)。和Pi()一样,function TypeInfo()也不是一个真的函数。在编译期,Delphi能确定每一个类型的类型信息地址,因此一旦编译器遇到function TypeInfo(),总是直接替换成相应的类型地址。如下例:

Unitl.pas.29:ptr:=TypeInfo(TForm);
//TFrom的类型信息在地址$00441054,直接送入寄存器eax
0044C878A154104400 mov eax,[$00441054]
//eax送入栈上的ptr
0044C87D 8945F8 mov [ebp-$08],eax

  在AddRefRecord()例程中就隐含了一个typeinfo参数,该例程实际的声明是:

Drocedure AddRefRecord (p:Pointer;typeInfo:Pointer);

  该例程的具体实现代码可查阅system.,pas,下面是根据源码改写的PUREPASCAL版本的AddRefRecord():

//define in system.pas
type
TFieldInfo =packed record
TypeInfo:PPTypeInfo;
Offset:Cardinal;
end;

procedure _AddRefArray (p:Pointer;typeInfo:Pointer;
elemcount:Longint);
begin
// code in system.pas
end;

procedure _AddRefRecord(const R;typeInfo:Pointer);
var
i,maxI:Integer;
D:Pointer;
begin
    inc(PByte(typeInfo));//skip kind
    inc(PByte(typeInfo),Byte(typeInfo~)+1);// skip name(ShortString)
    inc(PByte(typeInfo),4);
    maxI:=PInteger(typeInfo);//取引用表(数组)的长度
    inc(PByte(typeInfo),4);//定位到引用表的头部
    for i:=0 to maxI-1 do
    begin
        p:=Pointer(Integer(eR)+TFieldInfo(typeInfo").offset);
        AddRefArray(p,TPieldInfo(typeInfo~).TypeInfo,1);
    end;
end;

  AddRefRecord()总是在记录的typeInfo中找出引用列表(数组)的长度和表首地址。引用列表(数组)只保存需要定位的类型变量的信息,它的元素类型为TFieldInfo。TFieldInfo.Offset 表示需要定位的元素自变量首地址开始的偏移字节数。因此,对于如下的记录:

type
TRec=record
I:Integer;
S:String;
end;

来说,引用列表(数组)只有一个元素,其FieldInfo.TypeInfo指向TRec.S的类型信息(即String的类型信息),而FieldInfo.Offset值为4,表明TRec.S的地址相对于记录首部的偏移为4Byte。

  整型是简单数据类型,所以TRec.I直接复制值;而String类型使用引用计数机制,所以TRec.S被加入引用列表(数组)——这是它们的不同。

 

2.6.4写复制与值参数的备份

  写复制技术被用在很多的地方,例如DLL的映像,但这是在运行期由操作系统决定的行为。在Delphi中,写复制是在编译期就被决定的,它通过内核例程和编译器的语义分析两个方面来实现。
只有使用引用计数的数据类型会发生写复制。具体的说,是指长字符串、宽字符串、动态数组、接口和变体这五种数据类型。写复制机制内嵌于类型的基本操作例程中,且只有在数据发生修改时,写复制才会发生。
  以长字符串为例。编译器在语义分析时,会检查每一段使用下标访问字符串的语句,并在最前面插入“ca11@UniqueStringA”。这个例程用来检查字符串的引用计数,如果发现字符串被引用(引用计数大于1),就复制一个新的字符串并返回给原变量。这样,操作这个字符串就不会再影响到引用它的其他字符串。
  但是,字符串的内部例程并不调用_UniqueStringA()。例如_LStrCat(),该例程会首先调用_LStrSetLength()来为结果字符串分配内存,而_LStrSetLength()则先检测引用计数位,如果大于1,则调用_NewAnsiString()来创建新的字符串。源代码如下:

procedure LStrSetLength(var str: Ansistring; newLength: Integer);
asm
    CMP [ EAX-skew]. StrRec. refcnt,1
    JNE @@copyString
    //...
@@ecopyString:
    MOV EAX, EDX
    CALL_NewAnsistring
    //..
end;

  值参数的备份源自于例程的调用约定:值参数可以被改写,但是不会影响到传值的原始变量。因此,编译器需要为值参数制作一个备份,而无论代码中是否要写该值参数。这个复制操作在编译时就被决定了,例程被调用一次,就发生一次复制操作。这与写复制是不同的。前面已经讲述过,在值参数备份中,针对不同的数据类型,生成备份的方法不同,具体如下:

  • 如果是简单数据类型,则直接复制参数的值。
  • 如果是具有引用计数位的数据类型,则调用相应的函数产生一个引用。
  • 如果是数组或记录,则将元素或域按照上述规则分别处理。

 

■其他

  数组和记录的引用列表(数组)其实也是它们的初始化表。在第4章的“初始化与结束化过程”一节中,会再一次讲到它们。
  如果确知在代码中不需要改写入口的值参数,那么可以用const声明它。这样做的结果是编译器将向例程直接传入参数的地址,而不再是使用参数值的引用。正是因此,如果使用指针访问该地址,就可以对原值进行直接修改。下面的例子揭示如何修改用const声明的值参数:

($O-}
(SAPPTYPE CONSOLE}
type
PRec=~TRec;
TRec=record
I: Integer;
s: String;
end;

procedure ChangeConstParam(const R: TRec);
begin
    PRec(QR)".I:=6;
end;

var
    Rec: TRec;
begin
    Rec.I:=5;
    writeln(Rec.I);
    ChangeConstParam(Rec);
    writeln(Rec.I);
end.

 

转载于:https://www.cnblogs.com/YiShen/p/9878947.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值