显式调用析构函数

Posted

技术标签:

【中文标题】显式调用析构函数【英文标题】:Explicit call to destructor 【发布时间】:2019-04-15 04:19:40 【问题描述】:

我偶然发现了以下代码sn-p:

#include <iostream>
#include <string>
using namespace std;
class First

    string *s;
    public:
    First()  s = new string("Text");
    ~First()  delete s;
    void Print() cout<<*s;
;

int main()

    First FirstObject;
    FirstObject.Print();
    FirstObject.~First();

文中说这个sn-p应该会导致运行时错误。现在,我不太确定,所以我尝试编译并运行它。有效。奇怪的是,尽管所涉及的数据很简单,但程序在打印“文本”后卡顿,仅在一秒钟后完成。

我添加了一个要打印到析构函数的字符串,因为我不确定显式调用这样的析构函数是否合法。该程序打印了两次字符串。所以我的猜测是析构函数被调用了两次,因为正常的程序终止不知道显式调用并尝试再次销毁对象。

简单的搜索证实,在自动化对象上是危险的,因为第二次调用(当对象超出范围时)具有未定义的行为。所以我对我的编译器(VS 2017)或这个特定程序很幸运。

关于运行时错误的文字是否完全错误?或者运行时错误真的很常见吗?或者也许我的编译器针对这种事情实现了某种防护机制?

【问题讨论】:

C++ 标准从不保证运行时错误(总是未定义的行为),所以文本肯定是错误的 @UnholySheep,我不知道我会这么说。例如,离开 noexcept 函数的异常是对 std::terminate 的保证调用,我将其归类为运行时错误。 @chris 好点,我只是在考虑所描述的情况(而且我看到太多的文本声称调用 UB 的代码总是会导致运行时错误/分段错误) 如何将字符串打印两次?它将释放它两次,因为当对象被销毁时会调用析构函数。 运行时错误肯定会发生:析构函数被调用了两次,这是一个错误,它发生在运行时。我敢打赌,如果您在调试模式下运行测试,您可以“捕捉”它。为什么没有消息弹出?知道它所做的最后两件事是删除同一个指针时,VC2017 在终止您的应用程序时会在发布模式下做什么?是否有一些优化会错误地隐藏/修复您的错误?您可能应该询问 MS 支持... 【参考方案1】:

简单的搜索证实,在自动化对象上显式调用析构函数是危险的,因为第二次调用(当对象超出范围时)具有未定义的行为。

确实如此。如果您使用自动存储显式销毁对象,则会调用未定义的行为。 Learn more about it.

所以我很幸运能够使用我的编译器(VS 2017)或这个特定的程序。

我会说你是不幸的。 UB 可能发生的最好的(对你来说,编码器)是在第一次运行时崩溃。如果它看起来工作正常,那么崩溃可能会在 2038 年 1 月 19 日在生产中发生。

关于运行时错误的文字是否完全错误?或者运行时错误真的很常见吗?或者也许我的编译器针对这种事情实现了某种防护机制?

是的,文字有点错误。 未定义的行为是未定义的。运行时错误只是众多可能性之一(包括鼻恶魔)。

关于未定义行为的好读物:What is undefined behavor?

【讨论】:

"当您使用自动存储显式销毁对象时调用未定义行为" - 更像是在对象到达其自然生命周期结束而没有被转世时调用它。析构函数调用本身不是 UB。 更准确地说,2038 年 1 月 19 日 03:14:08 UTC :D @StoryTeller 是的。我想避免细节。我已经修好了。【参考方案2】:

关于运行时错误的文字是否完全错误?

错了。

或者运行时错误真的很常见吗?或者也许我的编译器针对这种事情实现了某种防护机制?

你不知道,当你的代码调用Undefined Behavior时会发生这种情况;你不知道执行它时会发生什么。

在你的情况下,你是(不)幸运*并且它起作用了,而对我来说,它导致了error(双倍免费)。


*因为如果您收到错误,您将开始调试,否则,例如在大型项目中,您可能会错过它...

【讨论】:

【参考方案3】:

不,这只是 C++ 标准草案[class.dtor]p16 中未定义的行为:

一旦为对象调用析构函数,该对象就不再存在;如果为生命周期已结束的对象([basic.life])调用析构函数,则行为未定义。 [ 示例:如果自动对象的析构函数被显式调用,并且随后以通常会调用对象的隐式销毁的方式留下块,则行为未定义。 —— 结束示例

我们可以从defintion of undefined behavior看到:

本文档没有要求的行为

您不能对结果抱有任何期望。对于作者而言,在特定机器上使用特定选项的特定编译器可能会以这种方式表现,但我们不能指望它是可移植的或可靠的结果。虽然在某些情况下implementation does try to obtain a specific result 但这只是另一种可接受的未定义行为。

另外,[class.dtor]p15 提供了我上面引用的规范部分的更多背景信息:

[ 注意:很少需要显式调用析构函数。 这种调用的一种用途是使用放置新表达式放置在特定地址的对象。 为了处理专用硬件资源和编写内存管理工具,可能需要使用对象的显式放置和销毁。 例如,

void* operator new(std::size_t, void* p)  return p; 
struct X 
  X(int);
  ~X();
;
void f(X* p);

void g()                       // rare, specialized use:
  char* buf = new char[sizeof(X)];
  X* p = new(buf) X(222);       // use buf[] and initialize
  f(p);
  p->X::~X();                   // cleanup

— 尾注 ]

【讨论】:

以上是关于显式调用析构函数的主要内容,如果未能解决你的问题,请参考以下文章

在信号处理程序中显式调用析构函数

是否允许显式调用析构函数,然后在具有固定生命周期的变量上放置 new?

unity c#怎么调用析构函数

定位new表达式与显式调用析构函数

9——对象的创建和撤销,构造函数和析构函数

C++ 类设计总结回顾------析构函数