何时使用虚拟析构函数?

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

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

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

845464 次浏览

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

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

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

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

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

由于Base的析构函数不是virtualbBase*指向Derived对象,delete b未定义行为

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

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

总而言之,当基类的析构函数要被多态操作时,总是使它们virtual

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

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

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

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

覆盖删除在C++应该如何表现?

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

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

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

#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 CalledDerived constructor calledBase 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 CalledDerived Constructor calledDerived destructor calledBase destructor called

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

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

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

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

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

#include "stdafx.h"#include<iostream>using namespace std;// program to convert the lower to upper orlowerclass 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;}

从上面的示例中,您可以看到MakeTop和MakeLow类的析构函数都没有被调用。

使用虚拟析构函数查看下一个样本

#include "stdafx.h"#include<iostream>
using namespace std;// program to convert the lower to upper orlowerclass 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.

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

This is A.

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

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

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

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

当通过基类指针删除对象时,如果希望不同的析构函数遵循适当的顺序,则需要使用用于析构函数的 Virtual 关键字。 例如:

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

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

 #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,而你的派生类(/struct)有,并且你通过基类(/struct)指针删除了一个对象,那么你最终可能会崩溃。在这种情况下,传递给堆的空闲例程的地址无效,因此导致崩溃。

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

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

如果使用 shared_ptr(只有 share _ ptr,而不是 only _ 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的一个基本定义是,它确定类的成员函数是否可以在其派生类中重写。

类的 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 对象的 FruitPointsarray。

在从 Fruit 对象集合中删除时, ~ Apple ()和 ~ Orange ()不能被调用,除非 ~ Water ()是虚拟的。

举个正确的例子:

#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 + + 可以让你搬起石头砸自己的脚。但这是您的错误,而不是基类没有虚析构函数的错误。
  • 如果析构函数无法访问(受保护的或私有的) ,则此代码将无法编译,因此不希望发生的行为将不会发生。有一个受保护的析构函数是有用的,特别是对混合函数和(在较小程度上)接口。除非您实际使用了虚函数,否则不希望产生虚函数的开销。相反,让析构函数受到保护可以防止不必要的行为,但不会限制您的行为。
  • 如果您实际编写了一个应该从中派生的类,那么无论如何通常都会有虚函数。作为它们的用户,通常只能通过指向基类的指针使用它们。当这种使用包括处理它们时,它也需要是多态的。这就是应该使析构函数成为虚函数的情况。

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

我建议这样: 如果类或结构不是 final,则应为其定义虚析构函数。

我知道这看起来像是过度警惕的过度杀戮,以至于变成了经验法则。但是,这是唯一的方法,以确保从您的类派生的人不会有 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;
}