何时使用虚拟析构函数?


1485

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

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

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


6
看到这个:虚拟破坏者
Naveen

146
每个析构函数得到不管是什么所谓。 virtual确保它从顶部开始而不是从中间开始。
Mooing Duck


@MooingDuck的评论有些误导。
Euri Pinhollow

1
@FranklinYu您问的很好,因为现在我看不到该评论有任何问题(除了尝试在评论中给出答案)。
Euri Pinhollow

Answers:


1571

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

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的析构函数不是virtual并且bBase*指向Derived对象的指针,因此delete b具有未定义的行为

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

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

综上所述,在需要对基类virtual进行多态操作时,请始终使其成为析构函数。

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

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


173
这可以解释为什么我使用以前制造的工厂发生大量泄漏。现在一切都说得通了。谢谢
Lodle

8
嗯,这是一个糟糕的例子,因为没有数据成员。如果Base并且Derived具有所有自动存储变量怎么办?也就是说,在析构函数中没有“特殊”或附加的自定义代码要执行。那可以不用编写任何析构函数了吗?还是派生类仍然存在内存泄漏?
bobobobo 2012年


28
摘自Herb Sutter的文章:“准则4:基类析构函数应该是公共的和虚拟的,或者是受保护的和非虚拟的”。
圣代

3
同样在文章中-“如果您在没有虚拟析构函数的情况下进行多态删除,则会召唤出令人恐惧的“未定义行为”的幽灵,我个人宁愿在中等照明的小巷也不会遇到这种幽灵,非常感谢。大声笑
Bondolin '16

219

虚拟构造函数是不可能的,但虚拟析构函数是可能的。让我们尝试一下.......

#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

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


1
“不可能使用虚拟构造函数”意味着您不必自己编写虚拟构造函数。派生对象的构造必须遵循从派生到基础的构造链。因此,您无需为构造函数编写virtual关键字。谢谢
Tunvir Rahman Tusher 2013年

4
@Murkantilism,“无法完成虚拟构造函数”确实是正确的。构造函数不能标记为虚拟。
cmeub

1
@cmeub,但是有一个习惯用法可以实现虚拟构造函数的目标。参见parashift.com/c++-faq-lite/virtual-ctors.html
cape1232

@TunvirRahmanTusher您能否解释为什么调用基本析构函数?
rimalonfire

。@rimiro它自动通过C ++,你可以按照链接stackoverflow.com/questions/677620/...
Tunvir拉赫曼Tusher

195

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


14
+“如果一个类具有任何虚函数,则它应该具有一个虚析构函数,并且那些不是设计为基类或未设计为多态使用的类不应声明虚析构函数。”:在某些情况下,违反这条规则?如果不是这样,让编译器检查此条件并发出一个不满足的错误是否有意义?
乔治

@Giorgio我不知道该规则有任何例外。但是我不会将自己评价为C ++专家,因此您可能希望将此作为一个单独的问题发布。编译器警告(或静态分析工具的警告)对我来说很有意义。
比尔蜥蜴2012年

10
可以将类设计为不通过某种类型的指针删除,但仍然具有虚函数-典型示例是回调接口。人们不会通过回调接口指针删除其实现,因为这仅用于订阅,但是它确实具有虚函数。
dascandy

3
@dascandy没错-这或全部的许多在这里我们使用多态的行为,但不通过指针执行存储管理的其他情况-例如,保持自动或静态持续时间的对象,仅作为观察路线的指针。在任何此类情况下,都不需要/不需要实现虚拟析构函数。因为我们只是在这里引用人,所以我从上面更喜欢Sutter:“准则4:基类析构函数应该是公共的和虚拟的,或者是受保护的和非虚拟的。” 后者可确保任何人意外地尝试通过基本指针尝试删除,都显示了其方式的错误
underscore_d

1
@Giorgio实际上,可以使用一种技巧,避免对析构函数的虚拟调用:通过const引用将派生对象绑定到基类,例如const Base& = make_Derived();。在这种情况下,Derived将调用prvalue 的析构函数,即使它不是虚拟的也是如此,因此可以节省vtables / vpointers引入的开销。当然范围很有限。Andrei Alexandrescu在他的《现代C ++设计》一书中提到了这一点。
vsoftco '16

46

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

C ++中的覆盖删除应如何表现?

我已经使用C ++多年了,但仍然无法自拔。


我查看了您的问题,发现您已将基本析构函数声明为虚拟的。那么,“在没有虚拟析构函数时删除基类指针将导致未定义的行为”对于您的问题是否仍然有效?因为在该问题中,当您调用delete时,将首先检查派生类(由其新运算符创建)以查找兼容版本。由于它在那里找到一个,因此被称为。因此,您认为用“没有析构函数时删除基类指针会导致未定义的行为”这样的说法更好吗?
ubuntugod

那几乎是同一件事。默认构造函数不是虚拟的。
BigSandwich '16

40

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


13

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

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

是否有必要~Derived()在所有派生类中进行显式声明,即使只是~Derived() = default?还是语言所隐含的含义(使其可以安全省略)?
Ponkadoodle

@Wallacoloo不,仅在必要时声明它。例如放置protected部分,或使用来确保它是虚拟的override
Abyx

9

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


2
对同一问题的看法是不同的。如果我们从接口而不是基类与派生类的角度来考虑,那是很自然的结论:如果它是接口的一部分,那就使其虚拟化。如果不是,那就不要。
Dragan Ostojic 2012年

2
+1用于说明OO 接口和C ++ 纯虚拟类的相似性。关于析构函数,预计将被实现:这通常是不必要的。除非一个类正在管理诸如动态分配的原始内存之类的资源(例如,不通过智能指针),文件句柄或数据库句柄,否则在派生类中使用由编译器创建的默认析构函数就可以了。并请注意,如果virtual在基类中声明了析构函数(或任何函数),则virtual即使未声明它也自动在派生类中。
DavidRR

这错过了析构函数是 不一定是接口的一部分。可以轻松地编程具有多态功能但调用者无法管理/不允许删除的类。这样,虚拟析构函数就没有用了。当然,为了确保这一点,非虚拟的(可能是默认的)析构函数应该是非公共的。如果我不得不猜测,我会说这类类在项目内部更经常使用,但这并没有使它们在所有示例中都没有那么重要。
underscore_d

8

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

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

如果您的基类析构函数是虚拟的,则对象将按顺序被破坏(首先是派生的对象,然后是base)。如果您的基类析构函数不是虚拟的,则仅基类对象将被删除(因为指针是基类“ Base * myObj”的指针)。因此,派生对象将发生内存泄漏。


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


没有基本的虚拟析构函数,并且调用delete基本指针会导致未定义的行为。
James Adkison

@JamesAdkison为什么会导致不确定的行为?
rimalonfire

@rimiro 这就是标准所说的。我没有副本,但该链接会将您带到注释,其中有人引用标准中的位置。
James Adkison

@rimiro“因此,如果可以通过基类接口多态地执行删除操作,则它必须是虚拟的并且必须是虚拟的。确实,该语言要求这样做-如果您在没有虚拟析构函数的情况下进行多态删除,则会召唤出可怕的幽灵我个人不愿在一个光线充足的小巷里碰到“不确定的行为”,非常感谢。(gotw.ca/publications/mill18.htm)-赫伯·萨特
詹姆斯·阿迪克森

4

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


4

如果您使用 shared_ptr(仅shared_ptr,而不是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关键字可以使您免于沉重的痛苦。
MichalŠtein

3

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

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

请参阅以下带有虚拟功能的示例

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

#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


2

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


2

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

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& a{b}; a.foo();。不需要检查NULL-应该是nullptr-在delete输入之前-具有不正确的意图- delete nullptr;定义为无操作。如果有的话,您应该在调用之前进行检查->foo(),否则,如果new某种原因失败,则会发生未定义的行为。)
underscore_d

2
它是安全地调用delete一个NULL指针(即你不需要if (a != NULL)后卫)。
James Adkison

@SaileshD是的,我知道。那就是我在评论中
James Adkison

1

我认为,讨论通过基类(/ 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)有vtable并且您通过基类(/ struct)删除对象,则可能导致崩溃的原因指针。在这种情况下,您传递给堆的空闲例程的地址无效,从而导致崩溃。

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

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


0

一个基本的定义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;
}

-1

任何公开继承的类(无论是否具有多态性)都应具有虚拟析构函数。换句话说,如果基类指针可以指向它,则其基类应具有虚拟析构函数。

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


我会说这仅是必要的,“如果它可以由基类指针指向”,并且可以公开删除。但是我想养成添加虚拟dtor的习惯并没有什么坏处,以防以后可能需要它们。
underscore_d
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.