从 C++ 中的析构函数中恢复对象?
Revive object from destructor in C++?
免责声明:我知道这是糟糕的设计,我只是出于好奇问这个问题,以便更深入地了解析构函数在 C++ 中的工作原理。
在 C# 中,可以在 class (参见下面的编辑) 的析构函数中编写:GC.KeepAlive(this)
,这意味着对象即使在析构函数调用完成后,仍将存在于内存中。
C++ 的设计是否允许从类似于上述 C# 算法的析构函数中恢复对象?
Edit:正如下面的答案所指出的,GC.ReRegisterForFinalize()
比 GC.KeepAlive(this)
.
与问题的关系更密切
简短的回答是:没有。 C++ 不使用垃圾收集,如 Java 或 C#。当一个对象被销毁时,它会立即被销毁。永远消失了。加入了无形的合唱团。渴望峡湾等...
并用不同的词说几次,这样就不可能轻易地重新解释...
析构函数作为对象析构的一部分被调用。对象销毁包括调用析构函数和释放用于对象本身的内存。这是一个单一的过程,而不是两个独立的过程。虽然析构函数是运行,但对象仍然存在,供析构函数使用,但它存在是借来的时间。对象一析构returns就被气化已成定局。一旦调用了析构函数,对象就会被销毁,没有什么能改变它的命运。
理解这一点:调用析构函数的原因是:对象最初是在堆上分配 "new",现在是 "delete"d。 "delete" 表示 "delete",而不是 "delete maybe"。所以对象被删除了。或者,如果对象是在堆栈上分配的,则执行线程退出了作用域,因此作用域中声明的所有对象都将被销毁。从技术上讲,析构函数是由于对象被销毁而被调用的。因此,该对象正在被销毁。结束。
话虽如此,C++ 允许您为 类 实现自定义分配器。如果您愿意,可以编写自己的自定义内存分配和释放函数来实现您想要的任何功能。尽管这些从未用于堆栈分配的对象(即局部变量)。
您实际上曲解了 GC.KeepAlive
在 .NET 中的作用。它不能用在对象的析构函数中以防止该对象被破坏——实际上,GC.KeepAlive()
是空的并且没有实现。
请参阅 .NET 源代码 here.
它确保在 调用 GC.KeepAlive
之前,作为参数传递的对象不会被垃圾收集。作为参数传递给 KeepAlive 的对象可以在调用 GC.KeepAlive
后立即被垃圾回收。由于 KeepAlive
没有实际实现,因此发生这种情况纯粹是基于编译器必须维护对要作为参数传递给 KeepAlive
的对象的引用这一事实。也可以使用将对象作为参数的任何其他函数(未被编译器或运行时内联)。
这在任何语言中都是不可能的。
你的理解有点偏差。
GC.KeepAlive
会将对象标记为不可由垃圾收集器收集。这将防止垃圾收集策略破坏对象,如果对象用于垃圾收集器无法跟踪使用情况的非托管代码,这将非常有用。这并不意味着该对象在销毁后仍在内存中。
一旦对象开始销毁,代码将释放资源(内存、文件处理程序、网络连接)。顺序通常是从最深的派生class回到基class。如果中间的某些东西是为了防止破坏,则无法保证可以重新获取这些资源并且对象将处于不一致状态。
您更可能想要的是 std::shared_ptr
跟踪副本和引用,并且仅在不再需要时销毁该对象。
这是一个想法:
C* gPhoenix= nullptr;
C::~C ()
{
gPhoenix= new C (*this); // note: loses any further-derived class ("slice")
}
现在,如果 object 所涉及的(基础或成员)确实具有执行某些操作的析构函数,那么如果您 delete gPhoenix;
这会遇到问题,因此您将需要更精细的机制,具体取决于它真正想要完成的是什么。但是你没有任何真正的目标,只是好奇的探索,所以指出这一点就足够了。
当调用析构函数的body时,object仍然完美无缺。当您从析构函数中进行正常的成员函数调用时,它看起来非常重要和正常。
拥有 object 的内存将被回收,因此您不能停留在 in-place 周围。而在离开body后,其他破坏自动进行,无法干预。但是,您可以在此之前复制 object。
因为 , GC.KeepAlive
没有那样做。
只要 .NET 运行,就可以使用 GC.ReRegisterForFinalize
, you can still get a reference to it if you have a WeakReference
or GCHandle
跟踪恢复从终结器中复活,或者只是将 this
给 class 之外的东西。这样做会中止销毁。
这是在 .NET 2.0 中检测垃圾收集的老技巧 no longer relevant,但仍然有效(垃圾收集现在可以是部分的,并与其他线程并行完成)。
需要强调的是,在 .NET 上您使用的是 finalizer,它在销毁之前运行,并且可以防止销毁。因此,虽然从技术上讲您无法在销毁后恢复对象是正确的 - - 您可以接近您在 .NET 中描述的行为,但使用 GC.ReRegisterForFinalize
除外。
在 C++ 上,您已经获得 .
如果有帮助,析构函数和内存分配是不同的。
析构函数只是一个函数。您可以显式调用它。如果它没有做任何破坏性的事情,那么再次调用它(例如,当对象超出范围或被删除时)不一定有问题,尽管这会很奇怪;标准中可能有一个部分处理这个问题。请参见下面的示例。
例如,一些 STL 容器显式调用析构函数,因为它们分别管理对象生命周期和内存分配。
通常情况下,编译器会插入代码以在自动变量超出范围或堆分配对象被删除时调用析构函数。这种内存释放不能在析构函数内部被篡改。
您可以通过提供 new 运算符的额外实现,或使用现有的如 placement 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# 中,可以在 class (参见下面的编辑) 的析构函数中编写:GC.KeepAlive(this)
,这意味着对象即使在析构函数调用完成后,仍将存在于内存中。
C++ 的设计是否允许从类似于上述 C# 算法的析构函数中恢复对象?
Edit:正如下面的答案所指出的,GC.ReRegisterForFinalize()
比 GC.KeepAlive(this)
.
简短的回答是:没有。 C++ 不使用垃圾收集,如 Java 或 C#。当一个对象被销毁时,它会立即被销毁。永远消失了。加入了无形的合唱团。渴望峡湾等...
并用不同的词说几次,这样就不可能轻易地重新解释...
析构函数作为对象析构的一部分被调用。对象销毁包括调用析构函数和释放用于对象本身的内存。这是一个单一的过程,而不是两个独立的过程。虽然析构函数是运行,但对象仍然存在,供析构函数使用,但它存在是借来的时间。对象一析构returns就被气化已成定局。一旦调用了析构函数,对象就会被销毁,没有什么能改变它的命运。
理解这一点:调用析构函数的原因是:对象最初是在堆上分配 "new",现在是 "delete"d。 "delete" 表示 "delete",而不是 "delete maybe"。所以对象被删除了。或者,如果对象是在堆栈上分配的,则执行线程退出了作用域,因此作用域中声明的所有对象都将被销毁。从技术上讲,析构函数是由于对象被销毁而被调用的。因此,该对象正在被销毁。结束。
话虽如此,C++ 允许您为 类 实现自定义分配器。如果您愿意,可以编写自己的自定义内存分配和释放函数来实现您想要的任何功能。尽管这些从未用于堆栈分配的对象(即局部变量)。
您实际上曲解了 GC.KeepAlive
在 .NET 中的作用。它不能用在对象的析构函数中以防止该对象被破坏——实际上,GC.KeepAlive()
是空的并且没有实现。
请参阅 .NET 源代码 here.
它确保在 调用 GC.KeepAlive
之前,作为参数传递的对象不会被垃圾收集。作为参数传递给 KeepAlive 的对象可以在调用 GC.KeepAlive
后立即被垃圾回收。由于 KeepAlive
没有实际实现,因此发生这种情况纯粹是基于编译器必须维护对要作为参数传递给 KeepAlive
的对象的引用这一事实。也可以使用将对象作为参数的任何其他函数(未被编译器或运行时内联)。
这在任何语言中都是不可能的。
你的理解有点偏差。
GC.KeepAlive
会将对象标记为不可由垃圾收集器收集。这将防止垃圾收集策略破坏对象,如果对象用于垃圾收集器无法跟踪使用情况的非托管代码,这将非常有用。这并不意味着该对象在销毁后仍在内存中。
一旦对象开始销毁,代码将释放资源(内存、文件处理程序、网络连接)。顺序通常是从最深的派生class回到基class。如果中间的某些东西是为了防止破坏,则无法保证可以重新获取这些资源并且对象将处于不一致状态。
您更可能想要的是 std::shared_ptr
跟踪副本和引用,并且仅在不再需要时销毁该对象。
这是一个想法:
C* gPhoenix= nullptr;
C::~C ()
{
gPhoenix= new C (*this); // note: loses any further-derived class ("slice")
}
现在,如果 object 所涉及的(基础或成员)确实具有执行某些操作的析构函数,那么如果您 delete gPhoenix;
这会遇到问题,因此您将需要更精细的机制,具体取决于它真正想要完成的是什么。但是你没有任何真正的目标,只是好奇的探索,所以指出这一点就足够了。
当调用析构函数的body时,object仍然完美无缺。当您从析构函数中进行正常的成员函数调用时,它看起来非常重要和正常。
拥有 object 的内存将被回收,因此您不能停留在 in-place 周围。而在离开body后,其他破坏自动进行,无法干预。但是,您可以在此之前复制 object。
因为 GC.KeepAlive
没有那样做。
只要 .NET 运行,就可以使用 GC.ReRegisterForFinalize
, you can still get a reference to it if you have a WeakReference
or GCHandle
跟踪恢复从终结器中复活,或者只是将 this
给 class 之外的东西。这样做会中止销毁。
这是在 .NET 2.0 中检测垃圾收集的老技巧 no longer relevant,但仍然有效(垃圾收集现在可以是部分的,并与其他线程并行完成)。
需要强调的是,在 .NET 上您使用的是 finalizer,它在销毁之前运行,并且可以防止销毁。因此,虽然从技术上讲您无法在销毁后恢复对象是正确的 - GC.ReRegisterForFinalize
除外。
在 C++ 上,您已经获得
如果有帮助,析构函数和内存分配是不同的。
析构函数只是一个函数。您可以显式调用它。如果它没有做任何破坏性的事情,那么再次调用它(例如,当对象超出范围或被删除时)不一定有问题,尽管这会很奇怪;标准中可能有一个部分处理这个问题。请参见下面的示例。 例如,一些 STL 容器显式调用析构函数,因为它们分别管理对象生命周期和内存分配。
通常情况下,编译器会插入代码以在自动变量超出范围或堆分配对象被删除时调用析构函数。这种内存释放不能在析构函数内部被篡改。
您可以通过提供 new 运算符的额外实现,或使用现有的如 placement 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.