为什么要为C ++中的抽象类声明虚拟析构函数?


165

我知道在C ++中为基类声明虚拟析构函数是一个好习惯,但是virtual即使对于充当接口的抽象类,声明析构函数也总是很重要吗?请提供一些原因和示例。

Answers:


196

对于界面而言,它甚至更为重要。该类的任何用户都可能持有指向该接口的指针,而不是指向具体实现的指针。当他们删除它时,如果析构函数不是虚拟的,他们将调用接口的析构函数(如果未指定,则调用编译器提供的默认值),而不是派生类的析构函数。即时内存泄漏。

例如

class Interface
{
   virtual void doSomething() = 0;
};

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

void myFunc(void)
{
   Interface* p = new Derived();
   // The behaviour of the next line is undefined. It probably 
   // calls Interface::~Interface, not Derived::~Derived
   delete p; 
}

4
delete p调用未定义的行为。不能保证致电Interface::~Interface
Mankarse 2012年

@Mankarse:您能解释一下导致它不确定的原因吗?如果Derived没有实现自己的析构函数,那它仍然是未定义的行为吗?
Ponkadoodle 2012年

14
@Wallacoloo:这是未定义的,因为[expr.delete]/... if the static type of the object to be deleted is different from its dynamic type, ... the static type shall have a virtual destructor or the behavior is undefined. ...。如果Derived使用隐式生成的析构函数,则仍将是不确定的。
Mankarse 2012年

37

您问题的答案通常是但并非总是如此。如果您的抽象类禁止客户调用指向它的指针的delete(或者在其文档中如此说),则可以自由地声明一个虚拟析构函数。

您可以通过保护其析构函数来禁止客户端在指向它的指针上调用delete。像这样工作,省略虚拟析构函数是完全安全和合理的。

最终,您最终将没有虚拟方法表,最终会向您的客户端发信号,表明您打算通过指向它的指针将其设为不可删除,因此您确实有理由在这种情况下不将其声明为虚拟。

[请参阅本文的项目4:http//www.gotw.ca/publications/mill18.htm ]


使答案起作用的关键是“不要求删除”。通常,如果您有一个设计为接口的抽象基类,则将在接口类上调用delete。
John Dibling

正如约翰·约翰(John John)所指出的那样,您的建议非常危险。您所依据的假设是,接口的客户端永远不会破坏仅知道基本类型的对象。您可以保证,如果它是非虚拟的,则唯一的方法是使抽象类的dtor受保护。
米歇尔

Michel,我已经说过:)“如果这样做,则可以使析构函数受到保护。如果这样做,则客户端将无法使用指向该接口的指针进行删除。” 实际上,它并不依赖于客户端,但是必须强制执行它告诉客户端“您不能做...”。我看不到任何危险
Johannes Schaub-litb

我现在解决了我的回答的措辞很差。它现在明确声明它不依赖客户端。实际上,我认为很明显,依靠客户来做某事是一种阻碍。谢谢:)
约翰内斯·绍布

2
+1表示受保护的析构函数,这是删除指向基类的指针时意外调用错误的析构函数的另一个“出路”。
j_random_hacker

23

我决定进行一些研究,并尝试总结您的答案。以下问题将帮助您确定所需的析构函数:

  1. 您的课程是否打算用作基础课程?
    • 否:声明公共非虚拟析构函数,以避免在类*的每个对象上使用v指针。
    • 是:阅读下一个问题。
  2. 您的基类是抽象的吗?(即任何虚拟纯方法?)
    • 否:尝试通过重新设计类层次结构使基类抽象化
    • 是:阅读下一个问题。
  3. 是否要通过基本指针允许多态删除?
    • 否:声明受保护的虚拟析构函数以防止不必要的使用。
    • 是:声明公共虚拟析构函数(在这种情况下,无需任何开销)。

我希望这有帮助。

*需要特别注意的是,C ++中没有办法将一个类标记为最终类(即不可子类化),因此,在您决定声明析构函数为非虚拟且公开的析构函数的情况下,切记要明确警告其他程序员不要来自你的班级。

参考文献:


11
这个答案在某种程度上已经过时了,现在在C ++中有一个final关键字。
艾蒂安

10

是的,它总是很重要。派生类可以分配内存或保留对销毁对象时需要清除的其他资源的引用。如果不为接口/抽象类提供虚拟析构函数,则每次通过基类句柄删除派生类实例时,都不会调用派生类的析构函数。

因此,您正在释放潜在的内存泄漏

class IFoo
{
  public:
    virtual void DoFoo() = 0;
};

class Bar : public IFoo
{
  char* dooby = NULL;
  public:
    virtual void DoFoo() { dooby = new char[10]; }
    void ~Bar() { delete [] dooby; }
};

IFoo* baz = new Bar();
baz->DoFoo();
delete baz; // memory leak - dooby isn't deleted

没错,实际上在该示例中,它可能不仅是内存泄漏,而且还可能崩溃:-/
Evan Teran'Nov

7

它并不总是必需的,但我认为这是一种很好的做法。它的作用是允许派生对象通过基本类型的指针安全地删除。

因此,例如:

Base *p = new Derived;
// use p as you see fit
delete p;

如果Base没有虚拟析构函数,则格式错误,因为它将尝试删除对象,就好像它是一个Base *


您是否不想将boost :: shared_pointer p(new Derived)修复为boost :: shared_pointer <Base> p(new Derived); ?也许ppl会理解您的回答然后投票
Johannes Schaub-litb 08/11/29

编辑:“编纂”了两个部分,使尖括号可见,如litb建议。
j_random_hacker

@EvanTeran:我不确定自从最初发布答案以来,这种情况是否已经改变(boost.org/doc/libs/1_52_0/libs/smart_ptr/shared_ptr.htm上的Boost文档提示可能存在),但这不是真的这些天,shared_ptr它将尝试删除对象,就好像它是一个对象一样Base *-它会记住创建对象的类型。请参阅引用的链接,特别是写着“析构函数将使用相同的指针调用delete的指针,并使用其原始类型完成,即使T没有虚拟析构函数或为空也是如此。”
Stuart Golodetz

@StuartGolodetz:嗯,您可能是对的,但老实说我不确定。在这种情况下,由于缺少虚拟析构函数,它可能仍然不正确。值得研究。
埃文·特兰

@EvanTeran:如果它是有帮助的- stackoverflow.com/questions/3899790/shared-ptr-magic
Stuart Golodetz

5

这不仅是一种好习惯。这是任何类层次结构的规则#1。

  1. C ++中最基本的层次结构类必须具有虚拟析构函数

现在,为什么。采取典型的动物等级制度。与其他任何方法调用一样,虚拟析构函数也要进行虚拟调度。请看下面的例子。

Animal* pAnimal = GetAnimal();
delete pAnimal;

假设Animal是一个抽象类。C ++知道正确的析构函数调用的唯一方法是通过虚拟方法分派。如果析构函数不是虚拟的,则它将仅调用Animal的析构函数,而不销毁派生类中的任何对象。

使析构函数在基类中虚拟化的原因是,它只是从派生类中删除了选择。默认情况下,它们的析构函数变为虚拟的。


2
大多同意您的看法,因为通常在定义层次结构时,您希望能够使用基类指针/引用来引用派生对象。但这并非总是如此,在其他情况下,使基类dtor受到保护就足够了。
j_random_hacker

@j_random_hacker使其受到保护不会保护您免受不正确的内部删除
JaredPar 2009年

1
@JaredPar:是的,但是至少您可以对自己的代码负责—困难的是要确保客户端代码不会导致您的代码爆炸。(类似地,将数据成员
设为

@j_random_hacker,很抱歉回复博客文章,但确实适合这种情况。 blogs.msdn.com/jaredpar/archive/2008/03/24/...
JaredPar

@JaredPar:非常棒的帖子,我同意您100%的意见,尤其是关于检查零售代码中的合同。我只是说在某些情况下您知道不需要虚拟dtor。示例:用于模板分派的标签类。它们的大小为0,您只能使用继承来表示专长。
j_random_hacker 2009年

3

答案很简单,您需要它是虚拟的,否则基类将不是完整的多态类。

    Base *ptr = new Derived();
    delete ptr; // Here the call order of destructors: first Derived then Base.

您可能更希望进行上述删除,但是如果基类的析构函数不是虚拟的,则将仅调用基类的析构函数,并且派生类中的所有数据将保持不变。

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.