[英]About C++ destructors
我有一些Java经验,并且是C ++的初学者。
下面是我的代码,其输出是:
0 1 2 3 4 5 6 7 8 9
destructor ---s1
8791616 8785704 2
destructor ---s1
我期待以下输出:
0 1 2 3 4 5 6 7 8 9
destructor ---abc
0 1 2
destructor ---s1
我无法理解为什么析构函数释放第一个对象的资源。 如何打印我预期的输出?
#include <iostream>
using namespace std;
class Sequence{
public:
Sequence(int count=10,string name = "abc");
void show();
~Sequence();
int* _content;
int _count;
string _name;
};
Sequence::Sequence(int count,string name){
_count = count;
_content=new int[count];
_name = name;
for(int i=0;i<count;i++){
_content[i]=i;
}
}
Sequence::~Sequence(){
cout << "destructor ---"<<_name<<endl;
delete [] _content;
}
void Sequence::show(){
for(int i=0;i<_count;i++)
cout<<_content[i]<<" ";
cout<<endl;
}
int main(){
Sequence s1 = Sequence();
s1.show();
s1 = Sequence(3,"s1");
s1.show();
}
如果你提高编译器的警告级别,你会得到一个提示,你的类包含指针,但你没有定义Sequence(const Sequence&)
或operator=(const Sequence&)
(参见什么是三规则? )。
因为您没有提供复制构造函数或赋值运算符,所以编译器会为您提供这些,它们执行成员分配。
当你调用s1 = Sequence(3,"s1");
,您正在执行以下操作(这对Java开发人员来说可能是意外的):
Sequence
s1
,其中:
si._content
设置为指向刚刚创建的三个ints
的新数组的指针,泄漏旧的10个ints
。 si._count
设置为3
si._name
设置为"s1"
s1
)(在上面的实际输出中,你看到“s1”被销毁两次),留下_content
指向free'd内存(这就是你在第二次调用s1.show()
看到垃圾的s1.show()
)。 如果您声明这样的赋值运算符,您将获得更接近预期输出的内容:
Sequence& operator =(const Sequence& rhs)
{
if (this != &rhs)
{
delete [] _content;
_count = rhs._count;
_content = new int[_count];
_name = rhs._name + " (copy)";
for (int i = 0; i < _count ; ++i)
{
_content[i] = rhs._content[i];
}
}
return *this;
}
但是,你不会看到:
destructor ---abc
...因为你的_name
包含"abc"
时不会销毁s1
。
s1
当它超出范围在闭幕式被破坏}
,这就是为什么你看到第二个析构函数调用。 使用您的代码,这会s1._content
调用s1._content
的delete[]
(它会在临时删除,您会记得)。 这可能会导致程序结束时发生崩溃。
我在我的赋值运算符中为_name
添加了" (copy)"
来帮助说明这里发生了什么。
还请看一下什么是复制和交换习语? ,这是一个处理带有原始指针的类的非常简洁的方法。 这也会产生你想要的输出,因为_name
为"abc"
的s1
实例会被swap
掉并被销毁。 我已经在这里实现了这一点 ,还有一些其他的小改进,以便您可以看到它正常工作。
注意 :创建类实例的规范方法是:
Sequence s1; // Default constructor. Do not use parentheses [http://www.parashift.com/c++-faq-lite/ctors.html#faq-10.2]!
Sequence s2(3, "s2") // Constructor with parameters
C ++对象与Java对象有很大的不同,并且在C ++新手中遇到了一个常见的混淆点。 这是发生了什么:
Sequence s1 = Sequence();
这创建了一个新的Sequence,s1,带有默认构造函数(编辑:至少这是上面打印输出中发生的情况,尽管有几位评论者指出,创建一个临时序列然后通过分配给s1完全有效)而是复制构造函数)。
s1.show();
这将在s1上打印数据。
s1 = Sequence(3,"s1");
这是事情变得有点混乱的地方。 在这种情况下,会发生以下情况:
接下来,最后一个
s1.show();
再次调用原始s1对象上的show(),但其数据现在是匿名数据的副本。
最后,s1超出范围,并被删除。
如果您希望对象的行为更像Java对象,则需要将它们作为指针处理,例如
Sequence *s1 = new Sequence(); // constructor
s1->show(); // calling a method on a pointer
delete s1; // delete the old one, as it is about to be assigned over
s1 = new Sequence(3,"s1"); // assign the pointer to a new Sequence object
s1->show();
delete s1;
如果您想让内存管理更容易一些,请查看boost :: shared_ptr,它提供引用计数(而非垃圾收集)自动内存管理。
尽可能简单:
Sequence s1 = Sequence()
:默认构造的Sequence(不是复制构造函数),没有临时的,没有析构函数被调用。
s1.show()
:在s1._content
打印值。
s1 = Sequence(3,"s1");
:创建临时,使用隐式复制构造函数将值分配给s1。 删除临时,调用析构函数,从而使s1
和临时中的指针(_content)无效。
s1.show()
:未定义的行为,因为它是从无效指针打印的。
然后当s1超出范围时,它会尝试删除s1._content
; 更多未定义的行为。
这条线:
Sequence s1 = Sequence();
构造一个临时对象,并使用Sequence
的复制构造函数将其复制到s1
。 然后它调用临时的析构函数。 由于您没有编写复制构造函数,因此匿名对象成员的字节将复制到新的成员字节中,即s1
。 然后临时对象超出范围并调用析构函数。 析构函数打印名称并删除s1
也拥有的内存,所以现在s1
拥有一些deleted[]
ed内存。
然后你做
s1 = Sequence(3,"s1");
它使用赋值运算符将匿名Sequence
分配给s1
。 同样在这里,匿名对象超出范围并且析构函数被调用,并且s1
仍然拥有指向被破坏的内存的指针。
要解决此问题,您需要定义复制构造函数和赋值运算符:
Sequence::Sequence(const Sequence& rhs) : _name(rhs._name), _count(rhs._count), _content(new int[_count]) {
for (int i = 0; i < _count; ++i)
_content[i] = rhs._content[i];
}
Sequence& operator=(const Sequence& rhs) {
if (&rhs != this) {
delete[] _content;
_count = rhs._count;
_name = rhs._name;
_content = new int[_count];
for (int i = 0; i < _count; ++i)
_content[i] = rhs._content[i];
}
return *this;
}
原因是当你复制一个Sequence
,新的Sequence
不需要复制旧Sequence
所持有的指针(并指向同一块内存),而是创建一个新的内存块。本身并将旧Sequence
的内存块中的所有数据复制到新内存块中。
在该代码中可能有几个新的概念,所以研究一下,当你不理解某些东西时问问题。
Sequence s1 = Sequence();
这会创建两个Sequence
对象。 第一个是由Sequence()
创建的。 第二个是由Sequence s1
创建的(通过复制构造)。 或者,换句话说,这相当于:
const Sequence &temp = Sequence();
Sequence s1 = temp;
Sequence s1
不会创建对象的引用。 它创建了一个对象 。 完全成型。 你可以做:
Sequence s1;
s1.show();
这完全没问题。
如果要调用非默认构造函数,只需执行以下操作:
Sequence s2(3,"s1");
要了解问题的来源,请回顾此版本:
const Sequence &temp = Sequence();
Sequence s1 = temp;
您创建一个Sequence
对象。 这会导致构造函数使用new
分配数组。 精细。
第二行获取临时Sequence
对象并将其复制到s1
。 这称为“复制分配”。
由于您没有定义复制赋值运算符,这意味着C ++将使用默认的复制算法。 这只是一个字节副本(它还会触发类成员的副本分配)。 因此,不是Sequence
调用其构造函数,而是从临时temp
中将数据复制到其中。
这是问题所在。 在您的原始代码中,您使用Sequence()
创建的临时代码? 该声明结束时会被销毁 。 它的存在时间足以将其内容复制到s1
,然后将其销毁 。
销毁意味着它的析构函数被调用。 它的析构函数将删除该数组。
现在想想发生了什么。 临时存在并分配了一个数组。 指向此数组的指针被复制到s1
。 然后临时被破坏,导致数组被释放。
这意味着s1
现在拥有一个指向解除分配数组的指针。 这就是为什么裸指针在C ++中很糟糕的原因。 请改用std::vector
。
另外,不要像这样使用复制初始化。 如果您只想要一个Sequence s1
,请简单地创建它:
Sequence s1;
让我解释一下主函数中会发生什么:
Sequence s1 = Sequence();
执行此单行后发生了几件事:
Sequence()
还会创建一个具有默认ctor的未命名临时序列对象。 所以到这个时候,你的输出窗口应该有:析构函数--- abc
s1.show(); this shows the garbage data to the output window:
-572662307 -572662307 -572662307 -572662307 -572662307 -572662307 -572662307 -57 2662307 -572662307 -572662307
类似地, s1 = Sequence(3,"s1");
还会创建一个临时对象并将所有数据复制到s1中。 现在s1._name是“s1”,s1._count是3,s1._content指向为临时对象的_content指针分配的内存块。
到这个时候,你将拥有:
destructor ---abc // first temp object
-572662307 -572662307 -572662307 -572662307 -572662307 -572662307 -572662307 -57
2662307 -572662307 -572662307 // first s1.show()
destructor ---s1 // second temp object
由于同样的原因,第二个s1.show()
也给你垃圾数据,但count = 3。
当所有这些完成后,在main函数的末尾,s1对象被破坏。 这将导致您尝试删除已经解除分配的内存(已在第二个临时对象的析构函数中删除)的问题。
你之所以看到我的不同输出的原因可能是你的编译器“智能”足以消除使用默认复制构造函数构建临时对象。
希望这可以帮助。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.