【问题标题】:Destructor being called twice when being explicitly invoked显式调用时,析构函数被调用两次
【发布时间】:2018-03-06 19:53:20
【问题描述】:

我正在用这段代码在 C++ 中试验析构函数:

#include <iostream>

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

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

我看到“你好!”正在打印两次。析构函数的调用不应该释放对象并且当它超出范围时不应该再次调用析构函数?还是有什么别的概念?

(我不打算在实践中这样做。我只是想了解这里发生了什么。)

【问题讨论】:

  • 您不应该手动调用析构函数PERIOD。当对象超出范围或使用动态内存分配时它们被删除时,它会自动调用。
  • @ddriver:有一个例外:如果你使用placement new分配一个对象,那么你必须手动调用它的析构函数。
  • @ddriver:请不要那么苛刻。
  • @ddriver:我认为并非只有我一个人将PERIOD 之类的全高词视为以无益的语气表达愤怒或沮丧的一种方式。并不是提问者坚持要这样做,只是问为什么。
  • @ddriver:这当然是我的想法。但是为什么根本需要那个“时期”呢?即使没有帽子,它对我来说确实看起来很苛刻,我想起了我的一个朋友几年前和我谈论完全相同的话题。这有点像当你对你的孩子说“因为它就是这样,句号。”,这只是一种解释(在这种情况下,充其量是不正确或不完整的)。

标签: c++ destructor


【解决方案1】:

它发生是因为你告诉它发生。当变量超出范围时,自动变量的析构函数总是被调用。你也叫它。总共是两个电话。

调用对象的析构函数并不意味着 C++ 不再调用它,因为在正常执行中不需要跟踪。

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

【讨论】:

  • 好的。但是当我第一次调用它时,它不应该释放对象,而第二次调用应该导致段错误?
  • 没有。该对象是自动的,而不是动态的。这意味着它没有分配在可以释放的地方(在大多数系统上,它在堆栈而不是堆上。)由于您的对象是自动分配的并且没有动态分配的字段,它仍然是完整的其范围的尽头。这是高度特定于实现的行为,绝不能依赖。
  • @Cygnus:它会导致未定义的行为。
  • @Inverse:是的,根据C++03 12.4/14the behaviour is undefined if the destructor is invoked for an object whose lifetime has ended。从形式上讲,调用析构函数(手动或自动)表示生命周期结束。
  • @Excelcius:这将鼓励 OP 使用新的展示位置。 :P
【解决方案2】:

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

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

执行您正在执行的操作类似于您可以对一个对象调用两次 delete 的方式是错误的,但这样做是错误的。

只有极少数情况下您想手动调用析构函数,而这不是其中之一。当您使用placement new 在内存位置手动构造对象然后需要能够在不释放内存的情况下销毁它时,它确实存在。

【讨论】:

    【解决方案3】:

    我看到“你好!”正在打印两次。析构函数的调用不应该释放对象,并且当它超出范围时不应再次调用析构函数。还是有什么其他的概念?

    没错。

    我必须提到,我不打算在实践中这样做。我只是想了解这里发生了什么。

    你调用了析构函数,准备销毁一个对象。但这也会在对象超出范围时自动完成,在它实际被释放之前。

    要理解的是:如果你做的事情没有意义,那么坏事就会发生。所以不要做没有意义的事情。如果你手动调用析构函数,析构函数就会运行。除非析构函数确实做了一些有影响的事情,否则这对其他任何事情都没有影响。

    【讨论】:

    • 哈哈。这是正确的 !!正如我所说,这是我刚刚想到的!但是,是的-“不要做没有意义的事情”! :)
    • 它还展示了另一个编程格言——错误的代码比正确的代码更难理解。
    • David,您说“代码已损坏。它在对象仍在作用域内时将其销毁。所以它仍在作用域内但不再存在。哎呀。”。但是当他手动调用析构函数时,代码并没有“破坏”对象。他只是在调用一个函数(并做出错误的假设它正在被破坏。)它没有被破坏,它仍然存在。可以肯定的是,调用析构函数并不是一个好习惯,但它并不违法,甚至总是不正确的。
    • “不应该调用析构函数释放对象,并且当它超出范围时不应再次调用析构函数。” [...] “那是正确的”我误读了吗?这对我来说似乎完全不正确。当析构函数超出 OP 的范围时,可能会再次调用“不应该”,但由于语言可以保证,它再次调用。跨度>
    • 好的,我认为我们同意,但至少我们中的一个人误解了 OP 的意思 :) 我读了我从 OP 中引用的文字说“我相信明确调用析构函数可以释放对象,这意味着不会再次调用析构函数” ...这就是为什么当你说这是正确的时候我很惊讶。
    【解决方案4】:

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

    【讨论】:

    • 谢谢!!我现在明白了!!
    【解决方案5】:

    如果对象在堆栈中,则在对象超出范围时调用析构函数,如本例所示,或者在使用 new 运算符在堆上创建对象时使用 delete 显式销毁它时调用地点。

    编译器或运行时系统无法跟踪析构函数是否由您手动调用。调用析构函数也是一种非常糟糕的做法。

    如果您想在对象被删除之前进行一些手动清理(除了从内存中删除或从堆栈中删除的对象),您可以这样做。

    在这里,您希望允许客户端手动清理内容,甚至在对象被删除之前。但除此之外,如果客户没有清理它,你会清理它。

    class A
    {
    public:
        A() : _closed(false)
        {}
    
        ~A()
        {
            close();
        }
    
        void close()
        {
            if (! _closed()) {
                // close file handles etc.
            }
        }
    
    private:
        bool _closed
    }
    

    【讨论】:

    【解决方案6】:

    析构函数不是对象的“破坏者”。它只是一个普通的函数,但在销毁之前由语言自动调用。

    它的正式名称是析构函数,但如果我们称它为“Before-Destruction”函数可能会更容易理解。

    【讨论】:

    • 我非常不同意这个答案。根据定义,破坏是运行析构函数的过程,而不是之后发生的事情。标准规定对象的生命周期在析构函数调用开始时结束。析构函数完成后没有其他任何事情发生,所以如果那是“在破坏之前”,那么你建议什么是“破坏”?什么是“毁灭之时”?超出范围?还有什么?我认为使用“破坏”一词来表示析构函数的作用更有意义,并使用其他术语来表示您所指的任何内容。
    【解决方案7】:

    您不需要调用析构函数,尽管可以这样做。当不再使用对象时,编译器应该为您隐式运行析构函数。创建对象时,如果已使用类成员的特定和初始化值声明了该对象,则该对象将使用您的构造函数。当您不再需要您的对象时,您的析构函数将运行并删除成员变量声明及其值。这对于不使用自动垃圾收集的语言(如 C++)最有用。

    【讨论】:

      【解决方案8】:

      您不必显式调用析构函数,它会在变量超出范围时自动调用(在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 - 这是usually a code-smell
      • 嗯,调用 new 时正在初始化 t。我本可以在声明的同一行中完成初始化,但我想保持简单。至于指针,欢迎您编辑示例以合并高级范例,但我不知道这是否有助于操作;)
      【解决方案9】:

      您实际上不应该调用解构器。运行时支持会为您调用它。因此,您调用它一次,运行时支持将第二次调用它。

      这里有一些关于析构函数的思考:

      http://publib.boulder.ibm.com/infocenter/comphelp/v8v101/index.jsp?topic=%2Fcom.ibm.xlcpp8a.doc%2Flanguage%2Fref%2Fcplr380.htm

      您可以显式调用解构器,但不建议这样做,通常它们会被隐式调用。

      【讨论】:

      • 从技术上讲,调用解构器的是运行时支持(而不是编译器);)
      【解决方案10】:

      可以调用类的析构函数:

      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
      

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

      正如你所想的那样,析构函数会破坏构造函数创建的资源。所以不应该显式调用析构函数,因为它会导致销毁已经销毁的资源,这可能是致命的。

      【讨论】:

        猜你喜欢
        • 2012-08-06
        • 2015-02-25
        • 2012-11-21
        • 1970-01-01
        • 2013-12-11
        • 2021-11-06
        相关资源
        最近更新 更多