为啥自动对象的析构函数被调用两次?

Posted

技术标签:

【中文标题】为啥自动对象的析构函数被调用两次?【英文标题】:Why is automatic object's destructor called twice?为什么自动对象的析构函数被调用两次? 【发布时间】:2016-11-23 17:00:30 【问题描述】:

(我的问题的答案涉及复制构造函数,但是复制发生在从函数返回时,而不是在对另一个类的方法调用中。我实际上看到了引用的可能重复项,但没有从由vector::push_back 我这里的函数也做了一个副本。也许我应该有。)

我正在尝试了解自动对象的构造/销毁。我遇到了一些对我来说很可疑的代码,所以我编写了自己的版本来努力理解它。简而言之,原始代码包含一个函数,该函数返回该函数的本地对象(自动)。这对我来说看起来不安全,所以我编写了这个程序来探索它:

#include <stdio.h>

class Phantom

private:
    static int counter;
    int id;

public:
    Phantom()
    
        ++counter;
        id = counter;
        printf("Phantom %d constructed.\n", id);
    ;

    virtual ~Phantom()
    
        printf("Phantom %d destructed.\n", id);
    ;

    void speak()
    
        printf("Phantom %d speaks.\n", id);
    ;
;

int Phantom::counter = 0;

Phantom getPhantom()

    Phantom autoPhantom;

    return autoPhantom; // THIS CAN'T BE SAFE


int main()

    Phantom phantom;

    phantom = getPhantom();

    phantom.speak();

    return 0;

我得到这个输出:

幻影 1 构建。
幻影 2 构建。
幻影2被毁。
幻影2被毁。
Phantom 2 会说话。

让我困惑的是输出中的第四行。

输入main 时会自动构建幻影1。

输入getPhantom时会自动构建幻影2。

Phantom 2 在退出 getPhantom 时会自动销毁(这就是为什么我认为从 getPhantom 返回它是不安全的)。

但在那之后我很困惑。根据调试器,getPhantom 已经返回第四行输出出现之前。第二次调用Phantom的析构函数时,调用栈是这样的:

主要
~幻影

在托管语言中,我可以看到这一行:

phantom = getPhantom();

会破坏 Phantom 1,但不会触及 Phantom 2。这是 C++,而不是 Java。

是什么导致第二次调用 Phantom 2 的析构函数?

【问题讨论】:

任何时候你想计算构造函数/析构函数调用,你都需要记住打印出复制构造函数调用。 当然按值返回对象是安全的。否则,该语言将从根本上被破坏。 真的应该有一个FAQ关于如何正确计算构造函数和析构函数,这个问题不断出现。 Rule of three。服从! @StevensMiller 是的。有复制省略和返回值优化之类的东西,但返回一些东西意味着你把它复制到函数返回空间中。 【参考方案1】:

与其质疑这样简单的代码是否会导致破坏一个从未构造过的对象,还是破坏两次,不如考虑一下对象被构造并且每个对象只被破坏一次的可能性要大得多,但你没有准确地跟踪建筑和破坏。

现在想一想在 C++ 中构造对象的其他方法,并考虑如果在任何时候使用复制构造函数会发生什么。然后考虑如何从函数返回本地对象,以及是否使用了复制构造函数。

如果你想改进你的测试代码,在析构函数中打印出this 指针的值,你会发现你给每个对象一个ID 的尝试是有缺陷的。您有多个具有不同身份(即内存中的地址)但“ID”相同的对象。

【讨论】:

我没想到,乔纳森。我说输出让我很困惑。 "// THIS CAN'T BE SAFE" 和“这就是为什么我认为从 getPhantom 返回它是不安全的”是一个相当大的假设。 天哪,乔纳森,你让我明白了。我真是个傻瓜。 @JonathanWakely: “在你开始假设编译器不理解 C++ 之前” -- 你似乎在暗示 C++ 编译器总是会告诉你是否在做不安全的东西。 @BenjaminLindley,从我所说的情况来看,这将是一个相当大的概括。从函数返回对象并不是语言的黑暗和危险角落。【参考方案2】:

将此类代码添加到您的课程中:

Phantom& operator=(const Phantom& inPhantom)

    printf("Assigning.\n");

你会看到第二个对象没有被销毁两次。解释比较简单。在赋值操作中,第一个对象将其所有字段值更改为第二个对象的值,但它不会被销毁。它仍然是第一个对象。 您更新的示例:http://cpp.sh/6b4lo

【讨论】:

这非常有启发性。非常感谢您花时间像您一样编辑我的代码。清楚地表明我的“id”并没有告诉我我认为它是什么。【参考方案3】:

我将评论您对返回具有自动存储的对象不安全的担忧:

Phantom getPhantom()

    Phantom autoPhantom;

    return autoPhantom; // THIS CAN'T BE SAFE

如果那样不安全,那么 C++ 将毫无用处,你不觉得吗?要查看我在说什么,只需将类型替换为...说int

int getPhantom()

    int autoPhantom = 0;

    return autoPhantom; // How does this look to you now?

明确一点:这是非常安全的,因为您正在返回值(即对象的副本)。

不安全的是返回一个指针或对这样一个对象的引用:

int* getInt()

   int a = 0;
   return &a;

【讨论】:

是的,你说的很对,我现在明白了。我有很长很长的 C 编程背景,并且是 C++ 新手。我将返回对象与返回指向本地的指针混淆了。感谢您帮助我理清思路。 @StevensMiller C 在这方面没有什么不同。 ??? C 允许按值返回structs(在我看来这还不够,但它是有效的)。 是的,但是返回一个指向本地的指针是一个常见的错误,不知何故,这就是我的样子。 啊,对。预标准化 C 是一个非常不同的故事。但 C89 已在近 30 年前标准化。【参考方案4】:

幻影自动幻影;

返回自动幻象; // 这不安全

绝对安全。该函数按值返回对象,也就是说,将制作并返回一个副本(可能被“返回值优化”(RVO)省略)。

如果函数返回了一个局部变量的引用或指针,那么你是对的,它是不安全的。

“额外的”析构函数调用的原因仅仅是局部变量被破坏,然后返回的副本被破坏。

【讨论】:

返回对局部变量的 const 引用不会延长它的生命周期。 @underscore_d 不,自动变量的生命周期不能通过返回对它们的引用来延长,左值或右值。 你说得对,只有他临时归还你才能延长寿命。搞糊涂了。棘手且容易出错... 不,返回的临时对象也不能通过引用返回来延长它们的生命周期。 [class.temporary]/5.2: The lifetime of a temporary bound to the returned value in a function return statement is not extended; the temporary is destroyed at the end of the full-expression in the return statement. 我认为你们对返回的临时对象和像 const Foo&amp; f = make_foo(); 这样绑定的临时对象感到困惑,这有效的。【参考方案5】:

您忘记正确说明:

    复制构造函数。

    赋值运算符。

在这两种情况下,您最终都会得到多个具有相同id 的对象,并且两个对象最终都会在其析构函数中打印相同的id。在复制构造函数的情况下,构造函数中不会打印任何消息,因为您没有定义自己的复制构造函数。在赋值运算符的情况下,构造函数中分配的id 会被另一个对象的重复id 覆盖。这就是这里发生的事情:

phantom = getPhantom();

因此,您的会计处理错了。

【讨论】:

赋值操作符实际上不需要打印任何东西,因为被赋值的对象已经构造好了。 @NathanOliver:赋值运算符不需要打印任何内容,但它确实需要防止对象的id 被覆盖。【参考方案6】:

您退回一份。因此,getPhantom() 中的变量在作用域的末尾被销毁,并且您留下的副本也具有 id 2。这是因为在返回时它调用不增加 id 的复制构造函数(也是默认构造函数)。

【讨论】:

Sam Varshavchik 的回答也很有帮助,所有将我引向复制构造函数的 cmets 也是如此。你们中的那些像内森和其他回答问题而不侮辱提问者的人,是伟大的同事。谢谢!

以上是关于为啥自动对象的析构函数被调用两次?的主要内容,如果未能解决你的问题,请参考以下文章

为啥这个自定义分配器的析构函数在 GCC/MSVS 的 stdlib 中被调用两次

访问冲突 - 为啥基类析构函数被调用两次? [关闭]

静态对象成员会在所属类的析构函数被调用时自动析构吗?

为啥在调用它的析构函数后我可以访问这个堆栈分配的对象? [复制]

C++ 设置基类的析构函数为虚函数

cpp中的析构函数会自动调用吗?即使析构函数没有提及非动态变量,它们是不是也会被删除?