手动调用析构函数

4
#include <iostream> 
using namespace std; 
namespace GB
{
    class Test 
    { 
    public: 
        Test()  { cout << "Constructor is executed\n"; } 
        ~Test() { 
            cout << i << " " << "Destructor is executed\n";  
            this->i = 7;
        } 
        int i = -1;
    }; 
}
  
int main() 
{ 
    // Test();  // Explicit call to constructor 
    GB::Test t;    // local object 
    t.i = 6;
    t.~Test(); // Explicit call to destructor 
    return 0; 
}

输出

Constructor is executed
6 Destructor is executed
6 Destructor is executed

我的问题是:
1) 为什么析构函数会被调用两次。
2) 在第一次调用析构函数时,成员变量的值从6更改为7,但在第二次调用中它仍然是6。
3) 我们能够停止析构函数的第二次调用吗?(我只想手动调用析构函数)。


5
基本上,你永远不应该显式调用析构函数。除非你百分之百确定自己处于少数需要这样做的情况,否则这肯定是一个错误。 - François Andrieux
4
也许你需要退后几步,重新了解对象的生命周期和作用域? - Some programmer dude
2
this->i = 7; 没有任何效果,因为那是最后一次可以合法访问 this。对 i 的更改无法被观察到。编译器完全可以正确地将其优化掉。 - François Andrieux
2
C++会在对象超出作用域并被销毁时销毁它并调用其析构函数。这是一个绝对不能改变的规则。手动调用析构函数也无法改变这一点。 - Sam Varshavchik
2
手动调用析构函数最常见的原因是放置new,这是一个高级话题,很少在使用非平凡类型的联合或高度优化的容器(如std::vector)之外出现。 - user4581301
显示剩余9条评论
3个回答

8
析构函数为什么会被调用两次?
第一次是从“i.~Test();”这一行调用的。 第二次是在变量“i”超出作用域时自动调用的析构函数(在返回“main”之前)。
在析构函数第一次调用时,成员值从6更改为7,但在第二次调用时,它仍然是6。
这是由未定义的行为引起的。当一个对象的析构函数被调用两次时,应该预期会发生未定义的行为。当程序进入未定义行为领域时,不要试图做逻辑上的解释。
我们无法禁止自动变量的析构函数在变量超出作用域时被调用。
如果你想控制析构函数的调用时间,请使用动态内存创建对象(通过调用“new Test”),并通过调用“delete”销毁对象。
GB::Test* t = new GB::Test(); // Calls the constructor
t->i = 6;
delete t;                     // Calls the destructor

即使在这种情况下,显式调用析构函数几乎总是错误的。

t->~Test();  // Almost always wrong. Don't do it.

请注意,如果您想使用动态内存创建对象,则最好使用智能指针。例如:
auto t = std::make_unique<GB::Test>();  // Calls the constructor
t->i = 6;
t.reset();                              // Calls the destructor

如果没有 t.reset();,当变量 t 超出其作用域时,动态分配对象的析构函数将被调用,且内存会被释放。而使用 t.reset(); 可以控制底层对象的删除时间。

3

我想补充其他优秀答案。

通过使用联合,可以在不使用堆的情况下显式调用对象的构造函数和析构函数。以下是一个例子:

namespace GB
{
    class Test
    {
    public:
        Test()  { cout << "Constructor is executed\n"; }
        ~Test() {
            cout << i << " " << "Destructor is executed\n";
            this->i = 7;
        }
        int i = -1;
    };
    union OptTest
    {
        OptTest() : test() {}  // if there is no need to control the construction
        ~OptTest() {}
        Test test;
        char none;
    };
}
int main()
{
    // EDIT: The following comment appears in the original question,
    //       but it is misleading. Test() will create a temporary object,
    //       which will be immediately destroyed. Use placement-new to
    //       explicitly call a constructor instead.

    // Test();  // Explicit call to constructor
    GB::OptTest ot;    // local object
    ot.test.i = 6;
    ot.test.~Test(); // Explicit call to destructor
    return 0;
}

无论这是否是一个好主意,取决于使用情况。例如,如果你想要实现类似于 std::optional<Test> 的东西,那么使用 union 是控制 Test 析构的一种好方法。
注意:虽然 none 字段不是严格必需的,但在我看来,它更好地传达了 union 可能处于两种状态之一(具有 Test 对象和没有 - 在调用析构函数后)。

3

1)为什么析构函数会被调用两次。

这是因为这是C++而不是Pascal,当对象生命周期结束时(即在其生命周期结束时),语言负责销毁对象。对象的生命周期是任何语言语义的组成部分,它们经常是区分语言的区别,因此您不能仅仅假定C++将表现得像您正在考虑的其他某种语言一样。由于语言在作用域({})结束时销毁对象,因此第二次冗余调用是您手动编写的早期调用。 C ++的整个重点在某种程度上是为了替您处理这个问题:如果您认为需要手动销毁东西,则通常没有考虑C ++。

2)在第一个析构函数调用中,成员变量的值从6更改为7,但在第二个调用中,它仍然为6。

对象在销毁后就不存在了,所以在析构函数内对对象状态的更改将丢失。第二次调用析构函数是未定义的行为,因此它可以做“任何事情”。编译器可能会优化析构函数中的 this->i = 7 赋值,因为该赋值没有意图在其外可观测到的副作用:您编写了死代码,不要惊讶它被视为死代码。

3)我们可以停止析构函数的第二次调用吗(我只想保留手动调用的析构函数)。

不行。C++不是Pascal。整个重点是析构函数自动运行。这是C++和许多其他面向对象语言之间的重大语义差异:C++析构函数也与Java或.Net(C#)中的finalizers没有任何相似之处。如果要更早地销毁对象,请适当限制对象的范围。忘记手动调用析构函数。

换句话说:不要把析构函数看作是可以调用的函数。相反,考虑如何管理对象的生命周期,以便在您认为方便时销毁它。


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