提问



我对大多数面向对象的理论有深刻的理解,但令我困惑的一件事是虚拟析构函数。


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


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

最佳参考


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


class Base 
{
    // some virtual methods
};

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


在这里,你会注意到我没有声明Base的析构函数是virtual。现在,让我们看看下面的代码片段:


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


由于Base的析构函数不是virtualb是指向Derived对象的Base*delete b具有未定义的行为:



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




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


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


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


您可以在本文中从Herb Sutter了解有关虚拟性和虚拟基类析构函数的更多信息。[37]

其它参考1


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

其它参考2


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


#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


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

其它参考3


另请注意,在没有虚析构函数时删除基类指针将导致 未定义的行为 。我最近刚学到的东西:


如何在C ++中覆盖删除行为?


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

其它参考4


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

其它参考5


通过指向基类

的指针调用析构函数

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.
}

其它参考6


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

其它参考7


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


 #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




其它参考8


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


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


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

其它参考9


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

其它参考10


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


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


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


该示例还告诉您如何将字母转换为上限或下限


#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[40]

其它参考11


我认为讨论未定义行为或至少在没有虚拟析构函数的情况下通过基类(/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确实有vtable。因此,a或b指向ac或d对象实例的指针将被修复以考虑vtable。如果你通过这个或者b指针删除它会因为地址对堆的自由例程无效而崩溃。


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

其它参考12


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

其它参考13


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


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.


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

其它参考14


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


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