我正在使用C ++ / CLI,使用MSDN文档和ECMA standard以及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
}
在#1
的块结束时,自动变量x
消失,并且析构函数被调用(它反过来调用终结器,就像通常的习惯用法一样)。这一切都很好。但后来我通过引用r
再次删除了该对象!输出是这样的:
Foo()
~Foo()
!Foo()
~Foo()
!Foo()
问题:
在delete r
行上致电#2
是不明确的行为,还是完全可以接受?
如果我们删除行#2
,那么r
仍然是(在C ++意义上)不再存在的对象的跟踪句柄是否重要?这是一个“晃来晃去的手柄”吗?它的引用计数是否会导致尝试双重删除?
我知道没有实际的双重删除,因为输出变为:
Foo()
~Foo()
!Foo()
但是,我不确定这是一件幸福的事故还是保证是明确的行为。
在哪些其他情况下,可以多次调用托管对象的析构函数?
在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;
};
答案 0 :(得分: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...
}
StreamWriter对象将获取其基本流的所有权,并在最后一个大括号中调用其Dispose()方法。 FileStream对象上的 using 语句第二次调用Dispose()。编写此代码以便不会发生这种情况仍然提供异常保证太困难了。指定可以多次调用Dispose()来解决问题。
是否可以插入x .~Foo();紧接在r =%x之前或之后;?
没关系。结果不太可能令人愉快,NullReferenceException将是最可能的结果。这是你应该测试的东西,引发一个ObjectDisposedException,给程序员一个更好的诊断。所有标准.NET框架类都这样做。
换句话说,管理对象“永远活着”
不,垃圾收集器声明对象死了,并在它找不到对该对象的任何引用时收集它。这是一种故障安全的内存管理方式,没有办法意外引用已删除的对象。因为这样做需要参考,GC将始终看到。常见的内存管理问题(如循环引用)也不是问题。
代码段
删除a
对象是不必要的,不起作用。您只删除实现IDisposable的对象,但数组不会这样做。通用规则是.NET类在管理内存以外的资源时仅实现IDisposable。或者,如果它有一个类类型的字段本身实现IDisposable。
在这种情况下,是否应该实现析构函数还值得怀疑。您的示例类正在保持一个相当适度的非托管资源。通过实现析构函数,您可以将负担强加给客户端代码以使用它。它在很大程度上取决于类的用法,对于客户端程序员来说这样做是多么容易,如果对象预计会长时间存在,超出方法体,那么使用语句不可用。您可以让垃圾收集器了解它无法跟踪的内存消耗,调用GC :: AddMemoryPressure()。这也解决了客户端程序员根本不使用Dispose()的情况,因为它太难了。
答案 1 :(得分:1)
标准C ++指南仍然适用:
在自动变量或已经清理过的变量上调用delete
仍然是一个坏主意。
它是指向已处置对象的跟踪指针。取消引用这是一个坏主意。使用垃圾收集时,只要存在任何非弱引用就会保留内存,因此您无法意外访问错误的对象,但仍然无法以任何有用的方式使用此处置的对象,因为它的不变量可能是不再持有。
当您的代码以非常糟糕的方式编写时,只能在托管对象上发生多次破坏,而这种风格在标准C ++中是UB(参见上面的1和下面的4)。
在自动变量上显式调用析构函数,然后不为其自动销毁调用查找创建新的析构函数,这仍然是一个坏主意。
通常,您认为将对象生存期视为与内存分配不同(就像标准C ++一样)。垃圾收集用于管理释放 - 所以内存仍然存在 - 但对象已经死了。与标准C ++不同,您不能将该内存重用于原始字节存储,因为.NET运行时的某些部分可能认为元数据仍然有效。
垃圾收集器和“堆栈语义”(自动变量语法)都不使用引用计数。
(丑陋的细节:处理对象不会破坏.NET运行时自身关于该对象的不变量,因此您甚至可以将其用作线程监视器。但这只是一个丑陋难以理解的设计,所以请不要。)