为什么在删除派生类对象时调用基类析构函数(虚拟)?

时间:2010-07-16 03:18:35

标签: c++ inheritance destructor

析构函数(当然也包括构造函数)与其他成员函数之间的区别在于,如果常规成员函数在派生类中具有正文,则只会执行Derived类中的版本。在析构函数的情况下,派生以及基类版本都会被执行吗?

很高兴知道在析构函数(可能是虚拟的)和&的情况下究竟发生了什么。构造函数,即使删除了最派生的类对象,也会为它们的所有基类调用它们。

提前致谢!

7 个答案:

答案 0 :(得分:16)

标准说

  

执行析构函数体并破坏体内分配的任何自动对象后,   X类的析构函数调用X的直接非变体成员的析构函数, X的直接析构函数   基类和,如果X是派生类最多的类(12.6.2),它的析构函数调用析构函数   X的虚拟基类。所有析构函数都被调用,好像它们被引用了一个合格的名称,即   忽略更多派生类中任何可能的虚拟覆盖析构函数。 基地和成员被摧毁   按照完成构造函数的相反顺序(见12.6.2)。 a中的返回声明(6.6.3)   析构函数可能不会直接返回调用者;在将控制转移给调用者之前,析构函数   为会员和基地打电话。数组元素的析构函数以相反的顺序调用   他们的建筑(见12.6)。

同样,根据RAII资源需要与合适对象的生命周期联系起来,并且必须调用各个类的析构函数来释放资源。

例如,以下代码泄漏了内存。

 struct Base
 {
       int *p;
        Base():p(new int){}
       ~Base(){ delete p; } //has to be virtual
 };

 struct Derived :Base
 {
       int *d;
       Derived():Base(),d(new int){}
       ~Derived(){delete d;}
 };

 int main()
 {
     Base *base=new Derived();
     //do something

     delete base;   //Oops!! ~Base() gets called(=>Memory Leak).
 }

答案 1 :(得分:13)

构造函数和析构函数与其他常规方法不同。

构造

  • 不能虚拟
  • 在派生类中,您要么显式调用基类的构造函数
  • 或者,如果你没有调用基类构造函数编译器将插入调用。它将调用不带参数的基础构造函数。如果不存在这样的构造函数,则会出现编译器错误。

struct A {};
struct B : A { B() : A() {} };

// but this works as well because compiler inserts call to A():
struct B : A { B() {} };

// however this does not compile:
struct A { A(int x) {} };
struct B : A { B() {} };

// you need:
struct B : A { B() : A(4) {} };

<强>析构

  • 当您通过指针或引用在派生类上调用析构函数时,基类具有虚析构函数,将首先调用派生最多的析构函数,然后以相反的构造顺序调用其余派生类。这是为了确保所有内存都已正确清理。如果最后派生的类被调用,那么它将无法工作,因为到那时基类不会存在于内存中,你会得到段错误。

struct C
{
    virtual ~C() { cout << __FUNCTION__ << endl; }
};

struct D : C
{
    virtual ~D() { cout << __FUNCTION__ << endl; }
};

struct E : D
{
    virtual ~E() { cout << __FUNCTION__ << endl; }
};

int main()
{
    C * o = new E();
    delete o;
}

输出:

~E
~D
~C

如果基类中的方法标记为virtual,则所有继承的方法都是虚拟的,即使您没有将DE中的析构函数标记为{{ 1}}它们仍然是virtual,它们仍然会以相同的顺序被调用。

答案 2 :(得分:11)

这是设计的。必须调用基类上的析构函数才能释放其资源。经验法则是派生类应该只清理自己的资源并让基类自行清理。

来自C++ spec

  

执行后的身体   析构者并摧毁任何人   在。中分配的自动对象   body,类X调用的析构函数   X的直接析构函数   成员,X的析构者   直接基类,如果X是   派生类最多的类型   (12.6.2),它的析构函数调用了   X的虚拟基础的析构函数   类。所有析构函数都称为   如果他们被引用了   限定名称,即忽略任何名称   可能的虚拟覆盖   更多派生类中的析构函数。   基地和成员被摧毁   完成的逆序   他们的构造函数(见   12.6.2)。

另外,因为只有一个析构函数,所以类必须调用哪个析构函数没有歧义。构造函数不是这种情况,如果没有可访问的默认构造函数,程序员必须选择应该调用哪个基类构造函数。

答案 3 :(得分:2)

因为这就是dtor的工作方式。当你创建一个对象时,从基数开始调用ctors,并一直到最大的派生。当您(正确地)销毁对象时,会发生相反的情况。使dtor虚拟产生差异的时间是,是否/当你通过指针(或引用,虽然这是相当不寻常的)对基类型销毁对象时。在这种情况下,替代方案并不是真正只有派生的dtor被调用 - 相反,替代方案只是未定义的行为。这种情况恰好采用仅调用派生的dtor的形式,但它也可能采用完全不同的形式。

答案 4 :(得分:2)

正如Igor所说,必须为基类调用构造函数。考虑如果不会被调用会发生什么:

struct A {
    std::string s;
    virtual ~A() {}
};

struct B : A {};

如果在删除A实例时未调用B的析构函数,则永远不会清除A

答案 5 :(得分:2)

基类析构函数可能负责清理由基类构造函数分配的资源。

如果你的基类有一个默认的构造函数(一个不带参数或者它的所有参数都有默认值),那么在构造派生实例时会自动调用构造函数。

如果您的基类有一个需要参数的构造函数,则必须在派生类构造函数的初始化列表中手动调用它。

由于析构函数不接受参数,因此在删除派生实例时将始终自动调用基类析构函数。

如果您正在使用多态并且您的派生实例由基类指针指向,则只有在基本析构函数为虚拟时才会调用派生的类析构函数。

答案 6 :(得分:0)

当任何对象被销毁时,析构函数将针对所有子对象运行。这包括通过包含重用和通过继承重用。