显式调用析构函数会导致未定义的行为吗?

时间:2010-07-20 15:17:51

标签: c++ destructor undefined-behavior

在我看来,下面的代码(来自一些C ++问题)应该导致UB,但它似乎不是。这是代码:

#include <iostream>
using namespace std;
class some{ public: ~some() { cout<<"some's destructor"<<endl; } };
int main() { some s; s.~some(); }

答案是:

some's destructor
some's destructor

我学习了c ++ faq lite,我们不应该显式调用析构函数。我认为在显式调用析构函数后,应该删除对象s。程序完成后会自动再次调用析构函数,它应该是UB。但是,我在g ++上尝试了它,并获得与上述答案相同的结果。

是因为这个类太简单了(不涉及新的/删除)?或者在这种情况下根本不是UB?

9 个答案:

答案 0 :(得分:15)

行为未定义,因为对同一对象调用析构函数两次:

  • 一次明确调用
  • 一旦范围结束并且自动变量被销毁

在生命周期结束的对象上调用析构函数会导致每个C ++的未定义行为03§12.4/ 6:

  

如果为生命周期结束的对象调用析构函数

,则行为未定义

对象的生命周期在根据§3.8/ 1调用析构函数时结束:

  

类型T的对象的生命周期在以下时间结束:

     

- 如果T是具有非平凡析构函数(12.4)的类类型,则析构函数调用开始,或

     

- 重用或释放对象占用的存储空间。

请注意,这意味着如果您的类有一个简单的析构函数,行为是明确定义的,因为这种类型的对象的生命周期不会结束,直到它的存储被释放,自动变量直到最后才会发生功能。当然,我不知道为什么你会明确地调用析构函数,如果它是微不足道的。

什么是琐碎的析构函数? §12.4/ 3说:

  

如果析构函数是隐式声明的析构函数并且如果:

,则析构函数是微不足道的      

- 其类的所有直接基类都有简单的析构函数和

     

- 对于类的所有类型(或其数组)的非静态数据成员,每个这样的类都有一个简单的析构函数。

正如其他人所提到的,未定义行为的一个可能结果是您的程序似乎继续正常运行;另一个可能的结果是你的程序崩溃了。任何事情都可能发生,并且没有任何保证。

答案 1 :(得分:6)

这是未定义的行为 - 但与任何UB一样,一种可能性是它(或多或少)似乎起作用,至少对某些工作定义而言。

基本上,您需要(或想要)显式调用析构函数的唯一时间是与placement new结合使用(即,您使用placement new在指定位置创建对象,并使用显式dtor调用来销毁该对象)

答案 2 :(得分:3)

来自http://www.devx.com/tips/Tip/12684

  

未定义的行为表明当程序达到某个状态时,实现可能会出现不可预测的行为,这几乎毫无例外地是一个错误的结果。未定义的行为可以表现为运行时崩溃,不稳定和不可靠的程序状态,或者 - 在极少数情况下 - 甚至可能未被注意

在你的情况下,它不会崩溃,因为析构函数不会操纵任何字段;实际上,您的班级根本没有任何数据成员。如果确实如此,并且在析构函数的主体中以任何方式操纵它,那么在第二次调用析构函数时可能会遇到运行时异常。

答案 3 :(得分:1)

这里的问题是删除/释放和析构函数是独立且独立的结构。很像新的/分配和构造函数。没有另一个,可以只做上述之一。

在一般情况下,这种情况确实缺乏实用性,只会导致与堆栈分配值混淆。在我的头脑中,我想不出你想要做到这一点的好方案(虽然我确信有可能存在)。然而,有可能想到这是合法的人为场景。

class StackPointer<T> {
  T* m_pData;
public:
  StackPointer(T* pData) :m_pData(pData) {}
  ~StackPointer() { 
    delete m_pData; 
    m_pData = NULL; 
  }
  StackPointer& operator=(T* pOther) {
    this->~StackPointer();
    m_pData = pOther;
    return this;
  }
};

注意:请不要以这种方式编写类。请改为使用显式Release方法。

答案 4 :(得分:1)

它很可能正常工作,因为析构函数不引用任何类成员变量。如果你试图在析构函数中delete一个变量,那么第二次自动调用它时可能会遇到麻烦。

然后,对于未定义的行为,谁知道? :)

答案 5 :(得分:0)

主要功能的作用是在堆栈上保留空间,调用某些构造函数,最后调用一些析构函数。这总是发生在局部变量中,无论你在函数中放入什么代码。 您的编译器不会检测到您手动调用析构函数。

无论如何,你不应该手动调用对象的析构函数,除了使用placement-new创建的对象。

答案 6 :(得分:0)

我相信如果您希望您的代码正常,您只需要调用placement new并在退出之前重新填充。对析构函数的调用不是问题,它是你离开作用域时对析构函数的第二次调用。

答案 7 :(得分:0)

您能定义您期望的未定义行为吗?未定义并不意味着随机(或灾难性):给定程序的行为在调用之间可能是可重复的,它只是意味着您无法依赖于任何特定行为,因为它未定义且无法保证将会发生什么。 / p>

答案 8 :(得分:0)

这是未定义的行为。未定义的行为是双析构函数调用,而不是析构函数调用本身。如果您将示例修改为:

#include <iostream>
using namespace std;
class some{ public: ~some() { [INSERT ANY CODE HERE] } };
int main() { some s; s.~some(); }

其中 [INSERT ANY CODE HERE] 可以替换为任意代码。结果具有不可预测的副作用,这就是为什么它被认为是未定义的。