C ++析构函数调用了错误的对象?

时间:2019-01-16 18:44:21

标签: c++ destructor

我是C ++的新手,我写了一个小程序来学习赋值如何与对象一起工作。本页(http://www.cplusplus.com/doc/tutorial/classes2/)的cpp文档提示我执行此操作。在此页面上指出:

  

[副本分配运算符]的隐式版本 [em]执行浅表复制,该复制适用于许多类,但不适用于具有指向其处理其存储对象的指针的类。在这种情况下,不仅类会承担两次删除指向对象的风险,而且分配会通过不删除分配之前对象指向的对象而造成内存泄漏

我最后用粗体表示的部分是为什么我决定测试一下的原因。我认为可以通过处理析构函数中指向对象的删除(这是标准的?)来解决此问题,而不必使复制赋值运算符过载。如果不调用析构函数,那真的不方便吗?假设我有多个引用的对象,则必须将所有删除操作都放在析构函数中(对于大多数重新分配的情况)以及赋值重载中。

在此测试中,我遇到了一个完全不同的问题。我最初的想法是创建一个简单的类,该类存储一个int(作为测试目的的标识符),并重载构造函数和析构函数,以查看何时以及是否调用了析构函数。

这是我的代码:

class Test{
public:
    int id;
    explicit Test(int id) : id(id) {
        cout << "Created " << id << endl;
    }
    ~Test() {
        cout << "Destroyed " << id << endl;
    }
};

int main() {
    Test x = Test(1);
    x = Test(2);

    cout << x.id << endl;
    return 0;
}

我期望的输出是:

1:Created 1
2:Destroyed 1? (这是我不确定的那个,因为网站暗示如果将对象用另一个对象“替换”而不是超出范围,则不会调用该析构函数)
3:Created 2对象2“替换”了对象1,因为它已分配给x
4:2打印出对象2的ID的值
5:Destroyed 2对象2超出范围时被销毁

相反,我得到以下输出:

Created 1
Created 2
Destroyed 2
2
Destroyed 2

这对我来说真的没有意义。

使用调试器,Created 2Destroyed 2都在调用行x = Test(2);时显示。如果我们仅将x分配给对象2,为什么要立即调用其析构函数?接下来是下一部分。

第二,由于调用了对象2的析构函数,因此我们可以假定它已被销毁。 2的下一个输出似乎与此矛盾,因为它表明x仍在保存对象2(预期,但与它的析构函数的调用相矛盾)。

我不太确定为什么会这样。

最后,输出Destroyed 2。如果我们之前没有看到这一点,这将是有意义的。对象2存储在x中,因此当对象2超出范围时,将调用析构函数。

由于某种原因,我们两次调用了析构函数,而对象1(通过将对象2分配给x而被“覆盖”)从未调用过它的析构函数,而是我们刚刚创建的对象的析构函数有它的析构函数。

所以...这归结为两部分的问题:

1:为什么会发生这种奇怪的行为,并且有什么逻辑上的原因吗?
2:是否通过分配用另一个对象(对象2)“覆盖”一个对象(例如对象1)导致其析构函数(在这种情况下为对象1的析构函数)被调用?

谢谢。

2 个答案:

答案 0 :(得分:4)

  

使用调试器,当x = Test(2)行时,将同时显示Created 2和Destroyed 2;叫做。如果我们仅将x分配给对象2,为什么立即调用其析构函数?接下来是下一部分。

x = Test(2);行首先使用构造函数参数Test创建一个2。这就是产生Created 2的原因。然后,将此无名的Test分配给x,其值为x.id2。然后在表达式的末尾销毁无名的Test,生成“销毁2”。

  

第二,由于调用了对象2的析构函数,因此我们可以假定它已被销毁。下一个2的输出似乎与此矛盾,因为它表明x仍保留对象2(预期,但与它的析构函数的调用相矛盾)。

如该答案的第一部分所述,不是x被破坏,而是临时Tempx.id仍然有效,并将产生新值2。

  

最后,输出销毁2。如果我们之前没有看到这一点,这将是有意义的。对象2存储在x中,因此当它超出范围时,将调用析构函数。

在函数末尾x被销毁时会发生这种情况。 id的值已由上一次分配更改为2,因此它会产生“销毁2”。

  

1:为什么会发生这种奇怪的行为,并且有什么逻辑上的原因吗?

这可能不是您期望的行为,但这并不奇怪。我希望这个答案可以帮助您了解它为什么会发生。

  

2:是否通过赋值将另一个对象(对象2)“覆盖”一个对象(例如对象1)导致其析构函数(在这种情况下为对象1的析构函数)被调用?

分配给对象不会破坏它。它用新值替换了它的值,从这个意义上说,它“销毁”了以前帮助的 value ,但是实际的对象实例没有被销毁并且析构函数没有涉及。

编辑:似乎您可能担心资源泄漏。由于Test不会管理任何资源,因此不会发生泄漏,并且编译器生成的成员将表现良好。如果您的课程确实管理资源(通常以动态分配的内存形式),那么您将需要应用rule of 3/5/0。值得注意的是,您将需要自己实现赋值运算符,以便它清除以前保存的所有资源。仅实现析构函数是不够的,因为它不涉及分配。

答案 1 :(得分:2)

Test x = Test(1);

这将创建一个值为“ 1”的新对象。

x = Test(2);

这首先创建一个值为“ 2”的新对象,然后将其赋值 到为您的课程隐式创建的带有赋值运算符的第一个对象!此时,您有两个对象,两个对象的值均为2!

要获得更好的主意,您可以执行以下操作:

class Test{
    public:
        static int instanceCount;
        int id;
        int count;

        explicit Test(int id) : id{id}, count{instanceCount++} {
            std::cout << "Created " << id << " " << count << std::endl;
        }

        ~Test() {
            std::cout << "Destroyed " << id << " " << count << std::endl;
        }

        //Test& operator=(const Test&) = delete;
        Test& operator=(const Test& ex) 
        {
            id=ex.id;
            return *this;
        }
};  


int Test::instanceCount = 0;

int main() {
    Test x = Test{1};
    x = Test{2};

    std::cout << x.id << std::endl; 
    return 0;
}  

现在您可以看到何时创建新实例。如果删除类的赋值运算符,您将看到编写的第一条指令“ Test x = Test {1};”。不是工作,而是建设。第二个“ x = Test {2};”将失败,因为您现在删除了运算符。

输出如下:

Created 1 0
Created 2 1
Destroyed 2 1
2
Destroyed 2 0

如您所见,您首先获得一个具有计数0和值为1的实例。然后,将第二个临时实例创建为具有值为2的计数1。 然后,这个将被分配给第一个,并且临时实例将在您的std :: cout发生之前被删除!离开主函数作用域的那一刻,第一个实例将被删除!

您可以学到什么:

  • 使用X x=X(3);创建对象与写入X x(3);
  • 如果您还没有手动编写赋值运算符,则可能会获得默认值,具体取决于更多规则(在此处广泛介绍)。
  • 您应该看到您在此处创建了临时对象, 可以“即时”创建和删除,但是在大多数情况下都可以避免!
  • 您不应使用using namespace std
  • 您应该输入X x{3} instead of X x(3)`
  • 编写X x=X(3);完全是令人困惑的,因为看起来您正在构造一个临时文件,而不是将其分配给默认的构造文件。但这不会发生,因此您应该编写更简单的代码!