5

在我看来,以下代码(来自一些 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?

4

9 回答 9

15

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

  • 当您显式调用它时
  • 一旦作用域结束并且自动变量被销毁

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

如果为生命周期已结束的对象调用析构函数,则行为未定义

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

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

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

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

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

什么是微不足道的析构函数?§12.4/3 说:

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

— 其类的所有直接基类都有微不足道的析构函数和

— 对于其类的所有属于类类型(或其数组)的非静态数据成员,每个这样的类都有一个微不足道的析构函数。

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

于 2010-07-20T15:44:50.863 回答
6

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

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

于 2010-07-20T15:22:27.190 回答
3

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

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

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

于 2010-07-20T15:29:52.333 回答
1

这里的问题是删除/释放和析构函数是独立且独立的构造。很像 new / allocation 和构造函数。可以只做上述其中一项而不做另一项。

在一般情况下,这种情况确实缺乏用处,只会导致与堆栈分配的值混淆。在我的脑海中,我想不出一个你想要这样做的好场景(尽管我确信可能有一个)。但是,可以考虑人为的场景,这将是合法的。

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 方法。

于 2010-07-20T15:21:15.080 回答
1

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

再说一次,对于未定义的行为,谁知道呢?:)

于 2010-07-20T15:22:17.303 回答
0

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

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

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

于 2010-07-20T17:13:01.417 回答
0

main 函数的作用是在栈上预留空间,调用 some 的构造函数,最后调用 some 的析构函数。这总是发生在局部变量上,无论您在函数中放置什么代码。您的编译器不会检测到您手动调用了析构函数。

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

于 2010-07-20T15:24:22.220 回答
0

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

于 2010-07-20T15:42:24.630 回答
0

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

于 2010-07-20T15:45:59.227 回答