【问题标题】:Why base class destructor (virtual) is called when a derived class object is deleted?为什么在删除派生类对象时调用基类析构函数(虚拟)?
【发布时间】:2011-03-16 18:10:47
【问题描述】:

析构函数(当然还有构造函数)和其他成员函数之间的区别在于,如果常规成员函数在派生类中具有主体,则仅执行派生类中的版本。而在析构函数的情况下,派生版本和基类版本都会被执行?

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

提前致谢!

【问题讨论】:

标签: c++ inheritance destructor


【解决方案1】:

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

来自C++ spec

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

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

【讨论】:

  • “程序员必须选择哪个基类构造函数” - 只有在没有可访问的默认构造函数时才会出现这种情况。
  • 正确,让我澄清一下。
【解决方案2】:

因为这就是 dtor 的工作方式。当您创建一个对象时,会从基础开始调用 ctor,一直到最派生的。当您(正确地)销毁对象时,会发生相反的情况。使 dtor virtual 产生影响的时间是如果/当您通过指向基本类型的指针(或引用,尽管这是相当不寻常的)销毁对象时。在这种情况下,替代方案并不是只有派生的 dtor 被调用——相反,替代方案只是未定义的行为。这恰好采取了仅调用派生 dtor 的形式,但它也可能采用完全不同的形式。

【讨论】:

    【解决方案3】:

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

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

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

    【讨论】:

      【解决方案4】:

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

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

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

      您的基类析构函数将始终在派生实例被删除时自动调用,因为析构函数不带参数。

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

      【讨论】:

        【解决方案5】:

        标准说

        在执行析构函数的主体并销毁主体内分配的所有自动对象后, 类 X 的析构函数调用 X 的直接非变体成员的析构函数,X 直接的析构函数 基类,并且,如果 X 是最派生类 (12.6.2) 的类型,则其析构函数调用析构函数 X 的虚拟基类。所有的析构函数都被调用,就好像它们被一个限定名引用一样,也就是说, 忽略更多派生类中任何可能的虚拟重写析构函数。 基地和成员被摧毁 以它们的构造函数完成的相反顺序(见12.6.2)。中的 return 语句 (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).
         }
        

        【讨论】:

        • ill-formed 表示它没有编译(编辑,意思是说没有编译)。上面的代码会编译,只是漏了。
        • @Igor:我从来没有说过代码不会编译。我已经明确提到代码有内存泄漏。
        • 你说的ill-formed,意思是不是well-formed,从规范上看,意思是一个C++程序按照语法规则、可诊断语义规则和单一定义规则 - 即无法编译。
        • @Igor:感谢您的提醒。将格式错误更改为`泄漏内存。`
        【解决方案6】:

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

        【讨论】:

          【解决方案7】:

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

          构造函数

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

          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中的析构函数标记为virtual,它们仍然是@ 987654328@,它们仍然以相同的顺序被调用。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2015-02-24
            • 1970-01-01
            • 2012-12-20
            • 2015-07-18
            • 2021-08-22
            • 1970-01-01
            • 1970-01-01
            • 2022-07-01
            相关资源
            最近更新 更多