首页 文章

何时使用虚拟析构函数?

提问于
浏览
1283

我对大多数OO理论有了深刻的理解,但让我困惑的一件事是虚拟析构函数 .

我认为无论什么以及链中的每个对象,析构函数总是会被调用 .

你什么时候打算让它们成为虚拟的?为什么?

15 回答

  • 1382

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

    #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
    

    因此,基指针的破坏(在派生对象上进行分配!)遵循破坏规则,即首先导出然后是基数 . 另一方面,对于构造函数,没有像虚构造函数那样的东西 .

  • 175

    在多态基类中声明析构函数是虚拟的 . 这是Scott Meyers'Effective C++中的第7项 . Meyers接着总结说,如果一个类有任何虚函数,它应该有一个虚析构函数,那些不是基类或不能用于多态的类不应该声明虚析构函数 .

  • 8

    另请注意,在没有虚析构函数时删除基类指针将导致 undefined behavior . 我刚刚学到的东西:

    How should overriding delete in C++ behave?

    我已经使用C多年了,我仍然设法挂起自己 .

  • 0

    当您的类具有多态性时,使析构函数成为虚拟的 .

  • 40

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

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

    在这里,您'll notice that I didn' t声明Base的析构函数为 virtual . 现在,我们来看看以下代码段:

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

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

    [在删除b]中,如果要删除的对象的静态类型与其动态类型不同,则静态类型应为要删除的对象的动态类型的基类,静态类型应具有虚拟类型析构函数或行为未定义 .

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

    总而言之,当它们意味着被多态地操纵时,总是使基类的析构函数 virtual .

    如果要防止通过基类指针删除实例,可以使基类析构函数受保护且非虚拟;通过这样做,编译器将不允许您在基类指针上调用 delete .

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

  • 171

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

    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->f() ,调用将被调度到 Derived::f() ,对于 base->~Base() 它是相同的 - 它的重写函数 - 将调用 Derived::~Derived() .

    当间接调用析构函数时也会发生同样的情况,例如: delete base; . delete 语句将调用 base->~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.
    }
    
  • 11

    我喜欢考虑接口的接口和实现 . 在C语言界面是纯虚拟类 . 析构函数是界面的一部分,有望实现 . 因此析构函数应该是纯虚拟的 . 构造函数怎么样?构造函数实际上不是接口的一部分,因为对象始终是显式实例化的 .

  • 5

    简单来说,当您删除指向派生类对象的基类指针时,Virtual析构函数将以正确的顺序销毁资源 .

    #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
    

  • 6

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

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

    如果派生类析构函数是虚拟的,那么对象将按顺序被驱逐(首先是派生对象然后是基础) . 如果派生类析构函数不是虚拟的,那么只会删除基类对象(因为指针是基类“Base * myObj”) . 因此派生对象会有内存泄漏 .

  • 1

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

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

    请参阅以下示例以及一些虚函数

    该示例还告诉您如何将字母转换为更高或更低

    #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

  • 1

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

  • 2

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

    #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指针不同时,你将遇到这个问题 . 在上面的示例中,struct a和b没有vtable . 结构c和d确实有vtables . 因此,将修复指向c或d对象实例的a或b指针以考虑vtable . 如果你将这个或b指针传递给删除它会因为地址对堆的自由例程无效而崩溃 .

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

  • -1

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

    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.
    

    现在您应该了解何时使用虚拟析构函数 .

  • 32

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

  • 2

    任何公开继承的类,多态的或不具有的,都应该有一个虚拟的析构函数 . 换句话说,如果它可以由基类指针指向,则其基类应该具有虚拟析构函数 .

    如果是virtual,则派生类析构函数被调用,然后是基类构造函数 . 如果不是虚拟的,则只调用基类析构函数 .

相关问题