为什么使用常量字符串参数时程序会崩溃?

7

我的程序有以下代码:

function FooBar(const s: string): string;
var
  sa: AnsiString;
begin

  // ..........................

  sa := AnsiString(s);
  sa := AnsiString(StringReplace(string(sa), '*', '=', [rfReplaceAll]));
  sa := AnsiString(StringReplace(string(sa), ' ', '+', [rfReplaceAll]));
  result := string(sa);

  // ..........................

end;

我注意到程序在某处崩溃,FastMM4报告说我已经写入了一个被释放的对象。一旦我注释掉“const”,程序就能工作了。
我已经阅读了Delphi文档关于const参数的内容,但我无法理解为什么const参数会导致程序崩溃。我很想了解其中的原因。
更新:该程序只在Delphi 6中崩溃,并且仅在开启优化时才会发生。如果关闭优化,则程序将正常工作。这可能是Delphi的一个bug吗?

你能试着这样做,不使用sa:Result:=AnsiString(s);Result:=AnsiString(StringReplace(string(Result),...`? - Stijn Sanders
为什么在 Delphi6 中使用这些转换,其中字符串默认为 AnsiString? - MBo
因为我正在编写一个组件,必须被 Delphi 6 和 XE4 使用。我不想在我的代码中出现任何警告,所以我明确地进行了强制转换(而且这个函数只做 Base64 的事情,所以强制转换是可以的,不会影响 Unicode 能力)。 - Daniel Marschall
我注意到 "sa := AnsiString(s)" 会导致 "sa" 和 "s" 指针相同。"sa" 是一个局部变量,在过程退出时将被释放,而 "s" 属于调用者,可能不会被释放。我假设自动引用计数没有起作用...为什么?"sa := s" 应该将计数器设置为2。如果我删除 "const" 或者直接将 "s" 传递给 StringReplace() 函数,它就可以工作。 - Daniel Marschall
2
如果您从此函数中删除所有类型转换和对“AnsiString”的提及,则不会收到任何隐式转换警告,因为不会有任何隐式转换。 - Rob Kennedy
显示剩余3条评论
5个回答

3

涉及到const string参数时,有一些特殊的问题需要注意。
多年前,我曾帮助一位同事解决了类似的奇怪问题(如果我没记错,是D3)。下面的简化示例可能看起来与您的具体问题不太相同,但可以给您一些灵感:

type
  TMyClass
    FString: string;
    procedure AppendString(const S: string);
  end;

procedure TMyClass.AppendString;
begin
  FString := FString + S;
end;

如果您有一个 TMyClass 的实例并尝试调用 AppendString(FString); 来复制字符串,您可能会遇到访问冲突问题。 (还有一些其他因素可能会影响您的操作。)原因如下:

  • const 在方法调用时阻止了对字符串进行引用计数。
  • 因此,当其值更改时,FString 可能具有 refCount = 1
  • 在这种情况下,写时复制不适用,字符串将被重新分配。(很可能在不同的地址上。)
  • 因此,当方法返回时,S 引用无效地址并触发 AV。

当前版本的_UStrCat(在字符串附加时调用的RTL例程)知道这一点,并且不会使用错误的地址。我没有D6,所以它可能还没有检查这一点。 - Rudy Velthuis

2
今天我们调试了一个崩溃问题,这是由于Delphi 5编译器代码生成错误引起的。
procedure TForm1.Button1Click(Sender: TObject);
var
   s: string;
begin
   s := 'Hello, world! '+IntToStr(7);
   DoSomething(s);

   //String s now has a reference count of zero, and has already been freed
   ShowMessage(s);
end;

procedure TForm1.DoSomething(const Title: string);
var
    s: AnsiString;
begin
    s := AnsiString(Title);

    if Now = 7 then
        OutputDebugString('This is a string that is irrelevant');
end;

Delphi 5编译器错误地尝试将两个引用计数都减少:

  • Title
  • s

并且导致const字符串的引用计数变为零。这将导致其被释放。所以当DoSomething返回时,您正在使用一个被释放的字符串。

一个等待发生的访问冲突。

或者,对于客户:已经发生了。


2

对于这个案例:

 sa := s;

自动引用计数(ARC)是如何工作的。这是一种惯用的方式,编译器知道如何处理这些字符串——如果sa发生变化,它会创建一个新的副本等等。
对于强制类型转换的情况(尽管类型相同)。
sa := AnsiString(s);

你告诉编译器你只想得到一个指向字符串的指针,并且你知道如何使用这个字符串引用。编译器将不会干扰或打扰你,但是你需要负责正确的操作。
P.S. 我无法在Delphi XE5中复现问题 - 简单赋值和类型转换都会导致LStrLAsg(内部函数)调用ARC。(当然,编译器魔术可能会稍微改变)

非常感谢这个有用的提示。所以,我的理解是,“sa:= AnsiString(s)”将使ARC计数器保持为1(无论s是否定义为“const s”或只是“s”?),并且由于局部变量“sa”然后被清除,s也将被清除(因为它们位于同一地址),这将导致调用者对s进行非法引用。 - Daniel Marschall
我认为这只是D6中的一个漏洞,它在当前版本中已经不存在了。 我查看了XE4中的代码,并且在那里可以正确处理这个问题。 我不认为强制转换是问题所在。编译器应该能够处理(或忽略)它们。 - Rudy Velthuis
我曾经在某处读到过,“const”在新版本的Delphi中失去了其功能(请参见https://dev59.com/IXNA5IYBdhLWcg3wAIxP#1134704,Andreas Hausladen的第二条评论)。这可能是它在新版本上运行的原因。 - Daniel Marschall
我将这个答案标记为解决方案,尽管其他答案也是正确的,因为我的原始帖子中的代码存在许多不同的问题(例如返回本地变量)。然而,你提到了一个非常有趣的事实(类型转换不会增加ARC计数器),这是我不知道的。 - Daniel Marschall

0

由于在此处转换为 AnsiString 再转回来没有意义,因此我会将其重写为

function FooBar(const s:string):string;
begin
  Result:=
    StringReplace(
    StringReplace(
      s
      ,'*','=',[rfReplaceAll])
      ,' ','+',[rfReplaceAll]);
end;

是的。但我的问题是,为什么“sa:= s”或“const”会导致程序崩溃。为什么自动引用计数没有起作用? - Daniel Marschall
因为优化与本地变量的赋值和类型转换混淆了。 - Stijn Sanders
@StijnSanders:我认为将值分配给本地变量和强制转换不应该有影响。我认为这只是D6中的一个错误。 - Rudy Velthuis

0

这是因为 AnsiString 是一个指针。所以,你不能像操作普通字符串(也就是字符数组)那样操作它。崩溃是随机的,所以你可能会在任何时候遇到堆栈溢出或访问冲突导致的崩溃,而不是优化的结果。

当你的函数 FooBar 返回结果时,变量已经从内存中释放了。

尝试使用 PChar 或 PAnsiChar,并根据需要为这些变量分配内存。


我了解到Pascal字符串使用自动引用计数,因此我不需要关心内存的分配和释放。在Delphi 6中,“string”和“AnsiString”之间有什么区别吗?我以为它们是相等的。 - Daniel Marschall
你正在使用Delphi 6还是Delphi XE6? - Rudy Velthuis

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接