虚析构函数和未定义行为

16

这个问题与'何时/为什么应该使用虚拟析构函数?'不同。

struct B {
  virtual void foo ();
  ~B() {}  // <--- not virtual
};
struct D : B {
  virtual void foo ();
  ~D() {}
};
B *p = new D;
delete p;  // D::~D() is not called

问题:

  1. 这是否可以被归类为未定义行为(我们知道~D()肯定不会被调用)?
  2. 如果~D()是空的,它会以任何方式影响代码吗?
  3. 使用B* p;new[]/delete[]后,无论析构函数是否虚拟,~D()都肯定不会被调用。这是未定义行为还是良好定义行为?

1
我经常考虑问同样的问题。我想要一个全面的答案,考虑三种情况:(1)B没有虚拟方法,(2)B有虚拟方法,但没有虚拟析构函数,(3)B有虚拟析构函数。显然,只有后者是定义良好的:https://dev59.com/tHI95IYBdhLWcg3w-DL0 - Aaron McDaid
请参考此处有关第三点的内容:https://dev59.com/Am025IYBdhLWcg3wLipX - Hari
4个回答

20

何时/为什么应该使用虚析构函数?
遵循Herb Sutters的指导方针

一个基类的析构函数应该是公共的和虚拟的,或者是受保护的和非虚拟的。

这是否可以归类为未定义行为(我们知道~D()肯定不会被调用)?

依据标准,它是未定义行为,通常会导致派生类析构函数不被调用并导致内存泄漏,但是猜测未定义行为的后果是无关紧要的,因为标准在这方面没有任何保证。

C++03标准:5.3.5 Delete

5.3.5/1:

delete-expression运算符销毁由new-expression创建的最终派生对象(1.8)或数组。
delete-expression:
::opt delete cast-expression
::opt delete [ ] cast-expression

5.3.5/3:

在第一种情况(删除对象)中,如果操作数的静态类型与其动态类型不同,则静态类型必须是操作数的动态类型的基类,并且静态类型必须具有虚拟析构函数,否则行为未定义。在第二种情况(删除数组)中,如果要删除的对象的动态类型与其静态类型不同,则行为未定义。73)

如果~D()为空,会对代码产生影响吗?
依然是未定义行为,派生类析构函数为空可能只会使程序正常工作,但这又是特定实现的实现定义方面,从技术上讲,它仍然是未定义行为。

请注意,在此没有保证,不使派生类析构函数虚拟化就不会导致调用派生类析构函数,这种假设是不正确的。根据标准,一旦您跨越了未定义行为领域,所有赌注都失效了。

请注意标准对未定义行为的定义。

C++03标准:1.3.12 未定义行为[defns.undefined]

行为,例如可能在使用错误的程序结构或错误的数据时出现,对于此国际标准不强制执行任何要求。当此国际标准省略任何行为的明确定义时,也可能预期出现未定义的行为。[注意:可允许的未定义行为范围从完全忽略情况并产生不可预测的结果,到在翻译或程序执行期间以特定于环境的记录方式表现(带或不带诊断消息),到终止翻译或执行(带有诊断消息)。许多错误的程序结构不会引起未定义的行为;它们需要被诊断。]

只有派生类的析构函数是否被调用,由上述引用中的粗体文本所控制,这显然为每个实现留下了空间。


3
@iammilind:既然保证不会调用~D(),那是谁说的?标准只规定如果析构函数不是虚函数,则是未定义行为,析构函数不被调用只是大多数实现中的副作用,并非标准所保证或要求的。 - Alok Save
3
@iammilind 没有任何保证 ~D() 不会被调用。标准规定在这种情况下的结果是未定义的,可能包括编译器某种方式插入魔法使 ~D() 被调用!只有在虚函数表的实现中才能得出结论:在大多数编译器中,派生类析构函数不会被调用。 - Mark B
2
注意:在C++11和C++14中,5.3.5/3基本上没有改变,因此这个答案仍然是正确的。 - M.M
这个在“鼻妖”意义上真的是未定义的吗?有可能会引起比派生类型部分删除更糟糕的情况吗? - Kyle Strand
1
@KyleStrand 在未定义的情况下没有程度。 - M.M
显示剩余2条评论

7
  1. 未定义行为
  2. (首先需要注意的是,这些析构函数通常并不像您想象的那样为空。您仍然需要拆解所有成员)即使析构函数真正为空(POD?),它仍然取决于您的编译器。根据标准,它是未定义的。对于所有标准来说,您的计算机都可能在删除时爆炸。
  3. 未定义行为

在一个旨在被继承的类中,非虚公共析构函数的存在确实没有必要。请参考本文,Guideline #4。

请使用受保护的非虚拟析构函数和shared_ptrs(它们具有静态链接)或公共虚拟析构函数。


为什么它是“未定义的”... 析构函数肯定不会被调用,这难道不是“明确定义”的吗? - iammilind
我猜你可以依赖于它不调用D的事实。但是除非D实际上是一个空类,否则我相当确定这将会导致问题,因为D的成员不会得到析构函数的调用。 - user406009
1
真的。但我的问题是,一切都会按照预期发生,例如~D()没有被调用,~D()成员的析构函数也没有被调用等等...那么未定义的事情从哪里来? - iammilind
基于标准,正如在这个精彩的答案中所提到的。 - user406009

2
正如其他人所重申的那样,这是完全未定义的,因为基类的析构函数不是虚拟的,任何人都不能做出任何声明。有关标准和进一步讨论,请参见此线程
(当然,各个编译器有权作出某些承诺,但在这种情况下我没有听到任何消息。)
我认为有趣的是,在某些情况下,我认为mallocfreenewdelete更好地定义了。也许我们应该使用它们 :-)
给定一个基类和一个派生类,两者都没有任何虚拟方法,以下内容已被定义:
Base * ptr = (Base*) malloc(sizeof(Derived)); // No virtual methods anywhere
free(ptr); // well-defined

如果D有复杂的额外成员,那么可能会导致内存泄漏,但除此之外,这是定义良好的行为。


我认为像POD这样的东西删除可能会很好定义。是时候去标准潜水了。 - user406009
@EthanSteinberg,那个其他线程上的示例链接是基于POD的,就我所知。 (实际上,如果一个结构体只有非虚函数,它仍然可以被称为POD吗?) - Aaron McDaid
是的,但我听说新的C++标准在修订POD方面做了很多工作,结果发现我错了。措辞仍然相同,和以前一样未定义。 - user406009
malloc 是一个分配函数。C 只有一种分配方式,而 C++ 有两种正交的概念,一种是分配,另一种是构造 - Kerrek SB
1
@KerrekSB,是的,我提供的代码确实需要用户更明确地管理初始化。但它确实为C++中更好定义的行为提供了一条路线。我并不是真正建议任何人使用它,但这是一个有趣的观察。 - Aaron McDaid

0

(我想我可能会删除我的另一个答案。)

关于那种行为的一切都是未定义的。如果您想要更好定义的行为,您应该研究一下 shared_ptr,或者自己实现类似的东西。以下是定义良好的行为,无论任何东西是否具有虚拟性:

    shared_ptr<B> p(new D);
    p.reset(); // To release the object (calling delete), as it's the last pointer.

shared_ptr 的主要技巧是模板化构造函数。

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