【问题标题】:Will Derived class type compatibility to base class cause a memory leak?派生类类型与基类的兼容性会导致内存泄漏吗?
【发布时间】:2011-06-04 11:56:21
【问题描述】:

我了解派生类与其基类的指针类型兼容。在给定的示例代码中,new bar 对象构造发生在调用foo::foo() 后跟bar::bar()。在各自的构造函数中,我将资源分配给类成员foo::int *a 和bar::int *b。

现在我将这样构造的对象初始化为基类类型。使用obj,我可以调用基类析构函数,但不能调用派生类析构函数。那么,在这种情况下如何释放派生类资源呢?这不是内存泄漏吗?

#include <iostream>
class foo
{
    int *a;
public:
    foo()
    {
        a = new int[5];
        std::cout << "\n foo constructor" << std::endl;
    }
    ~foo()
    {
        std::cout << "\n foo destructor" << std::endl;
        delete[] a;
    }
};

class bar : public foo
{
    int *b;
public:
    bar()
    {
        b = new int[5];
        std::cout << "\n bar constructor" << std::endl;
    }
    ~bar()
    {
        std::cout << "\n bar destructor" << std::endl;
        delete[] b;
    }
};

int main()
{
    foo *obj = new bar; // Derived class object is type compatible with base class

    delete obj; // Equivalent to obj->~foo();
    return 0;
}

谢谢。

【问题讨论】:

标签: c++ constructor destructor


【解决方案1】:

这就是“虚拟析构函数”的想法出现的地方。从技术上讲,如果通过基类类型的指针删除对象,则必须将该基类析构函数标记为虚拟,否则结果未定义。如果确实将析构函数标记为virtual,则其含义与其他虚函数不同。而不是“派生类覆盖此行为”,而是“通过基类指针删除此对象时,在调用基构造函数之前调用派生析构函数”。这是你想要的行为。

作为一般规则,您定义的任何计划子类化的类都应该有一个虚拟析构函数来防止此类问题。

【讨论】:

    【解决方案2】:

    如果您通过指向其基类之一的指针delete 派生类对象,则基类析构函数必须声明为virtual,否则行为未定义。

    如果~foo() 被声明为virtual,你就可以走了。将首先调用~bar(),然后调用~foo()。

    【讨论】:

    • @James McNellis - 通过保留析构函数 foo 病毒,我看到两个析构函数都被调用了。但是为什么是现在而不是早期的情况呢?谢谢。
    • @Mahesh:在您的原始代码中,~foo() 不是虚拟的,因此对析构函数的调用不是动态调度的(好吧,从技术上讲,行为是未定义的,但一个常见的结果是只有 @将调用 987654329@,而不是 ~bar())。如果您声明~foo() virtual,那么析构函数调用将被动态分派,您将获得正确、定义明确的行为。
    • 我知道,如果一个方法在基类中声明为虚拟,那么它在派生类中也是虚拟的。析构函数也一样吗?
    • @VJo:是的,析构函数也是如此。
    • @James :您能否指定标准中的引用,如果不使用 virtual 关键字,则行为未定义?
    【解决方案3】:

    这实际上是一种未定义的行为。

    来自标准文档。 5.3.5.3 删除,

    在第一种选择(删除对象)中,如果操作数的静态类型与其动态类型不同,静态类型 应该是操作数动态类型的基类,静态类型应该有一个虚拟析构函数,否则行为是 未定义。 ......

    【讨论】:

    • 上例中,obj不是动态类型吗?
    • @Mahesh:*obj 的静态类型是foo。 *obj 的动态类型为bar。实际上,静态类型是代码中命名的类型;动态类型是对象的实际类型。两者可以不同,如您的原始代码所示。
    • @James McNellis - 我正要问区别。你怎么做到的。谢谢。
    【解决方案4】:

    这样做,

    virtual ~foo()
    {
       //your code
    }
    

    这确保执行delete *pFoo 也会调用派生类析构函数(~bar())。调用顺序为~bar(),后跟~foo()。


    如果你也对~bar()做同样的事情也很好,也就是说,

    virtual ~bar()
    {
       //your code
    }
    

    虽然如果您不想进一步从 bar 派生并希望将 bar* 用于它的派生类,那么对于这种情况并没有太大必要。

    【讨论】:

    • 如果~foo()被声明为virtual,那么~bar()也是virtual,不管它是否用virtual关键字声明。
    • 要清楚,在~bar() 的定义之前显式写virtual 不会改变任何东西——它自动是虚拟的,因为~foo() 是虚拟的,并且如果派生另一个类baz来自bar,~baz() 也将自动变为虚拟。这是很好的文档。
    • @James McNellis - 如果我进一步从 bar 派生一个新类,同样的概念是否适用?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-05-10
    • 1970-01-01
    • 2011-01-07
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多