从 C++ 中的析构函数中恢复对象?

Posted

技术标签:

【中文标题】从 C++ 中的析构函数中恢复对象?【英文标题】:Revive object from destructor in C++? 【发布时间】:2016-05-29 05:36:38 【问题描述】:

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

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

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

编辑:正如下面的答案所指出的,GC.ReRegisterForFinalize() 与问题的关系比GC.KeepAlive(this) 更密切。

【问题讨论】:

@becko 那将如何工作?如果类中的所有类型都是 POD 或 RAII,那么你就只有一个空的析构函数。 在c++中,你可以分开分配/释放和构造/销毁(通过placement new)。 @becko 不。一旦调用析构函数,对象就完成了。即使析构函数为空,对象使用的内存也会返回给系统。空的析构函数不会阻止类被销毁。 您应该问自己:您无法以任何方式访问的对象是否仍然存在?即使在调用析构函数之后,包含对象成员的内存很可能仍然具有所有值(直到它被另一个调用/移动/其他重新分配)但是从那里读取未定义的行为...... 如何“复活”对象?复制一份。在析构函数中,对象仍然存在,因此您可以制作它的副本。问题是当该类是另一个类的父类时 - 其子类的析构函数已被执行,因此子部分不再存在。 【参考方案1】:

简短的回答是:不。 C++ 不像 Java 或 C# 那样使用垃圾收集。当一个对象被销毁时,它会立即被销毁。一去不复返了。加入了无形的合唱团。渴望峡湾等......

并且用不同的词多说几遍,这样就不会轻易地重新解释......

析构函数作为对象销毁的一部分被调用。对象销毁包括调用析构函数和释放用于对象本身的内存。这是一个单独的过程,而不是两个单独的过程。在析构函数运行时,对象仍然存在,供析构函数使用,但它存在于借用时间。已成定局的结论是,一旦析构函数返回,对象就会被汽化。一旦调用了析构函数,对象就会被销毁,并且不会改变它的命运。

理解这一点:调用析构函数的原因是:对象最初是用“new”在堆上分配的,现在正在被“delete”。 “删除”的意思是“删除”,而不是“可能删除”。所以对象被删除了。或者,如果对象是在堆栈上分配的,则执行线程退出了作用域,因此在作用域中声明的所有对象都将被销毁。从技术上讲,析构函数是由于对象被销毁而被调用的。因此,对象正在被销毁。结局。

话虽如此,C++ 允许您为您的类实现自定义分配器。如果您愿意,您可以编写自己的自定义内存分配和释放函数来实现您想要的任何功能。尽管这些从未用于堆栈分配的对象(即局部变量)。

【讨论】:

那么析构函数是在对象被销毁之后调用的? 析构函数作为对象销毁的一部分被调用。对象销毁包括调用析构函数和释放用于对象本身的内存。这是一个单独的过程,而不是两个单独的过程。在析构函数运行时,对象仍然存在,供析构函数使用,但它存在于借用时间。已成定局的结论是,一旦析构函数返回,对象就会被汽化。一旦调用了析构函数,对象就会被销毁,并且不会改变它的命运。 没有。对此的赋值将替换对象成员的值。但是当析构函数完成时,对象仍然会被销毁。这是已成定局的结论。你无法改变对象的命运。您当然可以在第一个对象的析构函数中分配另一个与被销毁对象无关的对象,但它将是一个不同的独立对象。 理解这一点:调用析构函数的原因是因为:对象最初是用“new”在堆上分配的,现在正在被“delete”。 “删除”的意思是“删除”,而不是“可能删除”。所以对象被删除了。或者,如果对象是在堆栈上分配的,则执行线程退出了作用域,因此在作用域中声明的所有对象都将被销毁。从技术上讲,析构函数是由于对象被销毁而被调用的。因此,对象正在被销毁。结束。 @Falco 有可能您可以销毁该对象,然后跟随一个指向它曾经所在的内存的指针,并幸运地读取到一个有效的对象。您也可以遵循指向例如的指针。 0x8F3B2780,只是碰巧读取了一个相同的有效对象。然而,在这两种情况下,绝对没有 方法可以确定它是否会在不尝试的情况下工作。这两种行为同样未定义,不应以任何方式鼓励。如果您想访问该对象,不要销毁它【参考方案2】:

您实际上是在歪曲 GC.KeepAlive 在 .NET 中所做的事情。它不能用于对象的析构函数以防止该对象被破坏——实际上,GC.KeepAlive() 是空的并且没有实现。 请参阅 .NET 源代码here。

它确保作为参数传递的对象在调用GC.KeepAlive 发生之前 不会被垃圾收集。作为参数传递给 KeepAlive 的对象可以在调用 GC.KeepAlive 后立即被垃圾回收。由于KeepAlive 没有实际实现,这纯粹是基于编译器必须维护对要作为参数传递给KeepAlive 的对象的引用这一事实。也可以使用将对象作为参数的任何其他函数(不是由编译器或运行时内联)。

【讨论】:

在 .net 中,对象通过引用它们来保持活动状态(防止被垃圾收集)。线程在这里不起作用。 如果一个对象在另一个线程中被引用(即保持活动状态),则该对象保持活动状态 我想知道的是KeepAlive 上的评论:“[...] 这可能会导致终结线程的微妙 ----s。”。 “FU's”的破折号和审查是怎么回事?开发人员不允许在文档中提及“bug”这个词吗?什么? @CompuChip 是参考源站点中的 PITA。有关说明,请参阅 here。 实际上GC.KeepAlive 起作用的唯一原因是因为它被 JIT 特例化了,否则在内联函数后,JIT 会看到它不需要保留引用..跨度> 【参考方案3】:

这是一个想法:

C* gPhoenix= nullptr;

C::~C ()

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

现在,如果所涉及的对象(基类或成员)确实具有执行某些操作的析构函数,那么如果您 delete gPhoenix; 这会遇到问题,因此您将需要更精细的机制,具体取决于它真正想要完成的任务。但是你没有任何真正的目标,只是好奇的探索,所以指出这一点就足够了。

当调用析构函数的主体时,对象仍然是完美的。当您从析构函数中进行正常的成员函数调用时,它看起来非常重要和正常。

拥有该对象的内存将被回收,因此您不能原地不动。而离开身体后,其他的破坏会自动发生,无法干涉。但是,您可以在此之前复制对象。

【讨论】:

需要注意的是,如果 object 实际上是子类的实例,则任何子类的析构函数都已被调用。在这种情况下,事情可能会变得很棘手,这取决于子类所做的事情。充其量你会得到当前被破坏类的实例的有效副本,而只是丢失子类添加的任何内容。 @Deduplicator 析构函数调用顺序是从子类到超类,与构造函数调用顺序相反(很自然,当您考虑它时,子类的东西取决于它下面的有效超类的东西)。所以我认为我在上面写的正确。 @hyde 我总是交换超类和子类。可能是因为超类是一个子对象。 超类,子类:嗯,这两个术语在 C++ 中都没有使用,所以不用担心。使用 Standard 和 Stroustrup 之前的命名法:派生类(和大多数派生类)、基类。然后,当您在本地上下文中定义时,可以随意使用一般数学意义上的子类等术语。请注意,基数和派生数更难混淆!【参考方案4】:

正如already been pointed out 一样,GC.KeepAlive 不会那样做。

只要 .NET 运行,就可以使用 GC.ReRegisterForFinalize 从终结器中复活,如果您有 WeakReferenceGCHandle 跟踪恢复,您仍然可以获得对它的引用,或者只是给 @987654333 @到课外的东西。这样做将中止破坏。

这是在 .NET 2.0 no longer relevant 中检测垃圾收集的老技巧,但仍然有效(有点,垃圾收集现在可以是部分的,并与其他线程并行完成)。

应该强调的是,在 .NET 上,您使用的是 finalizer,它在销毁之前运行,并且可以防止它发生。因此,虽然在销毁后无法恢复对象(in any language)在技术上是正确的,但您可以接近您在 .NET 中描述的行为,除非改用GC.ReRegisterForFinalize


在 C++ 上,您已经获得了 correct answer。

【讨论】:

【参考方案5】:

这在任何语言中都是不可能的。

你的理解有点不对劲。 GC.KeepAlive 会将对象标记为垃圾收集器不可收集。这将防止垃圾收集策略破坏对象,如果对象用于垃圾收集器无法跟踪使用情况的非托管代码中,这将非常有用。这并不意味着对象在销毁后仍在内存中。

一旦对象开始销毁,代码将释放资源(内存、文件处理程序、网络连接)。顺序通常是从最深的派生类回到基类。如果中间有什么东西是为了防止破坏,则无法保证可以重新获取这些资源并且对象将处于不一致的状态。

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

【讨论】:

请注意我的回答,即 GC.KeepAlive() 仅在调用 KeepAlive() 之前保护对象不被垃圾收集,而不是之后。 你对GC.KeepAlive的理解是错误的。此外,需要固定对象才能在非托管代码中使用,此时它无论如何都不可收集。【参考方案6】:

如果有帮助,析构函数和内存分配是不同的。

析构函数只是一个函数。您可以显式调用它。如果它没有破坏性,那么再次调用它(例如,当对象超出范围或被删除时)不一定有问题,尽管它会很奇怪;标准中可能有一节处理这个问题。请参见下面的示例。 例如,一些 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.

【讨论】:

以上是关于从 C++ 中的析构函数中恢复对象?的主要内容,如果未能解决你的问题,请参考以下文章

c++与java 的析构函数

C++ 设置基类的析构函数为虚函数

C++ 中的析构函数(与 java 相比)

C ++中的析构函数调用顺序[重复]

C++虚析构函数

C++中的析构函数