Delphi 中 函数参数中的 const 修饰符的本质以及注意事项
分类:
Delphi2010-07-28 22:33 882 人阅读 评论(2) 收藏 举报
很多书籍中说函数参数如果是 String 类型的,如果在函数内部不改变参数的值,使用 const 修饰符会加快程序的执行速度,至于如何加快的?有的人说是因为 const 函数保证 了参数字符串不会被复制。 以前也没有对这个问题深入研究, 但是在不修改函数参数的时候, 总是习惯加上 const 修饰符, 前几天在 csdn 论坛上解答某个人的问题是, 发现程序产生的 结果和预期的不一样,检查了一遍代码,发现没有什么
问题,按理不应该出现错误。于是跟 踪调试, 发现是用 const 修饰的一个 String 类型的函数参数在该函数中被意外地更改了其 内容,这很奇怪啊,因为在函数内部没有修改其值的地方,况且用 const 修饰的参数,如 果存在修改其内容的语句,编译都过不去,更何谈执行了。于是,就跟踪代码,查找原因, 这一跟踪,发现了一个问题: 跟踪测试代码如下:
[delphi] view plaincopy
1. var 2. 3. 4. procedure test1(const s: String); 5. begin 6. 7. 8. 9. 10. try SS := '你好'; ShowMessage(s); except end; SS: String;
11. end; 12. 13. procedure TfrmMain.btnTestClick(Sender: TObject); 14. begin 15. 16. SS := 'Hello'; test1(SS);
17. end;
代码很简单,按照程序逻辑,ShowMessage(s) 的结果应该显示 "Hello",有些人看了前 面的描述,可能会觉得,好像应显示 "你好",这些都不对,显示的是乱码!呵呵,吓一跳 吧!好了,下面我就来解释下,为什么会显示乱码: 首先,看看加了 const 修饰符到底会有什么不同:
[delphi] view plaincopy
1. procedure test1(const s: String); 2. begin 3. 4. 5. 6. 7. try SS := '你好'; ShowMessage(s); except end;
8. end;
在 Test1 函数的 begin 处断开,然后看看汇编代码到底干了什么:
上图是在 begin 处加了断点后,
程序运行到这里的汇编代码,其中,红色方框中的,就 是在 begin 到第一句语句 try 处执行的代码。 再看看不加 const 会怎么样:
[delphi] view plaincopy
1. procedure test1(s: String);
2. begin 3. 4. 5. 6. 7. try SS := '你好'; ShowMessage(s); except end;
8. end;
在 Test1 函数的 begin 处断开,然后看看汇编代码到底干了什么:
上图就是不加 const 修饰符,程序运行到 begin 处的汇编代码,红色方框中,就是 begin 到第一句语句 try 处执行的代码。 对比上面的两处代码,发现不加 const 修饰符,多了一段代码
[delphi] view plaincopy
1. mov 2. mov
[ebp-$04],eax eax,[ebp-$04]
3. call @LStrAddRef 4. 5. xor eax,eax
6. push ebp 7. push $0045214b 8. push dword ptr fs:[eax] 9. 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 在 函数完成的功能
[delphi] view plaincopy
1. function _LStrAddRef(var str): Pointer; 2. {$IFDEF PUREPASCAL} 3. var 4. P: PStrRec;
5. begin 6. 7. 8. 9. 10. P := Pointer(Integer(str) - sizeof(StrRec)); if P <> nil then if P.refcnt >= 0 then InterlockedIncrement(P.refcnt); Result := Pointer(str);
11. end; 12. {$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 d
word ptr fs:[eax] mov fs:[eax],esp 这几句是与 delphi 实现的异常处理有关,在这里不做说明了,和本