关于C++:为什么没有虚拟方法的基类需要虚拟析构函数?


why doesnt base class without virtual methods need virtual destructor?

本问题已经有最佳答案,请猛点这里访问。

我一直在想,为什么只有使用虚拟方法的基类才需要虚拟描述器?看看这段代码(阅读注释):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Base{
private:
    int x;
public:
    Base():x(0){}
    ~Base(){
        cout<<"Base dtor"<<endl;
    }
};

class Derived : public Base{
    int y;
public:
    Derived():y(0){}
    ~Derived(){
        cout<<"Derived dtor"<<endl;
    }
};

int main(){
    Derived *pd = new Derived;
    Base *pb = pd;
    delete pb; // this destroys only the base part, doesn't it?
               // so why doesnt the derived part leak?
    return 0;
}

我用valgrind运行它,看到输出是"base dtor",没有发生内存泄漏。那么,如果只调用了基类dtor,那么派生类部分为什么不泄漏呢?


"只有具有虚拟方法的基类才需要虚拟析构函数"这个问题的前提是错误的。使用虚拟析构函数与类中具有(其他)虚拟方法无关。更正确的准则是,对需要通过指向基类的指针(或引用)销毁的基类使用虚拟析构函数。

正如其他人指出的,您的示例不会泄漏,因为您没有派生的特定分配,所以派生的析构函数是一个no-op。一旦您开始在Derived构造函数中分配,您将得到一个真正的泄漏。


你问:

why doesnt the derived class part leak?

当你打电话

1
delete pb;

运行时知道为对象分配了多少内存,并将其释放。首先,delete调用对象的析构函数,然后将内存释放到对象的地址。堆管理器知道这是多少空间。

在这种情况下,会删除pb指向的对象,因此删除Base::xDerived::y的空间。

如果Derived::~Derived()负责重新分配内存,您将遇到内存泄漏。


不管是否涉及虚拟方法。派生类是否可以分配资源很重要。如果他们这样做了,您将出现内存泄漏。

如果Derived使用new,不管方法是否是虚拟的,最终都会出现泄漏。

同样,正如oli在注释中所说,只删除"对象的一部分"会导致ub,因此当您怀疑可能需要通过指向基类的指针调用派生对象的析构函数时,将析构函数声明为virtual是一个很好的实践。

另外,除非使用rtti,否则您基本上不知道指向Base的指针是指向Base实例还是指向Derived。这就是为什么声明析构函数在很大程度上是一种"进入"方法。


why doesn't the derived class part leak?因为在Derived构造函数中,您没有使用new分配内存,所以请尝试下面的代码来查看内存泄漏。它是您给定代码的修改代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <iostream>
using namespace std;
class Base{
private:
    int x;
public:
    Base():x(0){}
    ~Base(){
        cout<<"Base dtor"<<endl;
    }
};

class Derived : public Base{
    int y;
    int *a;
    public:
    //Derived():y(0){}
    Derived()
    {
        a = new int[10];
        y = 10;
        cout<<"Derived ctor";
    }

    ~Derived(){
        cout<<"Derived dtor"<<endl;
        delete a;
    }
};


int main(){
    Derived *pd = new Derived;
    Base *pb = pd;
    delete pb; // this destroys only the base part, doesn't it?
               // so why doesnt the derived part leak?
    return 0;
}

Valgrind总结:

1
2
3
4
5
6
==21686== LEAK SUMMARY:
==21686==    definitely lost: 40 bytes in 1 blocks
==21686==    indirectly lost: 0 bytes in 0 blocks
==21686==      possibly lost: 0 bytes in 0 blocks
==21686==    still reachable: 0 bytes in 0 blocks
==21686==         suppressed: 0 bytes in 0 blocks

目前有两个问题:

  • 为什么给定示例中的派生类不会导致内存泄漏?
  • 为什么没有虚拟方法的基类不需要虚拟析构函数?
  • 对于问题1,答案是您不会在派生类的析构函数中释放任何内存。如果这样做,那么给定示例中的派生类将导致内存泄漏。

    对于问题2,答案是,如果您的基类没有声明任何虚拟方法,那么使用它作为指向要开始的派生类实例的指针实际上没有多大意义。

    更新:

    您似乎误解了"内存泄漏"这一术语,它只能应用于动态分配的内存:

    • 静态分配的变量(在堆栈或数据节中)在其声明范围的执行结束时自动释放。

    • 动态分配的变量(在堆中)必须"手动"解除分配,以防止内存泄漏。