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

C++ Destructor called for the wrong object?

本文关键字:对象 错误 析构 函数调用 C++      更新时间:2023-10-16

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

[复制赋值运算符的]隐式版本执行浅层复制,该复制适用于许多类,但不适用于具有指向它们处理其存储的对象的指针的类。在这种情况下,不仅类会产生两次删除指向对象的风险,而且赋值还会在赋值之前不删除对象指向的对象,从而创建内存泄漏

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

在这次测试中,我遇到了一个完全不同的问题。我最初的想法是创建一个简单的类,该类存储一个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中,因此当它超出范围时,将调用析构函数。

出于某种原因,我们得到了两次析构函数的调用,而对象 1(它被对象 2 分配给x"覆盖"),它永远不会调用其析构函数,而是我们刚刚创建的对象的析构函数调用了它的析构函数。

所以......这总结为一个由两部分组成的问题:

1:为什么会发生这种奇怪的行为,为什么会这样,有什么合乎逻辑的理由吗?
2:通过赋值用另一个对象(对象 2)"覆盖"一个对象(例如对象 1)是否会导致调用其析构函数(在本例中为对象 1 的析构函数)?

提前谢谢。

使用调试器,当 x = Test(2); 行被调用时,创建 2 和销毁 2 都显示。如果我们只是将 x 分配给对象 2,为什么会立即调用它的析构函数?接下来是下一部分。

该行x = Test(2);首先使用构造函数参数2创建一个Test。这就是产生Created 2的原因.然后将此无名Test分配给xx.id给出值 2。然后,该无名Test在表达式末尾被销毁,产生"已销毁 2"。

其次,由于对象 2 的析构函数已被调用,我们可以假设它已被销毁。2 的下一个输出似乎与此相矛盾,因为它表明 x 仍然持有对象 2(预期,但与其析构函数的调用相矛盾)。

正如这个答案的第一部分所表达的那样,被摧毁的不是x,而是暂时的Tempx.id仍然有效,并将产生它的新值 2。

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

x在函数结束时被销毁时,会发生这种情况。它的值id被上一个赋值更改为 2,因此它产生"已销毁 2"。

1:为什么会发生这种奇怪的行为,为什么会这样,有什么合乎逻辑的理由吗?

这可能不是您期望的行为,但并不奇怪。我希望这个答案能帮助你理解为什么会发生这种情况。

2

:通过赋值用另一个对象(对象 2)"覆盖"一个对象(例如对象 1)是否会导致调用其析构函数(在本例中为对象 1 的析构函数)?

分配给对象不会破坏它。它用一个新的值替换它的值,从这个意义上说,它"破坏"了它以前帮助的值,但实际的对象实例不会被破坏,析构函数也不参与。

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

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 的实例。然后,第二个临时实例将创建为计数 1,值为 2。 然后这个将被分配给第一个,临时实例将在你的 std::cout 发生之前被删除!在您离开主函数范围的那一刻,第一个实例将被删除!

您可以学到什么:

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