【问题标题】:When to use virtual destructors?何时使用虚拟析构函数?
【发布时间】:2022-01-01 12:33:56
【问题描述】:

我对大多数 OOP 理论都有深刻的理解,但让我很困惑的一件事是虚拟析构函数。

我认为析构函数总是被调用,不管是什么,对于链中的每个对象。

您打算什么时候将它们虚拟化?为什么?

【问题讨论】:

  • 看这个:Virtual Destructor
  • 每个析构函数 down 无论如何都会被调用。 virtual 确保它从顶部而不是中间开始。
  • 我也对@MooingDuck 的回答感到困惑。如果您使用子类(下)和超类(上)的概念,它不应该是up而不是down吗?
  • @Nibor:是的,如果你使用这个概念。与我交谈的大约一半的人将超类视为“高于”,一半将超类视为“低于”,因此两者都是相互冲突的标准,这让一切都变得混乱。我认为“高于”的超类更为常见,但这不是我被教导的方式:(

标签: c++ polymorphism shared-ptr virtual-destructor


【解决方案1】:

只要你的类是多态的,就将析构函数设为虚拟。

【讨论】:

    【解决方案2】:

    当您可能通过指向基类的指针删除派生类的实例时,虚拟析构函数很有用:

    class Base 
    {
        // some virtual methods
    };
    
    class Derived : public Base
    {
        ~Derived()
        {
            // Do some important cleanup
        }
    };
    

    在这里,您会注意到我没有将 Base 的析构函数声明为 virtual。现在,我们来看看下面的sn-p:

    Base *b = new Derived();
    // use b
    delete b; // Here's the problem!
    

    由于 Base 的析构函数不是 virtual 并且 b 是指向 Derived 对象的 Base*,所以 delete b 具有 undefined behaviour

    [在delete b],如果是静态类型 要删除的对象与其动态类型不同,静态 type 应该是对象的动态类型的基类 删除并且静态类型应该有一个虚拟析构函数或 行为未定义

    在大多数实现中,对析构函数的调用将像任何非虚拟代码一样被解析,这意味着将调用基类的析构函数而不是派生类的析构函数,从而导致资源泄漏。

    总而言之,当基类要进行多态操作时,请始终将基类的析构函数设为virtual

    如果要防止通过基类指针删除实例,可以将基类析构函数设为protected和nonvirtual;这样做,编译器不会让你在基类指针上调用delete

    您可以在this article from Herb Sutter 中了解有关虚拟性和虚拟基类析构函数的更多信息。

    【讨论】:

    • 这可以解释为什么我使用我以前制造的工厂会出现大量泄漏。现在一切都说得通了。谢谢
    • 好吧,这是一个不好的例子,因为没有数据成员。如果BaseDerivedall 自动存储变量怎么办?即在析构函数中没有要执行的“特殊”或额外的自定义代码。那么完全不写任何析构函数可以吗?还是派生类还是会有内存泄漏?
    • 来自 Herb Sutter 的文章:“准则 #4:基类析构函数应该是公共的和虚拟的,或者受保护的和非虚拟的。”
    • 同样来自文章 - '如果你在没有虚拟析构函数的情况下进行多态删除,你会召唤出可怕的“未定义行为”幽灵,我个人宁愿在光线充足的小巷里也不会遇到这种幽灵, 非常感谢您。'大声笑
    【解决方案3】:

    在多态基类中声明虚拟的析构函数。这是 Scott Meyers 的Effective C++ 中的第 7 项。 Meyers 继续总结说,如果一个类有任何虚函数,它应该有一个虚析构函数,并且那些不是被设计为基类或不是被设计为多态使用的类不应该不 声明虚拟析构函数。

    【讨论】:

    • +"如果一个类有任何虚函数,它应该有一个虚析构函数,并且那些不是被设计为基类或不是被设计为多态使用的类不应该声明虚析构函数。": 是在哪些情况下打破这条规则是有意义的?如果不是,让编译器检查此条件并发出错误是否不满足是否有意义?
    • @Giorgio 我不知道该规则有任何例外。但我不会将自己评为 C++ 专家,因此您可能希望将此作为单独的问题发布。编译器警告(或来自静态分析工具的警告)对我来说很有意义。
    • 类可以设计成不通过某个类型的指针被删除,但仍然有虚函数——典型的例子是回调接口。一个人不会通过回调接口指针删除他的实现,因为那只是为了订阅,但它确实有虚函数。
    • @dascandy 完全正确 - 我们使用多态行为但不通过指针执行存储管理的所有其他情况或所有 许多 情况 - 例如维护自动或静态持续时间对象,指针仅用作观察路线。在任何此类情况下都不需要/目的实现虚拟析构函数。因为我们只是在这里引用人们的话,所以我更喜欢上面的 Sutter:“准则 #4:基类析构函数应该是公共的和虚拟的,或者受保护的和非虚拟的。”后者确保任何不小心尝试通过基指针删除的人都会看到他们的方式错误
    • @Giorgio 实际上有一个技巧可以使用并避免对析构函数的虚拟调用:通过 const 引用将派生对象绑定到基类,例如 const Base& = make_Derived();。在这种情况下,Derived prvalue 的析构函数将被调用,即使它不是虚拟的,因此可以节省 vtables/vpointers 引入的开销。当然范围是相当有限的。 Andrei Alexandrescu 在他的书Modern C++ Design 中提到了这一点。
    【解决方案4】:

    还要注意,在没有虚拟析构函数的情况下删除基类指针将导致未定义的行为。我最近学到的东西:

    How should overriding delete in C++ behave?

    我已经使用 C++ 多年,但我仍然设法上吊。

    【讨论】:

    • 我查看了您的那个问题,发现您已将基本析构函数声明为虚拟的。那么“在没有虚拟析构函数时删除基类指针会导致未定义的行为”对于您的那个问题是否仍然有效?因为,在那个问题中,当您调用 delete 时,首先检查派生类(由其 new 运算符创建)的兼容版本。因为它在那里找到了一个,所以它被称为。那么,您不认为“在没有析构函数的情况下删除基类指针会导致未定义的行为”更好吗?
    • 这几乎是一回事。默认构造函数不是虚拟的。
    • @BigSandwich“上吊”?你的意思是内存泄漏?
    【解决方案5】:

    我喜欢思考接口和接口的实现。在 C++ 中,接口是纯虚拟类。析构函数是接口的一部分,并有望实现。因此析构函数应该是纯虚拟的。构造函数呢?构造函数实际上不是接口的一部分,因为对象总是被显式实例化。

    【讨论】:

    • 同一个问题的不同视角。如果我们考虑接口而不是基类与派生类,那么很自然的结论:如果它是接口的一部分,而不是使其成为虚拟。如果不是不要。
    • +1 用于说明 interface 的 OO 概念与 C++ 纯虚拟类 的相似性。关于析构函数应该被实现:这通常是不必要的。除非一个类正在管理诸如原始动态分配的内存(例如,不是通过智能指针)、文件句柄或数据库句柄之类的资源,否则在派生类中使用编译器创建的默认析构函数是可以的。请注意,如果析构函数(或任何函数)在基类中声明为 virtual,则它在派生类中自动声明为 virtual,即使未声明也是如此。
    • 这忽略了析构函数不一定是接口的一部分这一关键细节。人们可以轻松地编写具有多态函数但调用者不管理/不允许删除的类。那么虚拟析构函数就没有目的了。当然,为了确保这一点,非虚拟的——可能是默认的——析构函数应该是非公开的。如果我不得不猜测,我会说这样的类更常在项目内部使用,但这并没有降低它们作为示例/细微差别的相关性。
    【解决方案6】:

    虚拟构造函数是不可能的,但虚拟析构函数是可能的。 让我们进行实验......

    #include <iostream>
    
    using namespace std;
    
    class Base
    {
    public:
        Base(){
            cout << "Base Constructor Called\n";
        }
        ~Base(){
            cout << "Base Destructor called\n";
        }
    };
    
    class Derived1: public Base
    {
    public:
        Derived1(){
            cout << "Derived constructor called\n";
        }
        ~Derived1(){
            cout << "Derived destructor called\n";
        }
    };
    
    int main()
    {
        Base *b = new Derived1();
        delete b;
    }
    

    以上代码输出如下:

    Base Constructor Called
    Derived constructor called
    Base Destructor called
    

    派生对象的构造遵循构造规则,但是当我们删除“b”指针(基指针)时,我们发现只调用了基析构函数。但这绝不能发生。为了做适当的事情,我们必须将基本析构函数设为虚拟。 现在让我们看看下面会发生什么:

    #include <iostream>
    
    using namespace std;
    
    class Base
    { 
    public:
        Base(){
            cout << "Base Constructor Called\n";
        }
        virtual ~Base(){
            cout << "Base Destructor called\n";
        }
    };
    
    class Derived1: public Base
    {
    public:
        Derived1(){
            cout << "Derived constructor called\n";
        }
        ~Derived1(){
            cout << "Derived destructor called\n";
        }
    };
    
    int main()
    {
        Base *b = new Derived1();
        delete b;
    }
    

    输出变化如下:

    Base Constructor Called
    Derived Constructor called
    Derived destructor called
    Base destructor called
    

    所以基指针的销毁(它在派生对象上进行分配!)遵循销毁规则,即首先是派生,然后是基。 另一方面,没有什么能比得上虚拟构造函数。

    【讨论】:

    • "虚拟构造函数是不可能的"意味着你不需要自己编写虚拟构造函数。派生对象的构造必须遵循从派生到基础的构造链。所以你不需要为你的构造函数编写 virtual 关键字。谢谢
    • @Murkantilism,“无法完成虚拟构造函数”确实是真的。构造函数不能被标记为虚拟的。
    • @cmeub,但是有一个习惯用法可以从虚拟构造函数中实现您想要的。见parashift.com/c++-faq-lite/virtual-ctors.html
    • @TunvirRahmanTusher 你能解释一下为什么调用 Base Destructor 吗??
    • @rimiro 它是由 c++ 自动生成的。你可以点击链接stackoverflow.com/questions/677620/…
    【解决方案7】:

    什么是虚拟析构函数或如何使用虚拟析构函数

    类析构函数是一个与前面的类同名的函数 ~ 它将重新分配由该类分配的内存。为什么我们需要一个虚拟析构函数

    请参阅以下带有一些虚函数的示例

    该示例还告诉您如何将字母转换为大写或小写

    #include "stdafx.h"
    #include<iostream>
    using namespace std;
    // program to convert the lower to upper orlower
    class convertch
    {
    public:
      //void convertch(){};
      virtual char* convertChar() = 0;
      ~convertch(){};
    };
    
    class MakeLower :public convertch
    {
    public:
      MakeLower(char *passLetter)
      {
        tolower = true;
        Letter = new char[30];
        strcpy(Letter, passLetter);
      }
    
      virtual ~MakeLower()
      {
        cout<< "called ~MakeLower()"<<"\n";
        delete[] Letter;
      }
    
      char* convertChar()
      {
        size_t len = strlen(Letter);
        for(int i= 0;i<len;i++)
          Letter[i] = Letter[i] + 32;
        return Letter;
      }
    
    private:
      char *Letter;
      bool tolower;
    };
    
    class MakeUpper : public convertch
    {
    public:
      MakeUpper(char *passLetter)
      {
        Letter = new char[30];
        toupper = true;
        strcpy(Letter, passLetter);
      }
    
      char* convertChar()
      {   
        size_t len = strlen(Letter);
        for(int i= 0;i<len;i++)
          Letter[i] = Letter[i] - 32;
        return Letter;
      }
    
      virtual ~MakeUpper()
      {
        cout<< "called ~MakeUpper()"<<"\n";
        delete Letter;
      }
    
    private:
      char *Letter;
      bool toupper;
    };
    
    
    int _tmain(int argc, _TCHAR* argv[])
    {
      convertch *makeupper = new MakeUpper("hai"); 
      cout<< "Eneterd : hai = " <<makeupper->convertChar()<<" ";     
      delete makeupper;
      convertch *makelower = new MakeLower("HAI");;
      cout<<"Eneterd : HAI = " <<makelower->convertChar()<<" "; 
      delete makelower;
      return 0;
    }
    

    从上面的示例可以看出,MakeUpper 和 MakeLower 类的析构函数都没有被调用。

    使用虚拟析构函数查看下一个示例

    #include "stdafx.h"
    #include<iostream>
    
    using namespace std;
    // program to convert the lower to upper orlower
    class convertch
    {
    public:
    //void convertch(){};
    virtual char* convertChar() = 0;
    virtual ~convertch(){}; // defined the virtual destructor
    
    };
    class MakeLower :public convertch
    {
    public:
    MakeLower(char *passLetter)
    {
    tolower = true;
    Letter = new char[30];
    strcpy(Letter, passLetter);
    }
    virtual ~MakeLower()
    {
    cout<< "called ~MakeLower()"<<"\n";
          delete[] Letter;
    }
    char* convertChar()
    {
    size_t len = strlen(Letter);
    for(int i= 0;i<len;i++)
    {
    Letter[i] = Letter[i] + 32;
    
    }
    
    return Letter;
    }
    
    private:
    char *Letter;
    bool tolower;
    
    };
    class MakeUpper : public convertch
    {
    public:
    MakeUpper(char *passLetter)
    {
    Letter = new char[30];
    toupper = true;
    strcpy(Letter, passLetter);
    }
    char* convertChar()
    {
    
    size_t len = strlen(Letter);
    for(int i= 0;i<len;i++)
    {
    Letter[i] = Letter[i] - 32;
    }
    return Letter;
    }
    virtual ~MakeUpper()
    {
          cout<< "called ~MakeUpper()"<<"\n";
    delete Letter;
    }
    private:
    char *Letter;
    bool toupper;
    };
    
    
    int _tmain(int argc, _TCHAR* argv[])
    {
    
    convertch *makeupper = new MakeUpper("hai");
    
    cout<< "Eneterd : hai = " <<makeupper->convertChar()<<" \n";
    
    delete makeupper;
    convertch *makelower = new MakeLower("HAI");;
    cout<<"Eneterd : HAI = " <<makelower->convertChar()<<"\n ";
    
    
    delete makelower;
    return 0;
    }
    

    虚拟析构函数将显式调用类的最派生的运行时析构函数,以便能够以适当的方式清除对象。

    或访问链接

    https://web.archive.org/web/20130822173509/http://www.programminggallery.com/article_details.php?article_id=138

    【讨论】:

      【解决方案8】:

      我认为这个问题的核心是关于虚拟方法和多态性,而不是具体的析构函数。这是一个更清晰的例子:

      class A
      {
      public:
          A() {}
          virtual void foo()
          {
              cout << "This is A." << endl;
          }
      };
      
      class B : public A
      {
      public:
          B() {}
          void foo()
          {
              cout << "This is B." << endl;
          }
      };
      
      int main(int argc, char* argv[])
      {
          A *a = new B();
          a->foo();
          if(a != NULL)
          delete a;
          return 0;
      }
      

      将打印出来:

      This is B.
      

      如果没有virtual,它将打印出来:

      This is A.
      

      现在你应该明白什么时候使用虚拟析构函数了。

      【讨论】:

      • 不,这只是重读了虚函数的全部基础,完全忽略了析构函数何时/为什么应该是一个的细微差别——这并不直观,因此 OP 提出了这个问题。 (另外,为什么这里有不必要的动态分配?只需执行B b{}; A&amp; a{b}; a.foo();。检查NULL - 应该是nullptr - 在deleteing 之前 - 缩进不正确 - 不需要:delete nullptr; 被定义为无操作。如果有的话,您应该在调用 -&gt;foo() 之前检查这一点,否则如果 new 以某种方式失败,可能会出现未定义的行为。)
      • NULL 指针上调用delete 是安全的(即,您不需要if (a != NULL) 保护)。
      • @SaileshD 是的,我知道。这就是我在my comment 中所说的
      【解决方案9】:

      当你需要从基类调用派生类析构函数时。您需要在基类中声明虚拟基类析构函数。

      【讨论】:

        【解决方案10】:

        任何公开继承的类,无论是否多态,都应该有一个虚拟析构函数。换句话说,如果它可以被基类指针指向,那么它的基类应该有一个虚析构函数。

        如果是虚拟的,则调用派生类析构函数,然后调用基类析构函数。如果不是虚拟的,则仅调用基类析构函数。

        【讨论】:

        • 我想说这只是“如果它可以被基类指针指向”并且可以被公开删除的必要条件。但我想养成添加虚拟 dtor 的习惯以防以后可能需要它们并没有什么坏处。
        【解决方案11】:

        当您希望不同的析构函数在通过基类指针删除对象时遵循正确的顺序时,析构函数的虚拟关键字是必要的。 例如:

        Base *myObj = new Derived();
        // Some code which is using myObj object
        myObj->fun();
        //Now delete the object
        delete myObj ; 
        

        如果您的基类析构函数是虚拟的,那么对象将按顺序被销毁(首先是派生对象,然后是基类)。如果您的基类析构函数不是虚拟的,那么只有基类对象将被删除(因为指针属于基类“Base *myObj”)。所以派生对象会有内存泄漏。

        【讨论】:

          【解决方案12】:

          通过指向基类的指针调用析构函数

          struct Base {
            virtual void f() {}
            virtual ~Base() {}
          };
          
          struct Derived : Base {
            void f() override {}
            ~Derived() override {}
          };
          
          Base* base = new Derived;
          base->f(); // calls Derived::f
          base->~Base(); // calls Derived::~Derived
          

          虚拟析构函数调用与任何其他虚拟函数调用没有什么不同。

          对于base-&gt;f(),调用将被分派到Derived::f(),对于base-&gt;~Base()也是一样——它的覆盖函数——Derived::~Derived()将被调用。

          当间接调用析构函数时也会发生同样的情况,例如delete base;delete 语句将调用 base-&gt;~Base(),该语句将被发送到 Derived::~Derived()

          带有非虚析构函数的抽象类

          如果您不打算通过指向其基类的指针来删除对象 - 那么就不需要虚拟析构函数。只需将其设为protected,以免被意外调用:

          // library.hpp
          
          struct Base {
            virtual void f() = 0;
          
          protected:
            ~Base() = default;
          };
          
          void CallsF(Base& base);
          // CallsF is not going to own "base" (i.e. call "delete &base;").
          // It will only call Base::f() so it doesn't need to access Base::~Base.
          
          //-------------------
          // application.cpp
          
          struct Derived : Base {
            void f() override { ... }
          };
          
          int main() {
            Derived derived;
            CallsF(derived);
            // No need for virtual destructor here as well.
          }
          

          【讨论】:

          • 是否有必要在所有派生类中显式声明~Derived(),即使只是~Derived() = default?还是语言暗示了这一点(使其可以安全省略)?
          • @Wallacoloo 不,仅在必要时声明它。例如。放入protected 部分,或使用override 确保它是虚拟的。
          • @Abyx 打电话给base-&gt;~Base() 合适吗?按照你所说的,Base::~Base() 不会被调用,那么就会有内存泄漏。我说的对吗?
          【解决方案13】:

          简单来说, 虚拟析构函数是在删除指向派生类对象的基类指针时,按正确的顺序销毁资源。

           #include<iostream>
           using namespace std;
           class B{
              public:
                 B(){
                    cout<<"B()\n";
                 }
                 virtual ~B(){ 
                    cout<<"~B()\n";
                 }
           };
           class D: public B{
              public:
                 D(){
                    cout<<"D()\n";
                 }
                 ~D(){
                    cout<<"~D()\n";
                 }
           };
           int main(){
              B *b = new D();
              delete b;
              return 0;
           }
          
          OUTPUT:
          B()
          D()
          ~D()
          ~B()
          
          ==============
          If you don't give ~B()  as virtual. then output would be 
          B()
          D()
          ~B()
          where destruction of ~D() is not done which leads to leak
          

          【讨论】:

          • 没有基本的虚拟析构函数并且在基本指针上调用delete 会导致未定义的行为。
          • @JamesAdkison 为什么会导致未定义的行为??
          • @rimiro It's what the standard says。我没有副本,但该链接会将您带到有人引用标准中的位置的评论。
          • @rimiro "因此,如果删除可以通过基类接口以多态方式执行,那么它必须是虚拟的并且必须是虚拟的。确实,语言需要它 - 如果您在没有虚拟的情况下多态删除破坏者,你召唤了可怕的“未定义行为”幽灵,我个人宁愿在光线充足的小巷里也不会遇到这种幽灵,非常感谢。 (gotw.ca/publications/mill18.htm) -- 赫伯萨特
          【解决方案14】:

          虚拟基类析构函数是“最佳实践”——您应该始终使用它们来避免(难以检测)内存泄漏。使用它们,您可以确保类的继承链中的所有析构函数都被调用(以正确的顺序)。使用虚拟析构函数从基类继承会使继承类的析构函数也自动成为虚拟的(因此您不必在继承类析构函数声明中重新键入“virtual”)。

          【讨论】:

            【解决方案15】:

            我认为讨论“未定义”行为是有益的,或者至少是在通过没有虚拟析构函数或更准确地说没有 vtable 的基类 (/struct) 删除时可能发生的“崩溃”未定义行为。下面的代码列出了一些简单的结构(类也是如此)。

            #include <iostream>
            using namespace std;
            
            struct a
            {
                ~a() {}
            
                unsigned long long i;
            };
            
            struct b : a
            {
                ~b() {}
            
                unsigned long long j;
            };
            
            struct c : b
            {
                ~c() {}
            
                virtual void m3() {}
            
                unsigned long long k;
            };
            
            struct d : c
            {
                ~d() {}
            
                virtual void m4() {}
            
                unsigned long long l;
            };
            
            int main()
            {
                cout << "sizeof(a): " << sizeof(a) << endl;
                cout << "sizeof(b): " << sizeof(b) << endl;
                cout << "sizeof(c): " << sizeof(c) << endl;
                cout << "sizeof(d): " << sizeof(d) << endl;
            
                // No issue.
            
                a* a1 = new a();
                cout << "a1: " << a1 << endl;
                delete a1;
            
                // No issue.
            
                b* b1 = new b();
                cout << "b1: " << b1 << endl;
                cout << "(a*) b1: " << (a*) b1 << endl;
                delete b1;
            
                // No issue.
            
                c* c1 = new c();
                cout << "c1: " << c1 << endl;
                cout << "(b*) c1: " << (b*) c1 << endl;
                cout << "(a*) c1: " << (a*) c1 << endl;
                delete c1;
            
                // No issue.
            
                d* d1 = new d();
                cout << "d1: " << d1 << endl;
                cout << "(c*) d1: " << (c*) d1 << endl;
                cout << "(b*) d1: " << (b*) d1 << endl;
                cout << "(a*) d1: " << (a*) d1 << endl;
                delete d1;
            
                // Doesn't crash, but may not produce the results you want.
            
                c1 = (c*) new d();
                delete c1;
            
                // Crashes due to passing an invalid address to the method which
                // frees the memory.
            
                d1 = new d();
                b1 = (b*) d1;
                cout << "d1: " << d1 << endl;
                cout << "b1: " << b1 << endl;
                delete b1;  
            
            /*
            
                // This is similar to what's happening above in the "crash" case.
            
                char* buf = new char[32];
                cout << "buf: " << (void*) buf << endl;
                buf += 8;
                cout << "buf after adding 8: " << (void*) buf << endl;
                delete buf;
            */
            }
            

            我并不是建议您是否需要虚拟析构函数,尽管我认为一般来说拥有它们是一个好习惯。我只是指出如果你的基类(/struct)没有vtable而你的派生类(/struct)有并且你通过基类(/struct)删除一个对象,你最终可能会崩溃的原因指针。在这种情况下,您传递给堆的空闲例程的地址是无效的,因此是崩溃的原因。

            如果您运行上面的代码,您会清楚地看到问题发生的时间。当基类(/struct)的this指针与派生类(/struct)的this指针不同时,你就会遇到这个问题。在上面的示例中,结构 a 和 b 没有 vtable。结构 c 和 d 确实有 vtables。因此,指向 c 或 d 对象实例的 a 或 b 指针将被修复以解释 vtable。如果你将这个 a 或 b 指针传递给 delete,它会因为地址对堆的空闲例程无效而崩溃。

            如果您打算从基类指针中删除具有 vtable 的派生实例,则需要确保基类具有 vtable。一种方法是添加一个虚拟析构函数,无论如何您都可能希望正确清理资源。

            【讨论】:

              【解决方案16】:

              如果你使用shared_ptr(只有shared_ptr,而不是unique_ptr),你不必让基类析构函数为virtual:

              #include <iostream>
              #include <memory>
              
              using namespace std;
              
              class Base
              {
              public:
                  Base(){
                      cout << "Base Constructor Called\n";
                  }
                  ~Base(){ // not virtual
                      cout << "Base Destructor called\n";
                  }
              };
              
              class Derived: public Base
              {
              public:
                  Derived(){
                      cout << "Derived constructor called\n";
                  }
                  ~Derived(){
                      cout << "Derived destructor called\n";
                  }
              };
              
              int main()
              {
                  shared_ptr<Base> b(new Derived());
              }
              

              输出:

              Base Constructor Called
              Derived constructor called
              Derived destructor called
              Base Destructor called
              

              【讨论】:

              • 虽然这是可能的,但我会劝阻任何人不要使用它。虚拟析构函数的开销是微乎其微的,这只会使混乱成为可能,特别是对于不知道这一点的经验不足的程序员。那个小小的virtual 关键字可以让你免于很多痛苦。
              • 出于好奇 - 为什么在 shared_ptr 的情况下会调用 Base 析构函数,但在 unique_ptr 的情况下却没有?
              【解决方案17】:

              关于virtual 的基本定义是它确定类的成员函数是否可以在其派生类中被覆盖。

              一个类的D-tor基本上是在作用域结束时调用的,但是有一个问题,比如我们在Heap上定义一个实例(动态分配),我们应该手动删除它。

              指令一执行,就会调用基类析构函数,但不会调用派生的析构函数。

              一个实际的例子是,在控制领域,您必须操纵效应器、执行器。

              在作用域结束时,如果不调用其中一个动力元素(Actuator)的析构函数,将会产生致命的后果。

              #include <iostream>
              
              class Mother{
              
              public:
              
                  Mother(){
              
                        std::cout<<"Mother Ctor"<<std::endl;
                  }
              
                  virtual~Mother(){
              
                      std::cout<<"Mother D-tor"<<std::endl;
                  }
              
              
              };
              
              class Child: public Mother{
              
                  public:
              
                  Child(){
              
                      std::cout<<"Child C-tor"<<std::endl;
                  }
              
                  ~Child(){
              
                       std::cout<<"Child D-tor"<<std::endl;
                  }
              };
              
              int main()
              {
              
                  Mother *c = new Child();
                  delete c;
              
                  return 0;
              }
              

              【讨论】:

                【解决方案18】:

                除非您有充分的理由不这样做,否则将所有析构函数设为虚拟。

                否则会发生这样的恶事:

                假设您有一个包含 Apple 和 Orange 对象的 Fruit 指针数组。

                当你从 Fruit 对象的集合中删除时, ~Apple() 和 ~Orange() 调用失败,除非 ~Fruit() 是虚拟的。

                正确的示例:

                #include <iostream>
                using namespace std;
                struct Fruit { // good
                  virtual ~Fruit() { cout << "peel or core should have been tossed" << endl; } 
                };
                struct Apple:  Fruit { virtual ~Apple()  {cout << "toss core" << endl; } };
                struct Orange: Fruit { virtual ~Orange() {cout << "toss peel" << endl; } };
                
                int main() { 
                  Fruit *basket[]={ new Apple(), new Orange() };
                  for (auto fruit: basket) delete fruit;
                };
                

                良好的输出

                toss core
                peel or core should have been tossed
                toss peel
                peel or core should have been tossed
                

                示例做错了:

                #include <iostream>
                using namespace std;
                struct Fruit { // bad 
                  ~Fruit() { cout << "peel or core should have been tossed" << endl; } 
                };
                struct Apple:  Fruit { virtual ~Apple()  {cout << "toss core" << endl; } };
                struct Orange: Fruit { virtual ~Orange() {cout << "toss peel" << endl; } };
                
                int main() { 
                  Fruit *basket[]={ new Apple(), new Orange() };
                  for (auto fruit: basket) delete fruit;
                };
                
                

                输出错误

                peel or core should have been tossed
                peel or core should have been tossed
                

                (注意:为了简洁起见,我使用了struct,通常使用class并指定public)

                【讨论】:

                  【解决方案19】:

                  我认为这里的大多数答案都没有抓住重点,除了接受的答案,这是一件好事。不过,让我再补充一点,对这个问题有不同的看法:如果你想多态地删除这个类的实例,你需要一个虚拟析构函数。

                  这种回避了这个问题,所以让我详细说明一下:正如许多人指出的那样,如果您调用 delete base_ptr 并且析构函数不是虚拟的,则会出现不良行为。但是,有几个假设需要明确:

                  • 如果您的类不是基类,您希望不要编写这样的代码。在这种情况下,我不是指手动内存管理,它本身就是不好的,而是从这个类公开派生的。不应继承未设计为基类的类,例如std::string。 C++ 允许你在脚下射击自己。不过这是你的错,而不是基类没有虚拟析构函数。
                  • 如果析构函数不可访问(受保护或私有),则此代码将无法编译,因此不会发生不希望的行为。拥有一个受保护的析构函数是很有用的,特别是对于 mixin,而且(在较小程度上)对于接口也是如此。除非您实际使用它们,否则您不想招致虚函数的开销。相反,使析构函数受保护可以防止不良行为,但不会限制您。
                  • 如果您实际上编写了一个应该派生自的类,则通常无论如何都会有虚函数。作为它们的用户,您通常只会通过指向基类的指针来使用它们。当这种用途包括处置它们时,它也需要是多态的。在这种情况下,您应该将析构函数设为虚拟。

                  有关该主题的类似不同观点,另请阅读When should you not use virtual destructors?

                  【讨论】:

                    猜你喜欢
                    • 2011-05-20
                    • 2017-03-16
                    • 2013-07-06
                    相关资源
                    最近更新 更多