我是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 2
和Destroyed 2
都在调用行x = Test(2);
时显示。如果我们仅将x
分配给对象2,为什么要立即调用其析构函数?接下来是下一部分。
第二,由于调用了对象2的析构函数,因此我们可以假定它已被销毁。 2
的下一个输出似乎与此矛盾,因为它表明x
仍在保存对象2(预期,但与它的析构函数的调用相矛盾)。
我不太确定为什么会这样。
最后,输出Destroyed 2
。如果我们之前没有看到这一点,这将是有意义的。对象2存储在x
中,因此当对象2超出范围时,将调用析构函数。
由于某种原因,我们两次调用了析构函数,而对象1(通过将对象2分配给x
而被“覆盖”)从未调用过它的析构函数,而是我们刚刚创建的对象的析构函数有它的析构函数。
所以...这归结为两部分的问题:
1:为什么会发生这种奇怪的行为,并且有什么逻辑上的原因吗?
2:是否通过分配用另一个对象(对象2)“覆盖”一个对象(例如对象1)导致其析构函数(在这种情况下为对象1的析构函数)被调用?
谢谢。
答案 0 :(得分:4)
使用调试器,当x = Test(2)行时,将同时显示Created 2和Destroyed 2;叫做。如果我们仅将x分配给对象2,为什么立即调用其析构函数?接下来是下一部分。
第x = Test(2);
行首先使用构造函数参数Test
创建一个2
。这就是产生Created 2
的原因。然后,将此无名的Test
分配给x
,其值为x.id
2。然后在表达式的末尾销毁无名的Test
,生成“销毁2”。
第二,由于调用了对象2的析构函数,因此我们可以假定它已被销毁。下一个2的输出似乎与此矛盾,因为它表明x仍保留对象2(预期,但与它的析构函数的调用相矛盾)。
如该答案的第一部分所述,不是x
被破坏,而是临时Temp
。 x.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);
完全是令人困惑的,因为看起来您正在构造一个临时文件,而不是将其分配给默认的构造文件。但这不会发生,因此您应该编写更简单的代码!