如何编写正确的析构函数和终结器



我正在尝试弄清楚如何在 C++/CLI 中正确清理我的对象。

我阅读或浏览了这两篇文章(一篇,两篇),查看了标准并查看了其他一些问题,尤其是这个问题。

我有各种信息:

  1. 终结器应清理非托管资源(因此,当对象被垃圾回收时,所有内容都会被清理。
  2. 析构函数应该清理托管资源(删除 Foo 或 Foo.Dispose()?)并调用终结器(根据 1)
  3. 析构函数和终结器都可以多次调用(参见 3 p. 26 8.8.8 结尾)
  4. 如果析构函数被调用,则终结器将不再被调用(根据 1)(不是由 CLR 调用,也就是说,您仍然可以自己调用它)
  5. 析构函数
  6. 将调用基类析构函数(参见 3 第 25 页)
  7. 具有
  8. 终结器的类应始终具有析构函数(大概是为了确定性地清理非托管资源)
  9. 调用终结器
  10. 不会调用基类终结器(3 19.13.2 p. 131)

但也有很多混乱,部分原因是

  1. 终结器在 C# 中称为析构函数
  2. 析构函数在内部生成"释放"和
  3. "终结"方法(不确定"终结"),但"终结"方法不是终结器
  4. 析构函数的语义在C++上是不同的以及一般而言具有确定性清理和垃圾收集的复杂性

我想要的答案是一个类的示例,其中包含可能包含的所有不同类型的数据(托管的,非托管的,托管的但可处置的,无论您能想到的任何其他数据)以及正确编写的析构函数和终结器。

我有两个更具体的问题:

    通过
  1. 仅拥有一个bool hasBeenCleanedUp成员并使析构函数/终结器中的整个代码以此为条件来处理多次调用的可能性是否可以接受?
  2. 哪种数据只能由析构函数清理,但不能在终结器中清理,因为它可能已被 GC 清理?

不是您问题的完整答案,但太长而不适合评论。

在完全托管的世界中,每个对象仅引用托管对象,因此不需要终结器或析构函数,因为唯一的资源是内存GC 会处理它

当您引用非托管资源时,您有责任在不再需要它们时释放它们

因此,您需要实现专用的清理代码

有两种可能性:

  • 您知道何时不再需要非托管资源,以便可以确定地运行清理代码,这是通过析构函数/Dispose 实现的

  • 您不知道何时不再需要这些资源,因此您在最后一刻推迟清理,当 GC 收集包装资源的对象时,这是通过终结器实现的

猜在第一种情况下要好得多,因为您不会消耗比您需要的更多的内存,并且可以避免 GC 进程的一些额外开销。

您通常同时实现这两种方法,因为实例的生存期可能因使用情况而异。

CLR 级别,没有确定性清理之类的东西,只有终结器。

语言/API 级别,支持确定性清理:

  • 在本机C++中,在退出作用域或"删除"时调用析构函数

  • 在 .Net 世界中,您有"处置"模式

  • 在纯托管C++/CLI 世界中,析构函数映射到 Dispose

当你有机会确切地知道何时可以运行清理代码时,你调用(或让基础结构调用)析构函数。清理完成后,您可以摆脱所有完成过程,以便可以在下一个 GC 中立即收集对象。

关于您的第一个系列观点的一些澄清:

  1. 是的

  2. 析构函数还负责清理非托管资源;如果终结器是你分解清理代码的位置,它可以调用终结器。

  3. 从技术上讲,
  4. 它们可以,但从逻辑上讲,您应该使用简单的布尔守卫来防止它

  5. 是的,
  6. 因为应完成所有清理,因此您要求 CLR 不要完成对象

  7. 是,因为基类知道它分配了哪些资源

  8. 是的,这是用于确定性清理

  9. 你应该确保是这种情况

而其他的:

  1. 是的 ~MyClass 映射到 Finalize 方法的重写

  2. 如上所述,析构函数映射到 Dispose,但您应该自己实现终结器:!我的类

  3. 摘要:C++析构函数和 Dispose 模式用于确定性清理,C# 析构函数C++/CLI 终结器用于由 GC 触发的非确定性清理。

最新更新