当显式调用析构函数时,析构函数被调用两次

23

我正在尝试使用C++中的析构函数,以下是我的代码:

#include <iostream>

struct temp
{
    ~temp() { std::cout << "Hello!" << std::endl; }
};

int main()
{
    temp t;
    t.~temp();
}

我发现“Hello!”被打印了两次。析构函数的调用不应该在对象被释放后再次被调用吗?还是说有其他概念存在?
(我并不打算在实践中这样做。我只是想理解这里发生了什么。)

2
你不应该手动调用析构函数。当对象超出作用域或使用动态内存分配时被删除时,析构函数会自动调用。 - dtech
17
有一个例外情况:如果你使用定位new来分配对象,那么你必须手动调用其析构函数。 - Jason R
7
@ddriver:请不要过于严厉。 - Sebastian Mach
4
@ddriver:我认为不仅我一个人会把所有大写字母的单词(如“PERIOD”)作为表达愤怒或沮丧的方式,这种语气并不有益。问题提问者并没有坚持要这样做,只是在询问为什么。 - Sebastian Mach
2
@ddriver:当然这是关于我的想法。但为什么需要那个“PERIOD”呢?即使没有大写,它看起来对我来说也很严厉,我想起了几年前一个朋友和我谈论同样的话题。这有点像当你对孩子说“因为就是这样,没商量”,这根本不是解释(在这种情况下甚至是错误或不完整的)。 - Sebastian Mach
显示剩余4条评论
10个回答

44

发生这种情况是因为您告诉它要发生。自动变量的析构函数在变量超出范围时总是被调用。 您还调用了它。 总共有两次调用。

调用对象的析构函数并不表示不需要再次调用,因为在正常执行中没有必要进行跟踪。

解决方案是永远不手动调用您的析构函数。


好的。但是当我第一次调用它时,它不应该释放对象吗?第二次调用会导致段错误吗? - Cygnus
7
不行。该对象是“自动”的,而非“动态”的。这意味着它没有被分配在可以被释放的位置上(在大多数系统上,它会被分配在栈上而非堆上)。由于您的对象是自动分配的,并且没有动态分配的字段,因此在其作用域结束时仍然是完整的。这是高度依赖具体实现的行为,绝不能依赖它。 - Jonathan Grynspan
3
这会导致未定义的行为。 - GManNickG
8
@Inverse:是的,根据C++03 12.4/14的规定:“如果对一个生命周期已经结束的对象调用析构函数,则其行为未定义”。严格来说,手动或自动调用析构函数意味着该对象已经结束其生命周期。 - Justin ᚅᚔᚈᚄᚒᚔ
1
@Excelcius:这会鼓励OP使用放置new。 :P - Jonathan Grynspan
显示剩余4条评论

20

调用析构函数并不会 释放 对象。

析构函数的作用是清理对象的内部,然后在析构函数完成后释放对象本身。

和调用两次delete一样,你现在所做的事情是错误的。虽然你可以调用两次delete,但这样做是错误的。

只有极少数情况需要手动调用析构函数,而当前情况不是其中之一。手动调用析构函数主要用于在内存地址上手动构造一个对象(使用placement new),然后需要在不释放内存的情况下析构对象。


3
我发现“Hello!”被打印了两次,按照调用析构函数的逻辑,对象应该被释放,当超出作用域时不应再次调用析构函数。难道有其他概念吗?
这是正确的。
你已经调用了析构函数,准备销毁一个对象,但是在对象实际被释放之前,当对象超出范围时,也会自动执行此操作。
需要理解的是:如果你做一些没有意义的事情,那么就会发生不好的事情。所以不要做没有意义的事情。如果你手动调用析构函数,那么析构函数将运行。除非析构函数实际上执行了某些影响,否则它不会对任何其他东西产生影响。

哈哈。没错!!就像我说的,这只是我突然想到的东西!!但是没错 - “不要做没有意义的事情”!! :) - Cygnus
5
这也展示了另外一个编程准则--错误的代码比正确的代码更难理解。 - David Schwartz
David,你说:“代码出问题了。它在对象仍在作用域内时销毁了它。所以它仍然在作用域内但不再存在。哎呀。”。但是当他手动调用析构函数时,代码并没有“销毁”对象。他只是调用了一个函数(并做出了错误的假设,认为它被销毁了)。它没有被销毁,它仍然存在。调用析构函数并不是一个好的实践,但它既不是非法的,也不总是不正确的。 - John Deters
2
“调用析构函数不应该释放对象,当对象超出作用域时,析构函数也不应再次被调用。”[...] “没错。” 我是否理解错误?这对我来说似乎完全不正确。也许在OP的看法中,析构函数在超出作用域时“不应该”再次被调用,但是由于语言保证,它将再次被调用。 - Jonathan Wakely
@JonathanWakely 调用析构函数应该释放对象(因为他想要对象在被销毁的同时也被释放),并且当对象超出范围时不应再次调用析构函数(因为如果在对象超出范围时调用,那只有这个时候才应该调用)。他的代码强制执行了与此不同的行为,这就是为什么它无法正常工作的原因。 - David Schwartz
1
好的,我认为我们达成了一致,但我们中至少有一个人误解了OP的意思 :) 我从OP引用的文本中读到的是“我相信显式调用析构函数会释放对象,这意味着析构函数不会再次被调用”...这就是为什么当你说那是正确的时候,我感到惊讶。 - Jonathan Wakely

1
析构函数被用于在对象作用域结束时被调用,如果对象在堆栈中(如本例)则自动调用,或者当对象最初使用new操作符在堆上创建时,通过delete显式销毁时调用。
编译器或运行时系统无法跟踪析构函数是否由您手动调用。而手动调用析构函数是非常不好的实践。
如果您想在对象被删除之前进行一些手动清理(除了从内存中删除对象或将其从堆栈中移除之外),可以像这样做。
在这里,您希望允许客户端在对象被删除之前手动清理事物。但是除此之外,还要在客户端未能清理时进行清理。
class A
{
public:
    A() : _closed(false)
    {}

    ~A()
    {
        close();
    }

    void close()
    {
        if (! _closed()) {
            // close file handles etc.
        }
    }

private:
    bool _closed
}

2
运行时完全可以跟踪手动析构函数调用,但这将违反 C++ 的“零开销原则”。 - Björn Pollex

1

你只需要调用析构函数,实际上并没有释放任何内存(它是静态分配的)。如果你使用 new 然后 delete,析构函数只会被调用一次。


1
析构函数并不是对象的“破坏者”。它只是一个普通的函数,但在对象销毁之前,语言会自动调用它。
它的正式名称是析构函数,但如果我们称其为“销毁前函数”,可能更容易理解。

1
我强烈不同意这个答案。按照定义,销毁是运行析构函数的过程,而不是之后发生的事情。标准规定对象的生命周期在析构函数调用开始时结束。在析构函数完成后没有其他事情发生,所以如果那是“销毁”之前,那么你认为什么是“销毁”?销毁的时间是什么?离开作用域?还是其他什么?我认为使用“销毁”一词来表示析构函数执行的操作更有意义,并为您所指的任何其他术语使用其他术语。 - Jonathan Wakely

1
你不需要调用销毁函数,但也可以这么做。当一个对象不再被使用时,编译器应该会自动运行它的析构函数。创建对象时,如果已为类成员声明了特定且初始化的值,则会利用构造函数。当你不再需要对象时,析构函数将运行并删除成员变量声明及其值。这对于不使用自动垃圾回收的语言(如 C++)非常有用。

0

类的析构函数可以被调用:

  1. 显式地

    当使用该类对象显式调用析构函数时,与调用该类的其他成员函数相同。

  2. 隐式地

    当该类对象超出作用域或使用new操作符创建的对象使用delete操作符进行销毁时。

在您的示例程序中,您两者都做了

int main()
{
  temp t;

  t.~temp(); //1. Calling destructor explictly using the object `t`

  return 0;
} // 2. object `t` goes out of scope. So destructor invoked implictly

这就是你看到析构函数被调用两次的原因。

正如你所想,析构函数将销毁由构造函数创建的资源。因此,不应显式调用析构函数,否则会导致销毁已经被销毁的资源,这可能是致命的。


0

从技术上讲,是运行时支持(而不是编译器)调用析构函数 ;) - user836352

0

你不需要显式地调用析构函数,当变量超出作用域时(在return 0;语句之后),它会自动调用。这就是为什么它会被调用两次,你先调用它,然后系统再调用它。

如果你想要能够自己显式地删除该类的实例,你需要动态分配它:

temp *t = new temp;     
// do stuff with t...
delete t;    // do not forget this, or the constructor is not being called at all

这不是一个很好的代码示例。应该在声明时初始化“t”。 在“new”之后无需检查“0”,因为如果无法创建对象,它默认会抛出异常。此外,在“delete”之后不需要设置“t = NULL”,这通常是一种代码异味。 - Björn Pollex
当调用new时,t被初始化。我本可以在声明的同一行进行初始化,但我想保持简单。至于指针,您可以编辑示例以包含高级范例,但我不知道这是否会对操作有所帮助;) - SvenS

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