虚拟析构函数和未定义的行为

Posted

技术标签:

【中文标题】虚拟析构函数和未定义的行为【英文标题】:Virtual destructor and undefined behavior 【发布时间】:2012-01-25 19:40:55 【问题描述】:

这个问题不同于“我何时/为什么应该使用virtual 析构函数?”。

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

问题

    这是否可以归类为未定义的行为(我们知道~D() 不会被称为肯定)? 如果~D() 为空怎么办。它会以任何方式影响代码吗? 在使用new[]/delete[]B* p; 时,~D() 肯定不会 被调用,不管析构函数的virtualness。是吗 未定义的行为或明确定义的行为?

【问题讨论】:

我经常想问同样的问题。考虑到这三种情况,我想要一个全面的答案:(1)B 没有虚拟方法,(2)B 有一个虚拟方法,但有一个非虚拟析构函数,(3)。 B 有一个虚拟析构函数。显然,只有后者是明确定义的:***.com/questions/2065938/virtual-destructor 【参考方案1】:
    未定义的行为 (首先要注意,这些解构函数通常没有你想象的那么空。你仍然需要解构你的所有成员)即使解构函数真的是空的(POD?),那么它仍然取决于您的编译器。它没有被标准定义。对于所有标准护理,您的计算机可能会在删除时炸毁。 未定义的行为

确实没有理由在要继承的类中使用非虚拟公共析构函数。查看this article,准则#4。

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

【讨论】:

为什么它是undefined ...不是定义明确 析构函数肯定不会被调用吗? 我猜你可以依赖它不调用 D 的事实。但除非 D 实际上是一个空类,否则我相当肯定这会导致问题,因为 D 的成员不会得到解构函数调用。 是的。但我的问题是,一切都会发生如预期,例如,~D() 未被调用,~D() 成员的析构函数未被调用等等......未定义的东西来自哪里?跨度> 基于标准,如this精彩回答中所述。【参考方案2】:

正如其他人重申的那样,这是完全未定义的,因为 Base 的析构函数不是虚拟的,任何人都不能发表任何声明。请参阅this thread 以获取标准参考和进一步讨论。

(当然,个别编译器有权做出某些承诺,但在这种情况下我还没有听说过。)

我觉得有趣的是,在这种情况下,我认为 mallocfree 在某些情况下的定义比 newdelete 更好。也许我们应该改用那些:-)

给定一个基类和一个派生类,它们都没有任何虚方法,定义如下:

Base * ptr = (Base*) malloc(sizeof(Derived)); // No virtual methods anywhere
free(ptr); // well-defined

如果 D 有复杂的额外成员,您可能会发生内存泄漏,但除此之外还有定义的行为。

【讨论】:

我认为删除对于 POD 之类的东西可能已经很好地定义了。是时候进行标准潜水了。 @EthanSteinberg,就我的理解而言,另一个线程link again 上的示例基于 POD。 (其实如果一个struct只有非虚函数,那它还能叫POD吗?) 是的,但我听说新的 C++ 标准在修改 POD 方面做了很多工作,但事实证明我错了。措辞仍然是一样的,就像以前一样未定义。 malloc 是一个分配函数。 C 只有分配,但 C++ 有分配和构造这两个正交的概念。 @KerrekSB,是的,我提供的代码确实需要用户更明确地管理初始化。但它确实为在 C++ 中更好地定义行为提供了途径。我并不是真的建议任何人实际使用它,但这是一个有趣的观察。【参考方案3】:

何时/为什么应该使用虚拟析构函数? 关注 Herb Sutters guideline

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

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

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

C++03 标准:5.3.5 删除

5.3.5/1:

delete-expression 操作符销毁由 new-expression 创建的最派生对象 (1.8) 或数组。 删除表达式: ::opt 删除强制转换表达式 ::opt delete [ ] 转换表达式

5.3.5/3:

在第一种选择(删除对象)中,如果操作数的静态类型与其动态类型不同,则静态类型应为操作数动态类型的基类,静态类型应具有虚拟析构函数或行为未定义。在第二种选择(删除数组)中,如果要删除的对象的动态类型与其静态类型不同,则行为未定义。73)

如果~D() 为空怎么办。它会以任何方式影响代码吗? 根据标准,它仍然是未定义的行为,派生类析构函数为空可能只会使您的程序正常工作,但这又是特定实现的实现定义方面,从技术上讲,它仍然是未定义的行为。

请注意,这里没有保证不将派生类析构函数设为虚拟就不会导致对派生类析构函数的调用,这个假设是不正确的。根据标准,一旦您在未定义行为领域越界,所有赌注都将取消。

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

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

行为,例如在使用错误程序结构或错误数据时可能出现的行为,本国际标准对此没有任何要求。当本国际标准省略对行为的任何明确定义的描述时,也可能出现未定义的行为。 [注意:允许的未定义行为的范围从完全忽略具有不可预测结果的情况到在 以环境特征的文件化方式翻译或程序执行(发出或不发出诊断消息),终止翻译或执行(发出诊断消息)。许多错误的程序结构不会产生未定义的行为;他们需要被诊断出来。 ]

如果只有派生的析构函数不会被调用,则由上面引用中的粗体文本控制,这显然对每个实现都是开放的。

【讨论】:

+1 表示 std::quotes;但我还是不明白,为什么标准把它称为 UB。因为保证不会调用~D()。保证行为是 UB 吗? @iammilind:既然保证了 ~D() 不会被调用,谁说呢?标准仅说明如果析构函数不是虚拟的,则它是 UB,未调用的析构函数是大多数实现中的后效,并且它不是保证的,也不是标准要求的。 @iammilind 无法保证不会调用~D()。标准说它是 undefined 在这种情况下发生的事情,这可能包括编译器以某种方式插入魔术以使 ~D() 被调用!它只是从一个 v-table 实现中得出,在大多数编译器中派生的析构函数不会被调用。 注意:5.3.5/3 在 C++11 和 C++14 中基本保持不变,所以这个答案仍然是正确的。 @KyleStrand 没有未定义的程度【参考方案4】:

(我想我可能会删除我的其他答案。)

关于该行为的一切都是未定义的。如果您想要更好地定义行为,您应该查看shared_ptr,或者自己实现类似的东西。以下是已定义的行为,无论任何事物的虚拟性如何:

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

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

【讨论】:

以上是关于虚拟析构函数和未定义的行为的主要内容,如果未能解决你的问题,请参考以下文章

当派生类的析构函数是虚拟的而基类的 dtor 不是时代码崩溃

根据定义,将“虚拟析构函数放入接口”是不是不再使其不再是接口?

gcc中内联的行为:虚函数的内联,尤其是我的析构函数

CRT 虚拟析构函数

源文件与头文件中的虚拟默认析构函数定义

如何在不破坏移动和复制构造函数的情况下声明虚拟析构函数