在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
来自: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 处执行的代码。 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 处执行的代码。 mov [ebp-$04],eax mov eax,[ebp-$04] call @LStrAddRef xor eax,eax push ebp push $0045214b push dword ptr fs:[eax] mov fs:[eax],esp mov [ebp-$04],eax mov eax,[ebp-$04] 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 所指向的内存中。 push ebp
esp是栈指针,总是指向栈顶,因此,ebp所指向的内容就是这个函数用到的栈的栈顶的位置。紧接着:
push ecx
四个压栈指令,导致在栈中预留出了4个位置。ebp-$04 就是栈中第一个预留位置。因此,
mov [ebp-$04],eax
就是将函数参数的第一个参数放入到栈中。
mov eax,[ebp-$04]
这两句完成了一个功能,就是调用@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 >= 0 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 >= 0 then InterlockedIncrement(P.refcnt); Result := Pointer(str); end; {$ELSE} 其中用到了PStrRec,其结构如下 type
简单解释下: 好了 mov eax,[ebp-$04] 所完成的功能就是让字符串 ebp-$04 中保存的字符串的引用计数加一,而 ebp-$04 中保存的,恰好是函数第一个参数传入的内容,也就是说,这两句完成的是让传入的字符串参数的引用计数加一
xor eax,eax 这几句是与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 赋值的语句为上面红框框出来的部分,调用了@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 < 0 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 > 0 then if InterlockedDecrement(P.refCnt) = 0 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 < 0 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 > 0 then if InterlockedDecrement(P.refCnt) = 0 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 <= 0 then Exit; //开辟一块内存,其大小是 Length 加 StrRec 结构的长度加1 //之所以要加1,是为了在字符串的最后加入一个#0,以便和PChar类型兼容 //后面的((length + 1) and 1) 是为了地址对齐而加入的,暂时不用理会 GetMem(P, length + sizeof(StrRec) + 1 + ((length + 1) and 1)); //返回新开辟的内存,注意,这里不是返回其首地址, //而是跳过了 StrRec 结构 Result := Pointer(Integer(P) + sizeof(StrRec)); P.length := length; //设置新字符串长度为传入的长度 P.refcnt := 1; //设置引用计数为1 //为字符串的最后面加入#0 PWideChar(Result)[length div 2] := #0; // length guaranteed >= 2 end; {$ELSE}
//传入字符串的长度,返回新分配的内存 function _NewAnsiString(length: Longint): Pointer; {$IFDEF PUREPASCAL} var P: PStrRec; begin Result := nil; if length <= 0 then Exit; //开辟一块内存,其大小是 Length 加 StrRec 结构的长度加1 //之所以要加1,是为了在字符串的最后加入一个#0,以便和PChar类型兼容 //后面的((length + 1) and 1) 是为了地址对齐而加入的,暂时不用理会 GetMem(P, length + sizeof(StrRec) + 1 + ((length + 1) and 1)); //返回新开辟的内存,注意,这里不是返回其首地址, //而是跳过了 StrRec 结构 Result := Pointer(Integer(P) + sizeof(StrRec)); P.length := length; //设置新字符串长度为传入的长度 P.refcnt := 1; //设置引用计数为1 //为字符串的最后面加入#0 PWideChar(Result)[length div 2] := #0; // length guaranteed >= 2 end; {$ELSE}
这个函数没什么好说的,看看加入的注释就知道怎么回事了。这里可以看到,所有新字符串的引用计数都为1
好了,给 SS 赋值为 'Hello' 就到这里
下面看看调用 Test1 的部分 在图中可以看到,为 test1 传递参数就是 SS 的内容,即 $00B23E4C,这个地址指向的就是字符串'Hello'
调用后,从图中可以看到,全局变量 SS 中保存的依然是 $00B23E4C 从上图中看 $00B23E4C 地址中的字符串,其引用计数依然是 1。 从图中可以看到,SS 的内容已经变为 $00B23E60 了,为什么会这样呢?我们还是来看看赋值函数 @LStrAsg 都做了些什么吧。 //如果原来的目标不为空,则将其引用计数减一 //减一之后,如果引用计数为零,则释放这个字符串占的内存 if D <> nil then begin P := PStrRec(Integer(D) - sizeof(StrRec)); if P.refCnt > 0 then if InterlockedDecrement(P.refCnt) = 0 then FreeMem(P); end;
//如果原来的目标不为空,则将其引用计数减一 //减一之后,如果引用计数为零,则释放这个字符串占的内存 if D <> nil then begin P := PStrRec(Integer(D) - sizeof(StrRec)); if P.refCnt > 0 then if InterlockedDecrement(P.refCnt) = 0 then FreeMem(P); end; 取出原始字符串(也就是 'Hello' 字符串),的 StrRec 结构中的内容,因为其引用计数为 1 ,所以执行
if InterlockedDecrement(P.refCnt) = 0 then
减 1 后刚好是 0,因此就执行了
FreeMem(P);
看到了吧,'Hello' 这个字符串被释放了。 也就是说,当程序执行完了
SS := '你好'
之后,原来的 'Hello' 被释放了。
ShowMessage(s);
也就成了显示一个无效字符串的内容的语句,所以出来的是乱码。
而不使用 const 修饰的函数,因为在最开始的部分,调用了 @LStrAddRef ,导致字符串 'Hello' 的引用计数变成了 2,因此,当执行
if InterlockedDecrement(P.refCnt) = 0 then
时,其引用计数虽然被减1,但是因为原始值是二,因此不会执行
FreeMem(P);
好了,这个问题就说到这里。
上面写的东西有点多,导致看起来比较乱,简单来说:
也许有的人会认为,既然是全局参数,在函数内部直接就可以访问到,干吗还要用参数传进来啊? 全局变量这种比较敏感的使用方式往往遭人诟病,但是,类中的成员变量,一样会涉及到这个问题。只要是能被函数直接访问到的生存期自管理变量,当其作为参数传入到函数中时,如果使用 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 := '1234'; s4 := '5678'; 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 := '1234'; s4 := '5678'; s := s3 + s4; //这里产生了内存复制 s1 := s; //这里没有产生内存复制,仅仅是引用计数加1 abc(s1); end; 上面的代码,应该是函数 abc 内部的代码不会影响到调用函数的部分外面,也就是调用之后,s和s1应该是不变的,但是在调用完 abc 之后,s 和 s1 的内容也被改变了!! 如果把上面的abc函数换成下面的函数 procedure abc(Value: String); var p: PChar; begin p := @Value[1]; //在这里,引起了内存字符串复制操作,也就是 Value 被分离出来 p^ := 'a'; end;
procedure abc(Value: String); var p: PChar; begin p := @Value[1]; //在这里,引起了内存字符串复制操作,也就是 Value 被分离出来 p^ := 'a'; end; 那么s1和s的内容都不会被改变了。
(以上的程序仅仅是临时写的测试程序,基本没有什么应用的价值,算是为了测试而测试吧) |
2023-10-27
2022-08-15
2022-08-17
2022-09-23
2022-08-13
请发表评论