派生类具有非虚析构函数

81

是否有任何情况下派生类可以拥有非 virtual 析构函数?非 virtual 析构函数意味着该类不应作为基类使用。如果派生类拥有非 virtual 析构函数,会像 Java 中的 final 修饰符一样弱化吗?

我特别关注的是基类具有 virtual 析构函数的派生类的情况。


3
补充说明,针对未来读者:如果一个类有一个带有虚析构函数的基类,则该类的析构函数(无论是用户声明还是隐式声明)都是虚拟的。 - andreee
11个回答

111
有没有一些情况下,派生类拥有非虚析构函数是合法的?
是的。
非虚析构函数表示一个类不应该被用作基类。
不完全正确;非虚析构函数表示通过基类指针删除派生类的实例将无法正常工作。例如:
class Base {};
class Derived : public Base {};

Base* b = new Derived;
delete b; // Does not call Derived's destructor!

如果您不以上述方式进行delete,那么就不会有问题。但如果是这种情况,您可能会使用组合而不是继承。

一个派生类没有虚析构函数是否就像Java中的final修饰符的弱形式?

不是的,因为虚性传播给了派生类。

class Base
{
public:
    virtual ~Base() {}
    virtual void Foo() {};
};

class Derived : public Base
{
public:
    ~Derived() {}  // Will also be virtual
    void Foo() {}; // Will also be virtual
};

这是C++11标准中正式规定的摘录:
如果一个类有一个带有虚析构函数的基类,那么它的析构函数(无论是用户声明还是隐式声明)都是虚函数。
- [class.dtor] p9 在C++03或更早的版本中,没有内置的语言机制来阻止子类化(*)。不过这并不是什么大问题,因为你应该始终优先使用组合而不是继承。也就是说,当“是一个”关系比真正的“有一个”关系更合理时,使用继承。
(*) 'final'修饰符在C++11中引入。

55
virtual”性质会传递到派生类中。它确实会传递到派生类中,可以查阅标准文件12.4.7:如果一个类有一个具有虚析构函数的基类,则它的析构函数(无论是用户声明的还是隐式声明的)都是虚的。 - Raedwald
7
相关的提示是,如果您有一个非虚拟的基类,而且您的派生类中有一些虚拟方法,则“Base *b = new Derived(); delete b;”将是未定义行为,并可能导致程序崩溃。这看起来很安全,但实际上并不是。这是因为“b”指向的不会是“Derived”对象的“开始”,而是会偏移vtable所需的空间。然后,“delete”将不会在完全相同的地址上操作“new”,因此它不是有效的地址可以释放。如果您要在任何地方使用虚拟方法,则在基类中放置虚拟析构函数。 - Aaron McDaid
3
@AaronMcDaid 如果需要vtable所需的空间,它将被偏移,它是vptr而不是vtable,vtable是每个类的,vptr是每个对象的。它是包含在对象中的vptr。 - Gab是好人

38

如果您从不会调用指向派生类对象的基类指针上的delete,那么拥有一个没有虚析构函数的基类是完全有效的。

遵循Herb Sutter的建议:

指南#:只有在派生类需要调用虚函数的基础实现时,才使虚函数受保护。 仅适用于析构函数的特殊情况:

指南#:基类析构函数应该是public和virtual,或者是protected和nonvirtual。


也许您的问题实际上是:
如果基类析构函数是虚拟的,那么派生类中的析构函数是否需要是虚拟的?

答案是不需要
如果基类析构函数是虚拟的,则派生类析构函数已经隐式地成为虚拟函数了,您不需要将其显式声明为虚拟函数。


2
“如果基类析构函数是虚拟的,那么派生类中的析构函数需要是虚拟的吗?” 是的,这确实是我的问题。 - Raedwald
如果派生类中没有分配任何数据(堆栈),即基类和派生对象的大小相等,则可以使用公共非虚拟基础析构函数。这可以解决需要显式转换的“typedefs”问题:使用具有显式构造函数的点、法线和方向子类的向量基类(某些不可修改API)。 - Matthias

16

针对最新的编辑内容:

编辑:我尤其关注基类拥有虚析构函数的派生类情况。

在这种情况下,无论是否添加virtual关键词,派生类的析构函数都将是虚的:

struct base {
   virtual ~base() {}       // destructor is virtual
};
struct derived : base {
   ~derived() {}            // destructor is also virtual, because it is virtual in base
};

如果在类型层次结构的任何一个点上声明了虚函数成员,那么与该函数相同的所有重写(而不是重载)都将是虚函数,无论它们是否声明为虚函数。析构函数的具体位于于 ~derived() 即使成员名称不同也会覆盖virtual ~base() --这是析构函数唯一的特殊情况。


5

你的问题并不是很清楚。如果基类有一个虚析构函数,无论如何派生类都会有一个。一旦声明了虚性,就没有办法关闭虚性。

当然,有些情况下从一个没有虚析构函数的类继承是有意义的。基类析构函数应该是虚拟的原因是可以通过指向基类的指针来删除。如果派生是私有的,你不必担心这个问题,因为你的Derived*不会转换成Base*。否则,我已经建议,如果基类的析构函数不是虚拟的,它应该是受保护的;这可以防止发生一种未定义行为的情况(通过指向基类的指针进行删除)。实际上,很多基类(例如std::iterator<>)具有这样的语义,以至于甚至没人想到要创建指向它们的指针;更不用说通过这样的指针删除了。因此,添加保护可能更费力气而得不偿失。


这可以防止一种未定义行为的情况(通过指向基类的指针进行删除),这种情况可能会发生。但是,您可以拥有一个在其自身上非常有用且非虚拟的基类,对吗? - pooya13

4

根据您的类的目的,有时将析构函数保护起来但不是虚的是一个好的做法。这基本上意味着:“您不应该通过基类型指针删除派生类的对象”。


2
如果你的派生类没有向基类添加任何数据成员,并且具有一个空的析构函数体,那么无论析构函数是否为虚函数都不会有影响 - 所有派生析构函数将调用基类析构函数。这并不推荐使用,因为很容易有人在不知道这些限制的情况下修改类。
如果您从未尝试通过指向基类的指针删除对象,则是安全的。这是另一条难以强制执行并应谨慎使用的规则。
有时您无法控制基类并被迫从中派生,即使析构函数不是虚函数。
最后,在基类中有一个非虚拟析构函数并不会对编译器强制执行的派生类施加任何限制,因此我认为它与Java的final并不相似。

听听。我曾经不得不从std::vector中派生出一个类,因为它有一个非虚析构函数,以保持向后兼容性。新添加的类没有添加任何数据成员,也没有自己的析构函数,所以一切都很好;并且可以保证是好的。根据我对C++标准的理解,在这种特殊情况下,这种行为是标准的,并且永远不会出现未定义的行为。 - Graham Asher

1

是的,有:

void dothis(Base const&);

void foo() {
  Derived d;
  tothis(d);
}

这里类被多态地使用,但是没有调用delete,因此没问题。

另一个例子是:

std::shared_ptr<Base> create() { return std::shared_ptr<Base>(new Derived); }

因为shared_ptr能够通过类型擦除使用非多态的delete

我在Clang中专门实现了一个警告来检测对具有非虚析构函数的多态非最终类进行delete调用,因此如果您使用clang -Wdelete-non-virtual-dtor,它将特别针对此情况发出警告。


1
一个非虚析构函数是完全可以的,只要你不想在删除对象时将它用作派生类的基指针。
如果你想以多态的方式使用它的派生类,通过基指针传递和存储它,然后再删除它,那么答案是否定的,请使用虚析构函数。

我的评论是:只要您不想使用其超类之一的指针删除对象,非虚析构函数就完全没问题。即使析构函数是虚拟的,如果超类的析构函数不是虚拟的,则使用超类类型的指针删除对象将会引发未定义行为。 - Nawaz
那还是不正确的。你需要编辑旧句子。 - Nawaz

0

在基类中,您可能不想创建虚析构函数?在这种情况下,不需要析构函数。如果您使用指向基类的指针,并在父类中创建非虚析构函数,则编译器会自动生成警告!如果您想创建最终的父类,则可以阻止它。但最好将其标记为final,如下所示:

class Base{
public:
    //No virtual destructor defined
    virtual void Foo() {};
};

class Derived final : public Base{
public:
    ~Derived() {}  // define some non-virtual destructor
    void Foo() {}; // Will also be virtual
};

在这种情况下,编译器知道你想要什么,不会生成警告。 空虚基类析构函数的决定并不太好,但是可以接受。你不需要设置final属性。但这不是你想要的。你也不需要定义空的虚基类方法Foo。 最好的方式是:
class Base{
public:
  //No virtual destructor defined
  virtual void Foo() = 0; // abstract method
};
class Derived final : public Base{
public:
  ~Derived() {}  // define some non-virtual destructor
  void Foo() {}; // Will also be virtual
};

这是一个完整的编译器代码。没有产生任何警告,也没有使用多余的代码。


0
拥有一个非虚析构函数的派生类是否会像Java中的final修饰符一样具有弱化的作用?
完全不是这样。以下是我的建议,以防止在C++中创建子类(就像Java中的final修饰符):将析构函数设为私有。然后,您可以防止从它创建子类。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接