如果 "Assigned()" 无法检测到 "悬空指针",该如何检测?

15
在另一个问题中,我发现Assigned()函数与Pointer <> nil相同。我一直认为Assigned()是用来检测这些悬空指针的,但现在我知道它不行。悬空指针是那些可能在某个时候创建的指针,但已经被释放并且尚未分配给nil
如果Assigned()无法检测悬空指针,那么可以用什么来检测呢?我想检查我的对象确实是一个有效的创建对象,然后再试着去操作它。我不使用很多人推荐的FreeAndNil,因为我喜欢更加直接地操作。我只是使用SomeObject.Free
访问冲突是我最大的敌人-我会尽我所能防止它们的出现。

3
据我所知,你无法确定指针是否指向有效对象。这就是为什么你应该始终使用FreeAndNil的原因——它非常直接。 - Seth Carnegie
1
那么我在过去5年中键入的500多行“Object.Free”代码不是正确的方式吗? - Jerry Dodge
3
它能起作用,但不是最好的方法,正是因为你正在遇到这个问题。他们不会无缘无故地创造FreeAndNil. - Seth Carnegie
2
@Seth:你一直在追踪Delphi新闻组中的FreeAndNil线程吗?只是问一下,我不想再次引发那场圣战。这里。 - afrazier
1
@afrazier 那看起来我是走进了某些战场的中心? - Jerry Dodge
显示剩余9条评论
6个回答

14

如果您的作用域中有一个对象变量,它可能是一个有效的引用,也可能不是,那么您应该使用FreeAndNil。或者修复您的代码,使您的对象引用更紧密地管理,这样就永远不会有疑问了。

访问冲突不应被视为敌人。它们是错误:意味着您犯了一个需要修复的错误。(或者是您所依赖的某些代码中存在错误,但我发现在处理RTL、VCL或Win32 API时,我大多数情况下都会出错。)


10
当您有一个“拥有”对象的单个指针引用时,FreeAndNil 是有效的,但是当您有多个对对象的引用时,它将无法帮助。FreeAndNil 将把给定的指针设置为 nil,但是对被释放对象的任何其他引用也会变成“悬空指针”,就像正常释放一样。 - dthorpe

13
有时候可以检测指针所指向的地址是否是在堆上的已释放内存块列表中。但是,这需要将指针与堆的每个可能包含数千个块的空闲列表进行比较。因此,这可能是一个计算密集型操作,并且除了在严重诊断模式下之外,您不会经常这样做。
这种技术只有在指针曾经指向的内存块继续停留在堆空闲列表中时才有效。随着从堆中分配新对象,释放的内存块很可能会从堆空闲列表中删除,并重新作为新对象的容器被使用。原始的悬空指针仍然指向相同的地址,但是该地址上存储的对象已更改。如果新分配的对象与已释放的原始对象是相同(或兼容)类型,则实际上几乎无法知道指针最初是引用先前对象的。事实上,在这个非常特殊和罕见的情况下,悬空指针实际上可以完美地工作。唯一可观察到的问题可能是如果有人注意到数据在指针下意外地发生了变化。
除非您正在快速地反复分配和释放相同类型的对象,否则新分配的对象很可能不是与原始对象相同类型的。当原始对象和新对象的类型不同时,您有可能弄清楚指针下面的内容已经发生了改变。但是,要做到这一点,您需要知道指针所指向的原始对象的类型。在许多本地编译应用程序的情况下,指针变量本身的类型在运行时并不保留。就CPU而言,指针就是指针-硬件对数据类型几乎一无所知。在严重的诊断模式下,可以构建一个查找表,将每个指针变量与其分配和赋值的类型相关联,但这是一项巨大的任务。
这就是为什么Assigned()不是指针有效性的断言。它只是检测指针是否为nil。

Borland为什么创建了Assigned()函数?为了让初学者和偶尔编程的人更好地隐藏指针。函数调用比指针操作更容易阅读和理解。


如果我没记错,Assigned 的出现是为了解决另一个问题,当处理事件指针时,你实际上有一个变量包含两个指针(一个指向方法代码,另一个指向该类的对象实例),因此仅检查 nil 是不够的,在这种情况下 Assigned 能够胜任。当然,@dthorpe 你肯定比我更清楚,如果我说错了,请纠正我。 - jachguate
是的,Assigned() 也负责测试空事件指针。 - dthorpe
Allen Bauer 在博客中讨论了这个确切的主题:已分配还是未分配,这是个问题... - Remy Lebeau

9
底线是,你不应该尝试在代码中检测悬空指针。如果你在释放指针时仍要引用它们,那么在释放时将指针设置为nil。但最好的方法是不要在释放后引用指针。
那么,如何避免在释放后引用指针呢?有几种常见的惯用法可以帮助你做到这一点。
1. 在构造函数中创建对象,在析构函数中销毁对象。这样,你就无法在创建之前或销毁之后引用指针了。 2. 使用局部变量指针,在函数开始时创建它,在函数结束时销毁它。
我强烈建议你避免在代码中编写if Assigned()测试,除非指针未被创建是预期行为。你的代码将变得难以阅读,你也会失去追踪指针是否为nil的能力。
当然,我们都会犯错并留下悬空指针。使用FreeAndNil是一种廉价的方法,可以确保检测到悬空指针访问。更有效的方法是在完全调试模式下使用FastMM。我强烈推荐这个工具。如果你还没有使用这个神奇的工具,你应该尽快开始使用。
如果你发现自己在处理悬空指针时很困难,并且很难弄清楚原因,那么你可能需要重构代码以适应上述两种惯用法之一。
你可以将其与数组索引错误进行类比。我的建议是不要在代码中检查索引的有效性。相反,使用范围检查,让工具来完成工作并保持代码整洁。唯一的例外是输入来自于程序外部,例如用户输入。
最后,我要说:只有在指针为nil是正常行为时才编写if Assigned

7

使用内存管理器,例如FastMM,提供调试支持,特别是使用给定的字节模式填充已释放内存块。然后,您可以引用指针以查看它是否指向以字节模式开头的内存块,或者可以让代码正常运行并通过悬空指针尝试访问已释放的内存块来引发异常。 AV报告的内存地址通常要么完全与字节模式相同,要么非常接近。


3
我很喜欢FastMM4的这个功能。值得注意的是,当运行在FullDebugMode模式下时,在释放对象时进行内存填充的这种模式只会发生一次。 - Francesca
我猜如果我在调试某些东西时,这会很方便,但我的目标是将此检查放在最终应用程序中。 - Jerry Dodge

6

没有任何方法可以发现悬空指针(曾经是有效的,但现在不再有效)。确保在释放其内容时将其设置为nil,或者仅在其有效范围内限制指针变量的作用域是您的责任。(如果可能,第二种解决方案是更好的选择。)


0
核心问题在于,Delphi中对象的实现方式存在一些内置的设计缺陷:
  • 对象和对对象的引用之间没有区别。 对于“普通”变量,比如标量(如int)或记录,这两种用法可以很好地区分 - 要么有一个类型IntegerTSomeRec,要么有一个类型PInteger = ^IntegerPSomeRec = ^TSomeRec,它们是不同的类型。这听起来可能像是微不足道的技术细节,但实际上并非如此: SomeRec: TSomeRec表示“该作用域是该记录的原始所有者并控制其生命周期”,而SomeRec: PSomeRec则表示“该作用域使用某些数据的瞬态引用,但无法控制记录的生命周期”。因此,尽管听起来很愚蠢,但对于对象而言,实际上没有人明确地控制其他对象的生命周期。结果是 - 惊讶 - 在某些情况下,对象的生命周期状态可能不清楚。
  • 对象引用只是简单指针。基本上没问题,但问题在于,有很多代码将对象引用视为32位或64位整数。因此,如果Embarcadero想要更改对象引用的实现(并使其不再是简单指针),他们将破坏很多代码

但是,如果 Embarcadero 想要消除悬掛的物件指標,他們就必須重新設計 Delphi 物件參考:

  • 當一個物件被釋放時,所有對它的引用都必須被釋放。這只有通過雙重鏈接才能實現,也就是說,物件實例必須攜帶一個列表,其中包含所有對它的引用,也就是這些指針所在的所有記憶體地址(在最低層上)。在銷毀時,該列表被遍歷,所有這些指針都被設置為 nil
  • 更舒適的解決方案是,“持有”這樣一個引用的人可以註冊一個回調函數,在引用的物件被銷毀時得到通知。在代碼中:當我有一個引用 FSomeObject: TSomeObject 時,我希望能夠在 SetSomeObject 中編寫 FSomeObject.OnDestruction := Self.HandleDestructionOfSomeObject。但是,FSomeObject 就不能是一個指針;相反,它至少必須是一個(高級)記錄型別

當然,我可以自己實現所有這些,但那是很繁瑣的,難道不是應該由語言本身來解決嗎?他們還成功實現了 for x in ...


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