Ker*_* SB 14 c++ garbage-collection c++-cli
我正在使用C++/CLI,使用MSDN文档和ECMA标准以及Visual C++ Express 2010.令我震惊的是以下与C++的不同之处:
对于ref类,必须编写终结器和析构函数,以便可以多次执行它们以及尚未完全构造的对象.
我编造了一个小例子:
#include <iostream>
ref struct Foo
{
Foo() { std::wcout << L"Foo()\n"; }
~Foo() { std::wcout << L"~Foo()\n"; this->!Foo(); }
!Foo() { std::wcout << L"!Foo()\n"; }
};
int main()
{
Foo ^ r;
{
Foo x;
r = %x;
} // #1
delete r; // #2
}
Run Code Online (Sandbox Code Playgroud)
在块的末尾#1
,自动变量x
死亡,并且析构函数被调用(它反过来调用终结器,就像通常的习惯用法一样).这一切都很好.但后来我通过引用再次删除了该对象r
!输出是这样的:
Foo()
~Foo()
!Foo()
~Foo()
!Foo()
Run Code Online (Sandbox Code Playgroud)
问题:
delete r
在线呼叫是不确定的行为,还是完全可以接受#2
?
如果我们删除行#2
,那么r
对于一个(在C++意义上)不再存在的对象的跟踪句柄是否重要?这是一个"晃来晃去的手柄"吗?它的引用计数是否会导致尝试双重删除?
我知道没有实际的双删除,因为输出变为:
Foo()
~Foo()
!Foo()
Run Code Online (Sandbox Code Playgroud)
但是,我不确定这是一个幸福的事故还是保证是明确的行为.
在其他情况下,可以不止一次调用托管对象的析构函数?
可以x.~Foo();
在之前或之后立即插入r = %x;
吗?
换句话说,管理对象"永远活着"并且可以反复调用它们的析构函数和终结函数吗?
为了回应@Hans对非平凡类的需求,您也可以考虑这个版本(使用析构函数和终结符来符合多次调用的要求):
ref struct Foo
{
Foo()
: p(new int[10])
, a(gcnew cli::array<int>(10))
{
std::wcout << L"Foo()\n";
}
~Foo()
{
delete a;
a = nullptr;
std::wcout << L"~Foo()\n";
this->!Foo();
}
!Foo()
{
delete [] p;
p = nullptr;
std::wcout << L"!Foo()\n";
}
private:
int * p;
cli::array<int> ^ a;
};
Run Code Online (Sandbox Code Playgroud)
Han*_*ant 16
我将尝试解决您按顺序提出的问题:
对于ref类,必须编写终结器和析构函数,以便可以多次执行它们以及尚未完全构造的对象.
析构函数~Foo()
只是自动生成两个方法,IDisposable :: Dispose()方法的实现以及实现一次性模式的受保护的Foo :: Dispose(bool)方法.这些是普通方法,因此可以多次调用.在C++/CLI中允许直接调用终结器,this->!Foo()
并且通常就像你一样.垃圾收集器只调用一次终结器,它会在内部跟踪是否完成.鉴于允许直接调用终结器并且允许多次调用Dispose(),因此可以多次运行终结器代码.这是特定于C++/CLI,其他托管语言不允许.您可以轻松地阻止它,nullptr检查通常可以完成工作.
在第2行调用delete r是未定义的行为,还是完全可以接受?
它不是UB,完全可以接受.该delete
操作员只需调用了IDisposable :: Dispose()方法,从而运行您的析构函数.你在里面做什么,通常调用非托管类的析构函数,可能会调用UB.
如果我们删除第2行,那么r仍然是跟踪句柄是否重要
否.调用析构函数是完全可选的,没有强制执行它的好方法.没有什么问题,终结者最终将永远运行.在给定的示例中,当CLR在关闭之前最后一次运行终结器线程时将发生这种情况.唯一的副作用是程序运行"繁重",持续时间超过必要的资源.
在其他情况下,可以不止一次调用托管对象的析构函数?
这很常见,一个过分热心的C#程序员可能不止一次调用你的Dispose()方法.提供Close和Dispose方法的类在框架中非常常见.有些模式几乎是不可避免的,另一个类承担对象的所有权.标准示例是这个C#代码:
using (var fs = new FileStream(...))
using (var sw = new StreamWriter(fs)) {
// Write file...
}
Run Code Online (Sandbox Code Playgroud)
StreamWriter对象将获取其基本流的所有权,并在最后一个大括号中调用其Dispose()方法.FileStream对象上的using语句第二次调用Dispose().编写此代码以便不会发生这种情况仍然提供异常保证太困难了.指定可以多次调用Dispose()来解决问题.
是否可以插入x .~Foo(); 紧接在r =%x之前或之后;?
没关系.结果不太可能令人愉快,NullReferenceException将是最可能的结果.这是你应该测试的东西,引发一个ObjectDisposedException,给程序员一个更好的诊断.所有标准.NET框架类都这样做.
换句话说,管理对象"永远活着"
不,垃圾收集器声明对象死了,并在它找不到对该对象的任何引用时收集它.这是一种故障安全的内存管理方式,没有办法意外引用已删除的对象.因为这样做需要参考,GC将始终看到.常见的内存管理问题(如循环引用)也不是问题.
代码段
删除a
对象是不必要的,没有任何效果.您只删除实现IDisposable的对象,但数组不会这样做.通用规则是.NET类在管理内存以外的资源时仅实现IDisposable.或者,如果它有一个类类型的字段本身实现IDisposable.
在这种情况下,是否应该实现析构函数还值得怀疑.您的示例类正在保持一个相当适度的非托管资源.通过实现析构函数,您可以将负担强加给客户端代码以使用它.它在很大程度上取决于类的用法,对于客户端程序员来说这样做有多容易,如果对象被期望长时间存在,超出方法体,那么绝对不是这样,因此using语句不可用.您可以让垃圾收集器了解它无法跟踪的内存消耗,调用GC :: AddMemoryPressure().这也解决了客户端程序员根本不使用Dispose()的情况,因为它太难了.