为什么垃圾回收器不会自动释放我的类成员?

3

当我在VS2008中构建以下C++/CLI代码时,会显示代码分析警告CA1001。

ref class A
{
public:
    A()   { m_hwnd = new HWND; }
    ~A()  { this->!A(); }
protected:
    !A()  { delete m_hwnd; }
    HWND* m_hwnd;
};

ref class B
{
public:
    B()   { m_a = gcnew A(); }
protected:
    A^    m_a;
};

警告:CA1001 : Microsoft.Design : 在类'B'上实现IDisposable,因为它创建了以下IDisposable类型的成员:'A'。

要解决此警告,我需要在B类中添加以下代码:

    ~B()  { delete m_a; }

但我不理解为什么。Class A通过其析构函数(和终结器)实现IDisposable。
因此,每当A被垃圾回收时,A的终结器或析构函数将被调用,释放其非托管资源。
为什么B必须添加一个析构函数来调用其A成员上的“delete”?
如果B没有显式调用“delete m_a”,那么GC只会调用A的析构函数吗?
编辑:如果您使用声明A成员的“语法糖”方法,如下所示,则似乎会自动工作:
ref class B
{
public:
    B()   { }
protected:
    A     m_a;
};

但这并非总是可行。

为什么垃圾回收器不能聪明地自动处理A^的托管引用指针,一旦没有其他人指向它?

1个回答

2

您应该使用堆栈语义来处理成员,并在包含类中添加析构函数。然后成员将被释放。 请参见http://msdn.microsoft.com/en-us/library/ms177197.aspx

ref class B
{
public:
    B()   {}
    ~B()  {}
protected:
    A    m_a;
};

该成员仍然是引用类型,并且仍然在堆上创建。

编辑:

.net中的Dispose最好不要使用,在C#中整个确定性行为都被打破,您必须非常严格地调用Dispose才能获得大多数C++开发人员期望的行为。

在C++/CLI中,堆栈语义使其更好。如果您无法使用它们,则需要显式调用Dispose,而在C++/CLI中,这由析构函数表示。

仅通过堆栈语义自动链接成员的Dispose调用,如果成员与C#一样是普通托管指针,那么您必须手动链接这些调用。

许多类可以持有相同的A^指针,没有办法知道哪个类应该调用析构函数。

您会收到警告,因为您实现了析构函数,这导致您的类实现了IDispose接口。这给了您一个机会以确定性方式清理。

仅依靠GC只能收集没有引用的对象并调用终结器。这远非确定性。请注意,依赖终结器进行清理应该仅作为安全网,因为它可能在很长时间内甚至根本不会被调用。

我建议尝试设计您的代码以允许上述模式。


谢谢,确实我可以使用语法糖方法 - 实际上如果我这样做,那么我根本不需要实现析构函数(好吧,它无论如何都会抑制警告)。但在某些/大多数情况下,您无法使用语法糖方法,只能存储托管引用指针。我想知道为什么GC需要显式调用删除托管引用指针以处理托管对象。 - demoncodemonkey
很多类可能持有相同的A^指针,没有办法知道哪一个应该调用析构函数。<<GC应该能够知道哪一个是最后一个,当最后一个被释放时,它应该调用析构函数。看来GC并不像它本应该那么厉害... - demoncodemonkey
@demoncodemonkey:「让垃圾回收器处理所有工作」的两个主要问题是:(1)在最后一个引用对象被消除之后,可能会经过任意长的时间,垃圾回收器才能到达包含它的内存区域,并且让对象在没有被清理的情况下持续那么长时间可能是不可接受的;(2)垃圾回收器可以知道哪些引用是可达的,但它无法知道哪些引用是“有用”的。例如,假设存在一个对象来计算表单引发其“Paint”事件的次数。每次引发“Paint”事件时... - supercat
该对象会增加一个计数器。表单需要保留对该对象的引用,以便可以发送“Paint”事件,但表单实际上并不关心该对象。如果没有任何关心计数器的东西保留对该对象的引用,那么该对象将停止提供任何有用的目的,但GC将无法知道这一点。如果在表单的生命周期内创建和放弃了许多这样的对象,则它们可能构成严重的内存泄漏。 - supercat

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