Delphi 中 函数参数中的 const 修饰符的本质以及注意事项
来自:http://blog.csdn.net/farrellcn/article/details/9096787
------------------------------------------------------------------------------
很多书籍中说函数参数如果是String类型的,如果在函数内部不改变参数的值,使用 const 修饰符会加快程序的执行速度,至于如何加快的?有的人说是因为 const 函数保证了参数字符串不会被复制。以前也没有对这个问题深入研究,但是在不修改函数参数的时候,总是习惯加上 const 修饰符,前几天在csdn论坛上解答某个人的问题是,发现程序产生的结果和预期的不一样,检查了一遍代码,发现没有什么问题,按理不应该出现错误。于是跟踪调试,发现是用 const 修饰的一个 String 类型的函数参数在该函数中被意外地更改了其内容,这很奇怪啊,因为在函数内部没有修改其值的地方,况且用 const 修饰的参数,如果存在修改其内容的语句,编译都过不去,更何谈执行了。于是,就跟踪代码,查找原因,这一跟踪,发现了一个问题:
跟踪测试代码如下:
var
SS: String; procedure test1(const s: String);
begin
try
SS := '你好';
ShowMessage(s);
except
end;
end; procedure TfrmMain.btnTestClick(Sender: TObject);
begin
SS := 'Hello';
test1(SS);
end;
var
SS: String; procedure test1(const s: String);
begin
try
SS := '你好';
ShowMessage(s);
except
end;
end; procedure TfrmMain.btnTestClick(Sender: TObject);
begin
SS := 'Hello';
test1(SS);
end;
代码很简单,按照程序逻辑,ShowMessage(s) 的结果应该显示 "Hello",有些人看了前面的描述,可能会觉得,好像应显示 "你好",这些都不对,显示的是乱码!呵呵,吓一跳吧!好了,下面我就来解释下,为什么会显示乱码:
首先,看看加了 const 修饰符到底会有什么不同:
procedure test1(const s: String);
begin
try
SS := '你好';
ShowMessage(s);
except
end;
end;
procedure test1(const s: String);
begin
try
SS := '你好';
ShowMessage(s);
except
end;
end;
在Test1函数的begin处断开,然后看看汇编代码到底干了什么:
上图是在 begin 处加了断点后,程序运行到这里的汇编代码,其中,红色方框中的,就是在 begin 到第一句语句 try 处执行的代码。
再看看不加 const 会怎么样:
procedure test1(s: String);
begin
try
SS := '你好';
ShowMessage(s);
except
end;
end;
procedure test1(s: String);
begin
try
SS := '你好';
ShowMessage(s);
except
end;
end;
在 Test1 函数的 begin 处断开,然后看看汇编代码到底干了什么:
上图就是不加 const 修饰符,程序运行到 begin 处的汇编代码,红色方框中,就是 begin 到第一句语句 try 处执行的代码。
对比上面的两处代码,发现不加 const 修饰符,多了一段代码
mov [ebp-$],eax
mov eax,[ebp-$]
call @LStrAddRef xor eax,eax
push ebp
push $0045214b
push dword ptr fs:[eax]
mov fs:[eax],esp
mov [ebp-$],eax
mov eax,[ebp-$]
call @LStrAddRef xor eax,eax
push ebp
push $0045214b
push dword ptr fs:[eax]
mov fs:[eax],esp
下面一句一句来解释
mov [ebp-$04],eax
将 eax 寄存器中的内容复制到 ebp-$04 所指向的内存中。
因为eax是函数的第一个参数,也就是字符串参数 s 的内容。在 delphi 中字符串是一个指针,指向在堆中开辟的字符串内容的地址,因此这句的意思就是将字符串参数指针的内容放入 ebp-$04 所指向的位置,因为在程序一开始有
push ebp
mov ebp,esp
esp是栈指针,总是指向栈顶,因此,ebp所指向的内容就是这个函数用到的栈的栈顶的位置。紧接着:
push ecx
push ebx
push esi
push edi
四个压栈指令,导致在栈中预留出了4个位置。ebp-$04 就是栈中第一个预留位置。因此,
mov [ebp-$04],eax
就是将函数参数的第一个参数放入到栈中。
mov eax,[ebp-$04]
call @LStrAddRef
这两句完成了一个功能,就是调用@LStrAddRef函数,在system单元翻看@LStrAddRef函数完成的功能
function _LStrAddRef(var str): Pointer;
{$IFDEF PUREPASCAL}
var
P: PStrRec;
begin
P := Pointer(Integer(str) - sizeof(StrRec));
if P <> nil then
if P.refcnt >= then
InterlockedIncrement(P.refcnt);
Result := Pointer(str);
end;
{$ELSE}
function _LStrAddRef(var str): Pointer;
{$IFDEF PUREPASCAL}
var
P: PStrRec;
begin
P := Pointer(Integer(str) - sizeof(StrRec));
if P <> nil then
if P.refcnt >= then
InterlockedIncrement(P.refcnt);
Result := Pointer(str);
end;
{$ELSE}
其中用到了PStrRec,其结构如下
type
PStrRec = ^StrRec;
StrRec = packed record
refCnt: Longint;
length: Longint;
end;
简单解释下:
先解释下 StrRec 结构,在delphi中,String类型的数据,实际上就是一个 StrRec 结构的数据再加上字符串本身的内容,其中 refCnt 表示字符串被引用的次数,length 是字符串的长度。refCnt 的作用是:当其值为0的时候,表示这个字符串没有任何一个地方使用了,这时,delphi 会在适当的时候释放这个字符串所占用的内存。
再说下 LStrAddRef 函数完成的功能,首先得到指向传入字符串所对应的StrRec结构体的指针,然后判断这个结构体是否为空,如果不为空,再判断这个结构体中的 refCnt 是否大于等于0,如果是,就将其加一,然后返回指向这个字符串本身的指针。
好了
mov eax,[ebp-$04]
call @LStrAddRef
所完成的功能就是让字符串 ebp-$04 中保存的字符串的引用计数加一,而 ebp-$04 中保存的,恰好是函数第一个参数传入的内容,也就是说,这两句完成的是让传入的字符串参数的引用计数加一
xor eax,eax
push ebp
push $0045214b
push dword ptr fs:[eax]
mov fs:[eax],esp
这几句是与delphi实现的异常处理有关,在这里不做说明了,和本文讨论的内容关系不大。
问题描述到这里,我们可以看到,使用 const 修饰和不使用 const 修饰的差别:不使用 const 会让函数参数多了一个增加引用计数的过程。好了,记住这个结论。实际上,问题就出在这里
首先分析下程序的流程:
procedure TfrmMain.btnTestClick(Sender: TObject);
begin
SS := 'Hello';
test1(SS);
end;
procedure TfrmMain.btnTestClick(Sender: TObject);
begin
SS := 'Hello';
test1(SS);
end;
为全局的字符串变量 SS 赋值为"Hello",然后以 SS 为参数调用Test1函数。
procedure test1(const s: String);
begin
try
SS := '你好';
ShowMessage(s);
except
end;
end;
procedure test1(const s: String);
begin
try
SS := '你好';
ShowMessage(s);
except
end;
end;
在 Test1 函数中,修改 SS 的内容,并显示参数S的内容
整个流程并不复杂,下面,在汇编下,看看是怎么完成的:
在 SS := "Hello" 处下断点,跟踪。
为 SS 赋值的语句为上面红框框出来的部分,调用了@LStrAsg函数完成了赋值功能,下面是@LStrAsg函数
//两个参数,第一个参数为目标字符串,也就是要赋值的字符串
//第二个参数是原始字符串,也就是要赋值的原始值
procedure _LStrAsg(var dest; const source);
{$IFDEF PUREPASCAL}
var
S, D: Pointer;
P: PStrRec;
Temp: Longint;
begin
S := Pointer(source); //得到指向原始字符串的地址
if S <> nil then
begin
P := PStrRec(Integer(S) - sizeof(StrRec)); //得到指向原始字符串的 StrRec 结构指针 //当原始字符串为常量时,其引用计数为-1
//这种时候,会引起字符串的复制操作
if P.refCnt < then // make copy of string literal
begin
Temp := P.length; //获得原始字符串的长度
S := _NewAnsiString(Temp); //开辟一块大小为原始字符串长度的内存
Move(Pointer(source)^, S^, Temp); //将原始字符串的内容复制到新开辟的内存中
P := PStrRec(Integer(S) - sizeof(StrRec)); //将 P 设置为指向新字符串的 StrRec 结构指针
end;
InterlockedIncrement(P.refCnt); //增加 P 所指向的字符串的引用计数
end; //设置目标字符串
D := Pointer(dest); //保存指向目标字符串原来的指针到 D
Pointer(dest) := S; //设置目标字符串为 S 指向的字符串 //如果原来的目标不为空,则将其引用计数减一
//减一之后,如果引用计数为零,则释放这个字符串占的内存
if D <> nil then
begin
P := PStrRec(Integer(D) - sizeof(StrRec));
if P.refCnt > then
if InterlockedDecrement(P.refCnt) = then
FreeMem(P);
end;
end;
{$ELSE}
//两个参数,第一个参数为目标字符串,也就是要赋值的字符串
//第二个参数是原始字符串,也就是要赋值的原始值
procedure _LStrAsg(var dest; const source);
{$IFDEF PUREPASCAL}
var
S, D: Pointer;
P: PStrRec;
Temp: Longint;
begin
S := Pointer(source); //得到指向原始字符串的地址
if S <> nil then
begin
P := PStrRec(Integer(S) - sizeof(StrRec)); //得到指向原始字符串的 StrRec 结构指针 //当原始字符串为常量时,其引用计数为-1
//这种时候,会引起字符串的复制操作
if P.refCnt < then // make copy of string literal
begin
Temp := P.length; //获得原始字符串的长度
S := _NewAnsiString(Temp); //开辟一块大小为原始字符串长度的内存
Move(Pointer(source)^, S^, Temp); //将原始字符串的内容复制到新开辟的内存中
P := PStrRec(Integer(S) - sizeof(StrRec)); //将 P 设置为指向新字符串的 StrRec 结构指针
end;
InterlockedIncrement(P.refCnt); //增加 P 所指向的字符串的引用计数
end; //设置目标字符串
D := Pointer(dest); //保存指向目标字符串原来的指针到 D
Pointer(dest) := S; //设置目标字符串为 S 指向的字符串 //如果原来的目标不为空,则将其引用计数减一
//减一之后,如果引用计数为零,则释放这个字符串占的内存
if D <> nil then
begin
P := PStrRec(Integer(D) - sizeof(StrRec));
if P.refCnt > then
if InterlockedDecrement(P.refCnt) = then
FreeMem(P);
end;
end;
{$ELSE}
从这个函数中可以看到,为 SS 赋值为 'Hello' 的过程是:因为 'Hello' 为常量,其引用计数为-1,因此,先开辟了一块内存,然后将 'Hello' 这个字符串的内容复制到新内存中,因为 SS 在赋值前是空(上图中,下面红框框出来的地方就是SS的原始值,可以看到,其内容为0),因此没有执行减少引用计数的部分代码。
顺便看看为新字符串开辟内存空间的_NewAnsiString函数吧
//传入字符串的长度,返回新分配的内存
function _NewAnsiString(length: Longint): Pointer;
{$IFDEF PUREPASCAL}
var
P: PStrRec;
begin
Result := nil;
if length <= then Exit; //开辟一块内存,其大小是 Length 加 StrRec 结构的长度加1
//之所以要加1,是为了在字符串的最后加入一个#0,以便和PChar类型兼容
//后面的((length + 1) and 1) 是为了地址对齐而加入的,暂时不用理会
GetMem(P, length + sizeof(StrRec) + + ((length + ) and )); //返回新开辟的内存,注意,这里不是返回其首地址,
//而是跳过了 StrRec 结构
Result := Pointer(Integer(P) + sizeof(StrRec));
P.length := length; //设置新字符串长度为传入的长度
P.refcnt := ; //设置引用计数为1 //为字符串的最后面加入#0
PWideChar(Result)[length div ] := #; // length guaranteed >= 2
end;
{$ELSE}
//传入字符串的长度,返回新分配的内存
function _NewAnsiString(length: Longint): Pointer;
{$IFDEF PUREPASCAL}
var
P: PStrRec;
begin
Result := nil;
if length <= then Exit; //开辟一块内存,其大小是 Length 加 StrRec 结构的长度加1
//之所以要加1,是为了在字符串的最后加入一个#0,以便和PChar类型兼容
//后面的((length + 1) and 1) 是为了地址对齐而加入的,暂时不用理会
GetMem(P, length + sizeof(StrRec) + + ((length + ) and )); //返回新开辟的内存,注意,这里不是返回其首地址,
//而是跳过了 StrRec 结构
Result := Pointer(Integer(P) + sizeof(StrRec));
P.length := length; //设置新字符串长度为传入的长度
P.refcnt := ; //设置引用计数为1 //为字符串的最后面加入#0
PWideChar(Result)[length div ] := #; // length guaranteed >= 2
end;
{$ELSE}
这个函数没什么好说的,看看加入的注释就知道怎么回事了。这里可以看到,所有新字符串的引用计数都为1
赋值结束后,SS 的内容由 0 变成了 $00B23E4C,这可以从下面的图中看到。
好了,给 SS 赋值为 'Hello' 就到这里
下面看看调用 Test1 的部分
调用前
在图中可以看到,为 test1 传递参数就是 SS 的内容,即 $00B23E4C,这个地址指向的就是字符串'Hello'
并且,从图中可以看到,调用函数 test1 之前,字符串 'Hello' 的引用计数是 1,长度是 5
调用后
调用后,从图中可以看到,全局变量 SS 中保存的依然是 $00B23E4C
从上图中看 $00B23E4C 地址中的字符串,其引用计数依然是 1。
这里需要注意的是:已经有两个地方引用到了这个地址,一个是 SS,另一个就是用 const 修饰的常量参数。
从这里可以看出,用 const 修饰的字符串参数,不会引起字符串的引用计数的变化,这点很重要,问题其实就出在这里。
当在函数中改变了全局变量的值,会发生什么事情呢?我们看看下图
从图中可以看到,SS 的内容已经变为 $00B23E60 了,为什么会这样呢?我们还是来看看赋值函数 @LStrAsg 都做了些什么吧。
结合着 图4 和 图5 给出的信息我们看到,调用@LStrAsg前,SS 的内容不为空,其内容是 $00B23E4C,而这个地址就是字符串 'Hello' 所在的内存,并且,其引用计数为 1。因此,函数 @LStrAsg 的第一个参数不为空,也就是说,目标字符串不为空。因为这次赋值依然是将一个常量赋值给 SS 因此,依然会分配一块新的内存,并将新字符串的内容复制到这块内存中,再将新内存的地址给 SS 。在向下执行的时候,问题来了,因为原始的 SS 不为空,因此进入了下面这段代码:
//如果原来的目标不为空,则将其引用计数减一
//减一之后,如果引用计数为零,则释放这个字符串占的内存
if D <> nil then
begin
P := PStrRec(Integer(D) - sizeof(StrRec));
if P.refCnt > then
if InterlockedDecrement(P.refCnt) = then
FreeMem(P);
end;
//如果原来的目标不为空,则将其引用计数减一
//减一之后,如果引用计数为零,则释放这个字符串占的内存
if D <> nil then
begin
P := PStrRec(Integer(D) - sizeof(StrRec));
if P.refCnt > then
if InterlockedDecrement(P.refCnt) = then
FreeMem(P);
end;
取出原始字符串(也就是 'Hello' 字符串),的 StrRec 结构中的内容,因为其引用计数为 1 ,所以执行
if InterlockedDecrement(P.refCnt) = 0 then
减 1 后刚好是 0,因此就执行了
FreeMem(P);
看到了吧,'Hello' 这个字符串被释放了。
从下面的图中,也可以看到,确实被释放了
也就是说,当程序执行完了
SS := '你好'
之后,原来的 'Hello' 被释放了。
但是,这个函数的参数还是一个指向原来 'Hello' 的地址,因此,这参数就变成了一个无效的指针,因此,随后的
ShowMessage(s);
也就成了显示一个无效字符串的内容的语句,所以出来的是乱码。
这就是为什么当使用 const 修饰的字符串参数,为什么会出这个问题的原因。
而不使用 const 修饰的函数,因为在最开始的部分,调用了 @LStrAddRef ,导致字符串 'Hello' 的引用计数变成了 2,因此,当执行
if InterlockedDecrement(P.refCnt) = 0 then
时,其引用计数虽然被减1,但是因为原始值是二,因此不会执行
FreeMem(P);
好了,这个问题就说到这里。
上面写的东西有点多,导致看起来比较乱,简单来说:
使用 const 修饰的字符串参数,delphi在编译的时候,不会为其加入更改引用计数的代码,导致了当改变了参数原始的字符串的时候,参数所指向的字符串被意外释放,致使参数变成了无意义的字符串指针,因此会显示出乱码;而不采用 const 修饰的参数,则因为改变了引用计数,所以就不会出问题。
也许有的人会认为,既然是全局参数,在函数内部直接就可以访问到,干吗还要用参数传进来啊?
其实,这不是什么问题,一般情况下,也不会发生这种情况,但问题是,代码不是一个人的代码,A写的代码,可能B要修改,C也要修改,那么改来改去,就有可能会出现上面的情况。
全局变量这种比较敏感的使用方式往往遭人诟病,但是,类中的成员变量,一样会涉及到这个问题。只要是能被函数直接访问到的生存期自管理变量,当其作为参数传入到函数中时,如果使用 const 修饰,都会出现这个问题。
还有一种情况,就是多线程,如果使用了const 修饰,当某个生存期自管理变量被当做参数传入到线程的某个函数中时,另外一个线程更改了这个生存期自管理变量的值,这时候,前一个线程中的那个参数就会出现非法访问。这个问题更加隐蔽了。
这种问题比较隐蔽,而且错误也出的莫名其妙,因为按照正常逻辑,是不会出问题的。
这种问题出在Delphi中的生存期自管理的变量当中,如果是 variant 或者接口类型的变量,也采用 const 修饰,那估计也会出现这个问题。
另外,还有一种情况也和引用计数有关,模型如下:
procedure abc(Value: String);
var
p: PChar;
begin
p := PChar(Value);
p^ := 'a';
end; var
s, s1, s2, s3, s4: String;
begin
s3 := '';
s4 := '';
s := s3 + s4; //这里产生了内存复制
s1 := s; //这里没有产生内存复制,仅仅是引用计数加1
abc(s1);
end;
procedure abc(Value: String);
var
p: PChar;
begin
p := PChar(Value);
p^ := 'a';
end; var
s, s1, s2, s3, s4: String;
begin
s3 := '';
s4 := '';
s := s3 + s4; //这里产生了内存复制
s1 := s; //这里没有产生内存复制,仅仅是引用计数加1
abc(s1);
end;
上面的代码,应该是函数 abc 内部的代码不会影响到调用函数的部分外面,也就是调用之后,s和s1应该是不变的,但是在调用完 abc 之后,s 和 s1 的内容也被改变了!!
也就是说,从s到s1再到参数Value,Delphi始终没有进行copy-on-write,仅仅是改变了引用计数,操作Value也就是操作 s 和 s1 指向的字符串。但是从代码的形式上看,s1 := s;的本意就是把s的内容给s1,而从函数abc的声明上看,是传值调用,应该是把s1的值(也就是字符串本身)复制一份给Value,从逻辑上,操作Value不应该对s1或者s有影响,但是,实际上确实有影响。
这个错误应该是很隐蔽的!
如果把上面的abc函数换成下面的函数
procedure abc(Value: String);
var
p: PChar;
begin
p := @Value[]; //在这里,引起了内存字符串复制操作,也就是 Value 被分离出来
p^ := 'a';
end;
procedure abc(Value: String);
var
p: PChar;
begin
p := @Value[]; //在这里,引起了内存字符串复制操作,也就是 Value 被分离出来
p^ := 'a';
end;
那么s1和s的内容都不会被改变了。
这应该说是Delphi在进行copy-on-write时候没有考虑到的遗漏呢,还是应该说本来就是这个样子呢?
(以上的程序仅仅是临时写的测试程序,基本没有什么应用的价值,算是为了测试而测试吧)
Delphi 中 函数参数中的 const 修饰符的本质以及注意事项的更多相关文章
- (转)python中函数参数中如果带有默认参数list的特殊情况
在python中函数参数中如果带有默认参数list遇到问题 先看一段代码 1 2 3 4 5 6 7 8 9 def f(x,l=[]): for i in range(x): ...
- python函数参数中带有默认参数list的坑
在python中函数参数中如果带有默认参数list遇到问题 先看一段代码 def f(x,l=[]): for i in range(x): l.append(i*i) print(l) print( ...
- C++中 容易忽视的const 修饰符
C++可以用const定义常量,也可以用#define定义常量,但是前者比后者有更多的有点: (1)const常量有数据类型,而宏常量没有数据类型.编译器可以对const进行类型安全检查,而后者只进行 ...
- C/C++ 中 const 修饰符用法总结
C/C++ 中 const 修饰符用法总结 在这篇文章中,我总结了一些C/C++语言中的 const 修饰符的常见用法,供大家参考. const 的用法,也是技术性面试中常见的基础问题,希望能够帮大家 ...
- 关于cmp函数参数中的&符号
关于cmp函数参数中的&符号 关于sort函数中的cmp函数有着不同的写法,以刚刚的整形元素比较为例 还有人是这么写的: bool cmp(const int &a, const in ...
- [原创] 基础中的基础(二):C/C++ 中 const 修饰符用法总结
在这篇文章中,我总结了一些C/C++语言中的 const 修饰符的常见用法,供大家参考. const 的用法,也是技术性面试中常见的基础问题,希望能够帮大家梳理一下知识,给大家一点点帮助.作者是菜鸟一 ...
- 转载----C/C++ 中 const 修饰符用法总结
感谢原创作者,写的好详细.不忍错过,所以转载过来了... 原文地址: https://www.cnblogs.com/icemoon1987/p/3320326.html 在这篇文章中,我总结了一些C ...
- c语言中函数参数入栈的顺序是什么?为什么
看到面试题C语言中函数参数的入栈顺序如何? 自己不知道,边上网找资料.下面是详细解释 #include <stdio.h> void foo(int x, int y, int z){ ...
- JAVA方法中的参数用final来修饰的原因
JAVA方法中的参数用final来修饰的原因 很多人都说在JAVA中用final来修饰方法参数的原因是防止方法参数在调用时被篡改,其实也就是这个原因,但理解起来可能会有歧义,有的人认为是调用语句的 ...
随机推荐
- windows2008 R2 系统 安装wampserver提示“缺少msvcr110.dll文件”处理办法
windows2008 R2 系统 安装wampserver提示“缺少msvcr110.dll文件”处理办法 原因分析: 因缺少Visual C++ Redistributable for Visua ...
- MongoDB Linux下的安装和启动
1. 下载MongoDB,此处下载的版本是:mongodb-linux-i686-1.8.1.tgz.tar. http://fastdl.mongodb.org/linux/mongodb-linu ...
- powerdesigner 外键生成sql语句设置在创建表里面
根据情况需要将创建外键表的sql语句生成在创建表的sql语句中,如下设置:
- 【Python】Linux crontab定时任务配置方法(详解)
CRONTAB概念/介绍 crontab命令用于设置周期性被执行的指令.该命令从标准输入设备读取指令,并将其存放于“crontab”文件中,以供之后读取和执行. cron 系统调度进程. 可以使用它在 ...
- HTML5表单提交与PHP环境搭建
PHP服务器使用xampp集成套件 路径 D:\xampp\htdocs\MyServer\index.php 访问 http://localhost/MyServer/index.php 能够正常显 ...
- [洛谷P2044][NOI2012]随机数生成器
题目大意:给你$m,a,c,X_0,n,g$,求$X_{n+1}=(a\cdot X_n+c) \bmod{m}$,最后输出对$g$取模 题解:矩阵快速幂+龟速乘,这里用了$long\;double$ ...
- [Leetcode] subsets ii 求数组所有的子集
Given a collection of integers that might contain duplicates, S, return all possible subsets. Note: ...
- 【ZJ选讲·压缩】
给一个由小写字母组成的字符串(len<=50) 我们可以用一种简单的方法来压缩其中的重复信息. 用M,R两个大写字母表示压缩信息 M标记重复串的开始, R表示后面的一段字符串重复从上一个 ...
- [SCOI2012]喵星球上的点名——堪称十种方法做的题
题意: 给你N个串对,M个询问串,对每个询问串求是多少串对的子串(在串对的某一个中作为子串),以及每个串对最终是包含了多少询问串 方法众多.. 可谓字符串家族八仙过海各显神通. 复杂度不尽相同,O(n ...
- c#中数据库字符串的连接几种方式
ADO.net 中数据库连接方式(微软提供) 微软提供了以下四种数据库连接方式:System.Data.OleDb.OleDbConnectionSystem.Data.SqlClient.SqlCo ...