我对大多数OOP理论都有很好的理解,但最让我困惑的是虚拟析构函数。

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

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


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


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

class Base 
{
    // some virtual methods
};

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

在这里,您会注意到我没有将Base的析构函数声明为虚拟。现在,让我们看一下以下片段:

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

由于Base的析构函数不是虚拟的,而b是指向派生对象的Base*,因此删除b具有未定义的行为:

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

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

总之,当基类的析构函数要以多态方式操作时,请始终将其设为虚拟。

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

在本文中,您可以从HerbSutter了解更多关于虚拟性和虚拟基类析构函数的信息。


在多态基类中声明虚拟的析构函数。这是Scott Meyers的有效C++中的第7项。Meyers接着总结道,如果一个类有任何虚拟函数,那么它应该有一个虚拟析构函数,而不是设计为基类或不是设计为多态使用的类不应该声明虚拟析构器。


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

C++中重写删除应该如何操作?

我已经使用C++多年了,但我还是设法自杀了。


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


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

#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

因此,基指针的销毁(对派生对象进行分配!)遵循销毁规则,即首先是派生指针,然后是基指针。另一方面,没有什么像虚拟构造函数。


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

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

请参见以下示例中的一些虚拟函数

该示例还说明了如何将字母转换为大写或小写

#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


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

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.

如果没有虚拟,它将打印出:

This is A.

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


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


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

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


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

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

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


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

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()。

具有非虚拟析构函数的抽象类

若您不打算通过指向其基类的指针删除对象,那个么就不需要使用虚拟析构函数。只需保护它,使其不会被意外调用:

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

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

 #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


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


我认为讨论“未定义”行为,或者至少讨论在通过没有虚拟析构函数的基类(/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,而派生类(/struck)有vtable,并且通过基类(/ststruct)指针删除对象,那么可能会导致崩溃的原因。在这种情况下,传递给堆的空闲例程的地址是无效的,因此是崩溃的原因。

如果运行上述代码,您将清楚地看到问题发生的时间。当基类(/struct)的this指针与派生类(/struct)的this指示器不同时,您将遇到此问题。在上面的示例中,结构a和b没有vtables。结构c和d确实有vtables。因此,指向c或d对象实例的a或b指针将被修复以说明vtable。如果传递此a或b指针进行删除,则会由于地址对堆的空闲例程无效而崩溃。

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


如果使用shared_ptr(仅shared_ptl,而不是unique_ptr),则不必将基类析构函数设为虚拟:

#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的一个基本定义是它确定类的成员函数是否可以在其派生类中被覆盖。

类的D-tor基本上在作用域的末尾被调用,但存在一个问题,例如,当我们在堆(动态分配)上定义一个实例时,我们应该手动删除它。

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

一个实际的例子是,在控制场中,你必须操纵效应器和致动器。

在范围结束时,如果没有调用其中一个动力元件(执行器)的析构函数,将产生致命的后果。

#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;
}

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

否则会发生这样的邪恶:

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

从Fruit对象集合中删除时,除非~Fruit()是虚拟的,否则无法调用~Apple()和~Orange()。

正确完成示例:

#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)


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

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

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

对于这个主题的一个类似的不同观点,也可以阅读“什么时候不应该使用虚拟析构函数?”?


我建议这样做:如果类或结构不是最终的,那么应该为其定义虚拟析构函数。

我知道这看起来像是一种过度警惕的过度杀戮,成为一种经验法则。但是,这是确保从类派生的人在使用基指针删除时不会使用UB的唯一方法。

Scott Meyers在下面引用的有效C++中的建议很好,但不足以确定。

如果一个类有任何虚函数,它应该有一个虚函数析构函数,并且类是否设计为基类设计用于多态性的不应声明虚拟析构函数。

例如,在下面的程序中,基类B没有任何虚拟函数,因此根据Meyer的说法,您不需要编写虚拟析构函数。然而,如果您没有以下UB:

#include <iostream>

struct A
{
    ~A()
    {
        std::cout << "A::~A()" << std::endl;
    }
};

struct B
{
};

struct C : public B
{
    A a;
};

int main(int argc, char *argv[])
{
    B *b = new C;
    delete b; // UB, and won't print "A::~A()"
    return 0;
}