关于 Delphi 参数传递方式的一点研究

某次看 D6DG 说默认的参数传递方式因为会为变量产生本地副本所以会消耗额外的内存,而 const 方式会优化字符串和记录类型的参数传递时的内存占用。从而猜测 const 方式的参数传递实际也是按地址传递,只是编译器强制不允许函数内的代码修改 const 方式传递进去的变量而已。
为了证实我的猜想,特设计以下实验,本实验中使用到了字符串(本文提到的字符串都是指 Delphi 默认的字符串 AnsiString)内部结构中的引用计数。
关于字符串的引用计数,结合 D6DG 中的说明和偶的实际研究,得出的结论是,字符串地址实际上是其内容的第一个字符的地址,在此地址之前的 12 字节内存中的内容才是字符串内部结构中的头部,分别是 32 位字符串占用的内容空间大小,32 位引用计数,32 位字符串长度(仅在 Delphi 7 中验证,推测 Delphi 7 以前的 32 位 Delphi 中的字符串结构都应该是这样,未验证)。


以下代码中 sGlobal 为全局字符串变量。

procedure Method1(S: string);

var
P: PInteger;
begin
// S := S + 'k';
// 字符串实际上是指针,这里只是将其强制转换为 PInteger 备用
P := PInteger(S);
// 得到字符串地址左偏移 8 字节的地址,也就是字符串的 32 位引用计数存储的位置
P := PInteger(Integer(P) - 8);
Form1.Memo1.Lines.Add('S 引用计数: ' + IntToStr(P^));
P := PInteger(sGlobal);
P := PInteger(Integer(P) - 8);
Form1.Memo1.Lines.Add('sGlobal 引用计数: ' + IntToStr(P^));
Form1.Memo1.Lines.Add('S 地址: ' + IntToStr(Integer(Pointer(S))));
Form1.Memo1.Lines.Add('');
end;

procedure Method2(var S: string);
var
P: PInteger;
begin
P := PInteger(S);
P := PInteger(Integer(P) - 8);
Form1.Memo1.Lines.Add('S 引用计数: ' + IntToStr(P^));
P := PInteger(sGlobal);
P := PInteger(Integer(P) - 8);
Form1.Memo1.Lines.Add('sGlobal 引用计数: ' + IntToStr(P^));
Form1.Memo1.Lines.Add('S 地址: ' + IntToStr(Integer(Pointer(S))));
Form1.Memo1.Lines.Add('');
end;

procedure Method3(const S: string);
var
P: PInteger;
begin
P := PInteger(S);
P := PInteger(Integer(P) - 8);
Form1.Memo1.Lines.Add('S 引用计数: ' + IntToStr(P^));
P := PInteger(sGlobal);
P := PInteger(Integer(P) - 8);
Form1.Memo1.Lines.Add('sGlobal 引用计数: ' + IntToStr(P^));
Form1.Memo1.Lines.Add('S 地址: ' + IntToStr(Integer(Pointer(S))));
Form1.Memo1.Lines.Add('');
end;

procedure sGlobalInfo;
var
P: PInteger;
begin
P := PInteger(sGlobal);
P := PInteger(Integer(P) - 8);
Form1.Memo1.Lines.Add('sGlobal 引用计数: ' + IntToStr(P^));
Form1.Memo1.Lines.Add('sGlobal 地址: ' + IntToStr(Integer(sGlobal)));
Form1.Memo1.Lines.Add('');
end;




在主程序里分别以初始化后的 sGlobal 为实参调用这几个过程后,我们会看到三种方式 S 的地址都跟 sGlobal 的一样,这初步说明 var 方式和 const 方式确实都是按地址传递参数。
但不可思议的是,默认参数传递方式不会是为变量产生本地副本吗?为什么 S 的地址还会跟 sGlobal 一样呢?因为一开始我设计这个实验时并没有添加显示引用计数的代码,所以很是迷茫。之后想起了 Borland 使用了引用计数的方式和 copy-on-write 技术(对于字符串等使用这两个技术的数据类型的变量 A、B,将 A 赋值给 B 时实际上只是赋值 A 的地址给 B,并增加 A 的引用计数,直到两者其中一个被修改时才为 B 申请新的内存空间并且复制 A 的内容,同时减少 A 的引用计数)来优化字符串的操作。于是查阅 D6DG 并且观察 Delphi 的汇编代码和内存(在 CPU 窗口中)得出了本文开头关于字符串引用计数的结论。
这样,添加了显示引用计数的代码之后我们就可以观察到,使用默认方式时 S 和 sGlobal 的引用计数,都是 2,而其他方式时两者的引用计数都是 1,这更有力的说明了 const 方式确实是按地址传递,而不是像默认方式那样只是增加引用计数。这样,当我们把第一个过程中第一行代码的注释去掉后再次运行程序,可以看到默认方式时 S 的地址已经跟 sGlobal 不同了,同时两者的引用计数都是 1 了,说明确实是在 S 被修改后才产生本地副本(copy-on-write)。

那么,现在我们知道参数类型为字符串时, const 方式的参数传递实际是传递参数地址,这可以优化内存的使用,而参数为其他数据类型时是不是这样呢?
为此修改刚才的程序如下,其中 iGlobal 为全局整形变量。


procedure Method1(I: Integer);
begin
Form1.Memo1.Lines.Add('I 地址: ' + IntToStr(Integer(@I)));
Form1.Memo1.Lines.Add('');
end;

procedure Method2(var I: Integer);
begin
Form1.Memo1.Lines.Add('I 地址: ' + IntToStr(Integer(@I)));
Form1.Memo1.Lines.Add('');
end;

procedure Method3(const I: Integer);
begin
Form1.Memo1.Lines.Add('I 地址: ' + IntToStr(Integer(@I)));
Form1.Memo1.Lines.Add('');
end;

procedure iGlobalInfo;
begin
Form1.Memo1.Lines.Add('iGlobal 地址: ' + IntToStr(Integer(@iGlobal)));
Form1.Memo1.Lines.Add('');
end;

在主程序里分别以初始化后的 iGlobal 为实参调用这个过程后,可以看到只有 var 方式中 I 的地址跟 iGlobal 一样,而默认方式和 const 都会为 iGlobal 产生本地副本,可见确实如 D6DG 所说 const 会(也只会)优化字符串和记录类型的参数传递时的内存占用。

至此实验目的达到,还附带了解了 string 的内部格式。

附上实验程序源代码。
点击下载
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值