使用FreeAndNil之后再使用对象会发生什么?

8
在我的Delphi7中,这段代码为:
var MStr: TMemoryStream;
...
FreeAndNil(MStr);
MStr.Size:=0; 

生成了一个AV:在模块“Project1.exe”中的地址0041D6D1处访问冲突。读取地址00000000。 但有人坚称不应该引发任何异常。他还说他的Delphi 5确实没有引发任何异常。他称之为“陈旧指针错误”。 换句话说,他说FreeAndNil不能用作调试器来检测双重尝试释放对象或使用已释放对象的情况。

有人能给我解惑吗?这个程序是否应该出现错误(总是/随机)或者程序是否应该无问题地运行过这个bug?

谢谢


我问这个问题是因为我相信我的程序中有“双重释放对象”或“释放并重新访问”的bug。我如何在释放对象后将分配给对象的内存填充为零?我希望通过这种方式来检测错误位置,通过获取AV。 最初,我希望如果将对象设置为FreeAndNil,那么在尝试重新访问它时,我将始终收到AV。


2
我认为“某人”缺少一些重要的概念。 - Toon Krijthe
1
正如我之前所说,FastMM有一个选项可以实现你描述的功能。Rob指出,它会用“魔数”覆盖内存,以便检测此类错误。你可以看一下它。下载完整的FastMM4,并启用文件日志记录。我发现它非常有用。 - Vegar
2
FreeAndNil() 不会将实例占用的内存清零,它只会将传递的引用置空。 - Bevan
6个回答

21

即使有时它似乎起作用,但使用空引用的方法或属性总是错误的。

FreeAndNil 确实不能用于检测双重释放。在已经为空的变量上调用 FreeAndNil 是安全的。由于它是安全的,所以它不能帮助你检测任何问题。

这不是陈旧的指针错误,而是空引用错误。陈旧的指针错误是当您释放了一个对象但没有清除所有引用它的变量时发生的。然后该变量仍然保留着对象的旧地址。这些非常难以检测。您可以像这样获得此类错误:

MStr := TMemoryStream.Create;
MStr.Free;
MStr.Size := 0;

你也可以获取一个像这样的:

MStr := TMemoryStream.Create;
OtherStr := MStr;
FreeAndNil(MStr);
OtherStr.Size := 0;
在释放引用了对象MStr之后使用MStr.Size是错误的,应该引发异常。是否引发异常取决于实现方式。也许会,也许不会,但这并非随机的。
如果您正在寻找双重释放错误,则可以使用FastMM提供的调试工具,正如其他人所建议的那样。它的工作方式不是实际将内存释放回操作系统,甚至不是释放回Delphi的内部自由内存池。相反,它将已知的坏数据写入对象的内存空间中,因此当您看到这些值时,就会知道您正在从已经释放的东西中读取。它还修改了对象的VMT,以便下次在该对象引用上调用虚拟方法时,您将获得可预测的异常,并且它甚至告诉您尝试使用哪个被认为已释放的对象。当您再次尝试释放对象时,它不仅可以告诉您已经释放了它,而且还可以告诉您第一次释放的位置(带有堆栈跟踪)和分配位置。它还收集这些信息以报告内存泄漏,其中您释放了一个对象少于一次,而不是多次。
还有一些习惯可以用来避免未来代码的问题:
- 减少全局变量的使用。全局变量可以被程序中的任何代码修改,迫使您在使用它时思考,“这个变量的值是否仍然有效,还是某些其他代码已经释放了它?”当您限制变量的作用范围时,减少了您需要考虑的程序代码量,以寻找变量没有您预期的值的原因。 - 明确对象的所有者。当有两个代码片段都可以访问同一个对象时,您需要知道哪个代码片段拥有该对象。它们可能各自具有用于引用该对象的不同变量,但仍然只有一个对象。如果一个代码片段在其变量上调用FreeAndNil,那么它仍然会保留另一个代码片段的变量不变。如果另一个代码片段认为自己拥有该对象,则会遇到麻烦。(这个所有者的概念不一定与TComponent.Owner属性绑定。它可能是程序的一般子系统。) - 不要保持对您不拥有的对象的持久引用。如果您不保留长期引用对象的引用,则无需担心这些引用是否仍然有效。唯一的持久引用应该在拥有该对象的代码中。需要使用该对象的任何其他代码应该将引用作为输入参数接收,使用该对象,然后在返回其结果时丢弃引用。

10

根据我所看到的代码,这段代码应该总是会导致错误。FreeAndNil明确地将传递的值设为了Nil(又称为0),因此在试图取消引用该对象时,绝对会出现访问冲突。


8

仅为了让问题更加复杂:

如果你所调用的方法是静态方法(非虚方法),并且它本身既不调用任何虚方法,也不访问对象的任何字段,则即使对象引用已被设置为NIL,你也不会收到访问冲突的提示。

造成这种情况的原因是对self指针(在此情况下为NIL)进行解引用导致访问冲突,但只有当访问字段或对象的VMT以调用虚方法时才会发生。

这只是一个特殊情况,即你不能调用一个NIL对象引用的方法,在这里我想提一下。


5
如果您将指针设置为nil,就不能再使用它了。但是,如果您有另一个指向同一对象的指针,则可以使用它而不会出现AV(访问冲突),因为该指针仍然指向对象地址而不是nil。
此外,释放对象并不清除该对象使用的内存。它只是将其标记为未使用。这就是为什么您不会得到AV的原因。如果释放的内存被分配给另一个对象,则会得到AV,因为它不再包含看起来有效的数据。
FastMM4有一些调试时可用的设置,可以检测到这种情况。从FsatMM4Options.inc中:
{将以下选项设置为对所有内存块进行广泛检查。所有块都带有用于验证堆完整性的标题和尾随内容。释放的块也被清除以确保在释放后不能重新使用。此选项会极大地减慢内存操作速度,应仅用于调试覆盖内存或重用已释放指针的应用程序。启用此选项会自动启用CheckHeapForCorruption并禁用ASMVersion。非常重要:如果启用此选项,您的应用程序将需要FastMM_FullDebugMode.dll库。如果此库不可用,则会在启动时收到错误。} {$define FullDebugMode}
同一文件中的另一段引用:
FastMM始终捕获尝试两次释放同一内存块的情况...
由于Delphi 2007(2006?)使用FastMM,因此如果尝试双重释放对象,则应该会收到错误。

Free和nil都会释放对象,所以对它的另一个指针也将失败。 - Toon Krijthe
这是否适用于旧版Delphi和经典内存管理器,还是仅适用于带有FastMM的Delphi 2007+?如果您所说的是真的,那么在FastMM4Options.inc中加入评论的目的是什么? - Vegar
Gamecat,重复使用一个过时(非nil)指针可能不会引发任何异常(并且有可能正常工作)。当然,这是一件坏事,如果它失败了那就好了千倍,但至少在Delphi 5和7中,您可以重复使用过时的指针。 - Mihai Limbășan
所有版本都是这样的。FastMM的注释意味着释放的块不能用于有用的目的。您仍然可以读取内存中的内容,但它将是FastMM在那里写入的已知不良值,而不是对象之前的值。 - Rob Kennedy
Moocha,你可以在所有版本中取消引用过时的指针。但这并不意味着你会得到正确的值。如果关闭FastMM的调试功能,它可能会重新分配已释放的内存给其他东西。你的过时指针将获取该数据,而不是对象的先前值。 - Rob Kennedy

1

Thomas Mueller:你试过使用虚拟类方法吗?构造函数有点像虚拟方法,但是你对类型而不是实例调用它。这意味着即使某些特定的虚拟方法会导致空引用的 AV 错误。

Vegar:你说的太对了!FastMM 是最好的工具之一,帮助我跟踪此类错误。


调用实例变量的虚拟类方法会失败。构造函数与此无关。 - Rob Kennedy

1

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