在C++中如何从析构函数中恢复对象?

49

免责声明:我知道这是不好的设计,我只是出于好奇问问题,以便更深入地了解C++中析构函数的工作原理。

在C#中,可以在类的析构函数中编写GC.KeepAlive(this) (请参见下面的编辑),这意味着即使析构函数调用完成后,对象仍将存活在内存中。

C++的设计是否允许从析构函数中复活对象,类似于上述C#算法?

编辑:正如下面的答案所指出的那样,GC.ReRegisterForFinalize()与问题更相关,而不是GC.KeepAlive(this)


1
@becko 这怎么可能?如果类中的所有类型都是POD或RAII,那么一个空析构函数就足够了。 - NathanOliver
2
在C++中,您可以通过放置new来分离分配/释放和构造/析构。 - Jarod42
17
不行。一旦调用析构函数,对象就完成了其生命周期。即使析构函数为空,对象使用的内存也将返回给系统。空析构函数不会阻止类被销毁。 - NathanOliver
1
如何“复活”一个对象? 复制它。在析构函数中,对象仍然存在,因此您可以创建其副本。问题在于,当该类是另一个类的父类时-其子类的析构函数已经执行,因此子部分不再存在。 - Marian Spanik
1
析构函数的工作是“拆除”对象。因此,即使您在销毁后以某种方式访问它(这是不可能的),此时对象也可以处于未定义状态。由于C++在其析构函数中是确定性的,因此RAII很强大:后端对象被销毁。数据库连接和所有文件句柄都已关闭。对象已经“死亡”。无论您如何将其复活为僵尸,它的大脑都已经消失了,它将一无是处... - DevSolar
显示剩余5条评论
6个回答

106
简短回答:不是的。C++不像Java或C#一样使用垃圾回收。当一个对象被销毁时,它会立即被销毁,永远消失了。这个过程不分两步进行。析构函数是对象销毁的一部分。在调用析构函数和释放用于对象本身的内存时,对象就被销毁了。尽管析构函数正在运行,对象仍然存在,供析构函数使用,但它存在于借来的时间。一旦析构函数被调用,对象就会被销毁,没有任何东西可以改变它的命运。需要注意的是,如果对象最初是在堆上使用“new”分配的,现在正在使用“delete”进行删除。如果对象在栈上分配,则执行线程退出作用域,因此作用域中声明的所有对象都将被销毁。析构函数实际上是作为对象被销毁的结果而被调用的。这意味着对象正在被销毁。另外,C++允许您为类实现自定义分配器。如果需要,您可以编写自己的内存分配和释放函数,实现所需的功能。不过,这些函数永远不会用于栈分配的对象(即局部变量)。

34
析构函数是对象销毁的一部分。对象的销毁包括调用析构函数和释放用于对象本身的内存。这是一个单一的过程,而不是两个独立的过程。当析构函数正在运行时,对象仍然存在以供析构函数使用,但是它的存在时间已经所剩无几。可以预见的是,一旦析构函数返回,对象就会被摧毁。一旦析构函数被调用,对象就会被销毁,没有任何东西能够改变其命运。 - Sam Varshavchik
3
给该对象赋值并不会替换对象成员的值。但是,当析构函数完成时,对象仍将被销毁。这是无法改变的结局,你不能改变对象的命运。你可以在第一个对象的析构函数中分配另一个与被销毁对象无关的对象,但那将是一个不同、独立的对象。 - Sam Varshavchik
7
请理解:调用析构函数的原因是因为对象要么最初使用“new”在堆上分配,现在被“delete”删除。 "delete"意味着"删除",而不是"或许删除"。所以对象将被删除。或者,如果对象在栈上分配,执行线程退出作用域,因此作用域中声明的所有对象都将被销毁。从技术上讲,析构函数被调用是由于对象被销毁的结果。 所以,对象正在被销毁。结束。 - Sam Varshavchik
5
可能会出现这种情况:你摧毁了一个对象,然后跟随指针到达它曾经所在的内存地址,并且有幸读取到有效的对象。或者,你可能会跟随指针到达例如0x8F3B2780的地址,刚好读取到相同的有效对象。但是,在这两种情况下,没有任何方法可以确定它是否有效,除非尝试一下。这两种行为都是未定义的,都不应该被鼓励。如果想要访问该对象,请不要摧毁它。 - Ray
1
第一句话有点误导性,因为拥有垃圾回收器并不一定意味着具有 OP 所要求的特殊功能。例如,Java 没有这样的功能。它有 finalize() 这个东西,可以在对象的生命周期中添加一个波浪线,但不能改变可达性原则。显然,其他具有垃圾回收功能的语言不需要具有这样的功能,因此支持生命周期扩展与是否具有 gc 无关。 - Holger
显示剩余3条评论

55
你实际上错误地描述了.NET中GC.KeepAlive的作用。它不是用于在对象析构函数中保留对象不被销毁的--实际上,GC.KeepAlive()为空,没有实现。 可以在这里查看.NET源代码 它确保在调用GC.KeepAlive之前,传递的对象参数不会被垃圾回收器回收。传递给KeepAlive的对象参数可以在调用GC.KeepAlive后立即被垃圾回收。由于KeepAlive没有实际实现,这纯粹基于编译器必须维护对要传递为KeepAlive参数的对象的引用。任何其他函数(不被编译器或运行时内联)都可以使用该对象作为参数。

7
在 .net 中,对象通过具有对它们的引用来保持存活(防止被垃圾回收),线程在这里不起作用。 - NineBerry
1
如果一个对象在另一个线程中被引用(即保持活动状态),则该对象将保持活动状态。 - MathuSum Mut
1
我想知道的是关于KeepAlive的注释中的这句话:“[...]这可能会对终结器线程造成微妙的----。”。破折号是什么意思,是为了“FU”的审查吗?开发人员在文档中不能提到“bug”这个词吗?是什么原因? - CompuChip
2
@CompuChip,在referencesource网站上很痛苦。请查看此处以获取解释。 - Lucas Trzesniewski
2
实际上, GC.KeepAlive 能够工作的唯一原因是因为 JIT 对它进行了特殊处理,否则在内联函数之后,JIT 将会发现它不需要保持该引用。 - Voo
@Voo:是的,这就是方法在 .NET 源代码中使用 [MethodImplAttribute(MethodImplOptions.NoInlining)] 的作用。我在我的回答中添加了一些细节。 - NineBerry

9

这里有一个想法:

C* gPhoenix= nullptr;

C::~C ()
{
gPhoenix= new C (*this);  // note: loses any further-derived class ("slice")
}

如果涉及的对象(基类或成员)确实有执行操作的析构函数,并且您使用delete gPhoenix;,那么就会遇到问题,因此您需要更复杂的机制,具体取决于它真正想要实现的内容。但是,如果您没有任何实际目标,只是好奇地探索,那么指出这一点就足够了。

当析构函数的主体被调用时,对象仍然完好无损。您从析构函数内部进行正常的成员函数调用时,它看起来非常重要和正常。

拥有对象的内存将被回收,因此您不能留在原地。并且在离开主体后,其他销毁会自动发生,无法干扰。但是,在此之前,您可以复制对象。


2
需要注意的一点是,如果对象实际上是子类的实例,则任何子类析构函数已经被调用。在这种情况下,情况可能会变得复杂,具体取决于子类所做的事情。最好的情况是您可以获得当前销毁类的有效实例副本,并且只失去子类添加的内容。 - hyde
@Deduplicator 析构函数的调用顺序是从子类到父类,与构造函数调用顺序相反(当你想一想时,这很自然,子类的东西依赖于它下面的有效的父类的东西)。所以我认为我上面写得是正确的。 - hyde
@hyde 我总是交换超类和子类。可能是因为超类是一个子对象。 - Deduplicator
3
超类,子类:好吧,在C++中都没有使用这些术语,所以不用担心。请使用标准和Stroustrup之前的命名法:派生类(和最派生类),基类。然后可以在定义上下文时自由地使用诸如子类之类的术语,以一般数学意义为定义。请注意,基类和派生类更难混淆! - JDługosz

6

正如已经指出的那样,GC.KeepAlive并不能做到这一点。

只要涉及到.NET,使用GC.ReRegisterForFinalize可以从终结器中复活,如果有WeakReferenceGCHandle跟踪复活,或者将this传递给类外部的某个东西,仍然可以获取对它的引用。这样做将中止销毁过程。

这是一种在.NET 2.0中检测垃圾回收的老技巧no longer relevant,但仍然有效(有点,因为垃圾回收现在可以是部分的,并且可以与其他线程并行进行)。
需要强调的是,在.NET上您正在使用一个终结器,它在销毁之前运行并可以防止其销毁。因此,虽然从技术上讲,在销毁后无法恢复对象in any language,但是您可以使用GC.ReRegisterForFinalize来接近您在.NET中描述的行为。

在C++中,您已经得到了正确的答案


4

任何语言都不可能做到这一点。

您的理解有些偏差。 GC.KeepAlive 将标记对象为垃圾回收器无法回收的对象。如果对象在非托管代码中使用,垃圾回收器无法跟踪其使用情况,则会防止垃圾回收策略销毁对象。这并不意味着对象在销毁后仍然在内存中。

一旦对象开始销毁,代码将释放资源(内存、文件处理程序、网络连接)。通常的顺序是从最深层的派生类返回到基类。如果中间某个部分阻止了销毁,就不能保证可以重新获取这些资源,并且对象会处于不一致状态。

您更希望拥有一个 std::shared_ptr,它跟踪副本和引用,并且只有在没有人再需要它时才销毁对象。


4
请注意,GC.KeepAlive()只在调用KeepAlive()之前保护对象不被垃圾回收,而不是之后。 - NineBerry
4
你对于GC.KeepAlive的理解是错误的。此外,在非托管代码中使用对象时需要将其固定,这时它就无法被垃圾回收了。 - Lucas Trzesniewski

3
如果有帮助的话,析构函数和内存分配是不同的。
析构函数只是一个函数。你可以显式地调用它。如果它没有任何破坏性,那么再次调用它(比如当对象超出范围或被删除时)就不一定会有问题,尽管这会非常奇怪; 可能标准文档中有相关部分。请参见下面的示例。例如,某些STL容器明确调用析构函数,因为它们分别管理对象生命周期和内存分配。
通常情况下,编译器会插入代码,在自动变量超出范围或用delete销毁堆分配对象时调用析构函数。在析构函数内部无法篡改内存的释放。
您可以通过提供new运算符的其他实现或使用像放置new之类的现有运算符来掌控内存分配,但一般默认行为是编译器将调用您的析构函数,并且这是一个整理的机会。一些内存随后被清除的事实是析构函数所无法控制的。
#include <iostream>
#include <iomanip>

namespace test
{
  class GotNormalDestructor
  {
    public:
      ~GotNormalDestructor() { std::wcout << L"~GotNormalDestructor(). this=0x" << std::hex << this << L"\n"; }
  };

  class GotVirtualDestructor
  {
    public:
      virtual ~GotVirtualDestructor() { std::wcout << L"~GotVirtualDestructor(). this=0x" << std::hex << this << L"\n"; }
  };

  template <typename T>
  static void create_destruct_delete(wchar_t const name[])
  {
    std::wcout << L"create_destruct_delete<" << name << L">()\n";
    {
      T t;
      std::wcout << L"Destructing auto " << name << L" explicitly.\n";
      t.~T();
      std::wcout << L"Finished destructing " << name << L" explicitly.\n";
      std::wcout << name << L" going out of scope.\n";
    }
    std::wcout << L"Finished " << name << L" going out of scope.\n";
    std::wcout << L"\n";
  }

  template <typename T>
  static void new_destruct_delete(wchar_t const name[])
  {
    std::wcout << L"new_destruct_delete<" << name << L">()\n";
    T *t = new T;
    std::wcout << L"Destructing new " << name << L" explicitly.\n";
    t->~T();
    std::wcout << L"Finished destructing new " << name << L" explicitly.\n";
    std::wcout << L"Deleting " << name << L".\n";
    delete t;
    std::wcout << L"Finished deleting " << name << L".\n";
    std::wcout << L"\n";
  }

  static void test_destructor()
  {
    {
      std::wcout << L"\n===auto normal destructor variable===\n";
      GotNormalDestructor got_normal;
    }

    {
      std::wcout << L"\n===auto virtual destructor variable===\n";
      GotVirtualDestructor got_virtual;
    }

    {
      std::wcout << L"\n===new variables===\n";
      new_destruct_delete<GotNormalDestructor>(L"GotNormalDestructor");
      new_destruct_delete<GotVirtualDestructor>(L"GotVirtualDestructor"); 
    }

    {
      std::wcout << L"\n===auto variables===\n";
      create_destruct_delete<GotNormalDestructor>(L"GotNormalDestructor");
      create_destruct_delete<GotVirtualDestructor>(L"GotVirtualDestructor");
    }

    std::wcout << std::endl;
  }
}

int main(int argc, char *argv[])
{
  test::test_destructor();

  return 0;
}

示例输出

===auto normal destructor variable===
~GotNormalDestructor(). this=0x0x23fe1f

===auto virtual destructor variable===
~GotVirtualDestructor(). this=0x0x23fe10

===new variables===
new_destruct_delete<GotNormalDestructor>()
Destructing new GotNormalDestructor explicitly.
~GotNormalDestructor(). this=0x0x526700
Finished destructing new GotNormalDestructor explicitly.
Deleting GotNormalDestructor.
~GotNormalDestructor(). this=0x0x526700
Finished deleting GotNormalDestructor.

new_destruct_delete<GotVirtualDestructor>()
Destructing new GotVirtualDestructor explicitly.
~GotVirtualDestructor(). this=0x0x526700
Finished destructing new GotVirtualDestructor explicitly.
Deleting GotVirtualDestructor.
~GotVirtualDestructor(). this=0x0x526700
Finished deleting GotVirtualDestructor.


===auto variables===
create_destruct_delete<GotNormalDestructor>()
Destructing auto GotNormalDestructor explicitly.
~GotNormalDestructor(). this=0x0x23fdcf
Finished destructing GotNormalDestructor explicitly.
GotNormalDestructor going out of scope.
~GotNormalDestructor(). this=0x0x23fdcf
Finished GotNormalDestructor going out of scope.

create_destruct_delete<GotVirtualDestructor>()
Destructing auto GotVirtualDestructor explicitly.
~GotVirtualDestructor(). this=0x0x23fdc0
Finished destructing GotVirtualDestructor explicitly.
GotVirtualDestructor going out of scope.
~GotVirtualDestructor(). this=0x0x23fdc0
Finished GotVirtualDestructor going out of scope.

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