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

Why is automatic object's destructor called twice?

本文关键字:两次 调用 对象 析构函数 为什么      更新时间:2023-10-16

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

#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已销毁。幻影2说话

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

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

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

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

但在那之后,我感到困惑。根据调试器的说法,getPhantom在第四行输出出现之前已经返回。当第二次调用Phantom的析构函数时,调用堆栈如下:

main~幻影

在一种管理语言中,我可以看到这句话:

phantom = getPhantom();

会摧毁幻影1,但不会触及幻影2。这是C++,而不是Java。

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

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

您忘记了正确解释:

  1. 复制构造函数。

  2. 赋值运算符。

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

phantom = getPhantom();

因此,您的会计核算会出错。

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

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;
}

幻影autoPhantom;

return autoPhantom;//这不可能是安全的

它非常安全。函数是按值返回对象,也就是说,将制作并返回一个副本(可能被"返回值优化"(RVO)所忽略)。

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

"额外"析构函数调用的原因只是局部变量被销毁,然后返回的副本被销毁。

与其质疑这样简单的代码是否会破坏一个从未构建过的对象,或者两次破坏某个对象,不如考虑对象更有可能是构建过的,每个对象只被破坏一次,但你没有准确地跟踪构建和破坏。

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

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

将这样的代码添加到您的类中:

Phantom& operator=(const Phantom& inPhantom)
{
    printf("Assigning.n");
}

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