我有一些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();
}
答案 0 :(得分:6)
如果你提高编译器的警告级别,你会得到一个暗示,你的类包含指针,但你没有定义Sequence(const Sequence&)
或operator=(const Sequence&)
(参见What is The Rule of Three?)。
因为您没有提供复制构造函数或赋值运算符,所以编译器会为您提供这些操作,它们执行成员分配。
当您调用s1 = Sequence(3,"s1");
时,您正在执行以下操作(这对Java开发人员来说可能是意外的):
Sequence
三个
s1
,其中:
si._content
设置为指向刚刚创建的三个ints
的新数组的指针,泄漏旧的10个数组。si._count
设置为3
si._name
设置为"s1"
s1
)(在上面的实际输出中,您会看到“s1”被销毁两次),离开{ {1}}指向free'd内存(这就是为什么你在第二次调用_content
时看到垃圾)。如果您声明这样的赋值运算符,您将获得更接近预期输出的内容:
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
包含s1
时,您不会销毁_name
。
"abc"
在结束s1
时超出范围时会被销毁,这就是您看到第二个析构函数调用的原因。使用您的代码,这会在}
上再次调用delete[]
(它会在临时删除,您会记得)。这可能会导致程序结束时发生崩溃。
我在我的赋值运算符中向s1._content
添加了" (copy)"
,以帮助说明此处发生的情况。
请同时查看What is the copy-and-swap idiom?,这是处理带有原始指针的类的非常简洁的方法。这也会生成您想要的输出,因为_name
s1
_name
"abc"
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
输出并销毁。我已经实现了这个here,以及其他一些小改进,以便您可以看到它正常工作。
N.B :创建类实例的规范方法是:
{{1}}
答案 1 :(得分:3)
C ++对象与Java对象有很大的不同,并且在C ++新手中遇到了一个常见的混淆点。以下是发生的事情:
Sequence s1 = 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,它提供了引用计数(而不是垃圾收集)的自动内存管理。
答案 2 :(得分:2)
尽可能简单:
Sequence s1 = Sequence()
:默认构造的Sequence(不是复制构造函数),没有临时的,没有析构函数被调用。
s1.show()
:打印s1._content
中的值。
s1 = Sequence(3,"s1");
:创建一个临时的,使用隐式复制构造函数将值赋给s1。删除临时,调用析构函数,从而使s1
和临时的指针(_content)无效。
s1.show()
:未定义的行为,因为它是从无效指针打印的。
然后当s1超出范围时,它会尝试删除s1._content
;更多未定义的行为。
答案 3 :(得分:1)
该行:
Sequence s1 = Sequence();
构造一个临时对象,并使用Sequence
的复制构造函数将其复制到s1
。然后它调用临时的析构函数。由于您没有编写复制构造函数,因此匿名对象成员的字节将复制到新的成员s1
中。然后临时对象超出范围并调用析构函数。析构函数打印名称并删除s1
也拥有的内存,因此现在s1
拥有一些deleted[]
内存。
然后你做
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
内存块中的所有数据复制到新内存块。
在该代码中可能有几个新的概念,所以研究一下,当你不理解某些东西时问问题。
答案 4 :(得分:1)
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;
答案 5 :(得分:1)
让我解释一下你的主要功能:
Sequence s1 = Sequence();
执行这一行后发生了几件事:
Sequence()
还会创建一个带有默认ctor的未命名临时序列对象。所以到目前为止,你的输出窗口应该有:destructor --- 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()
也会为您提供垃圾数据,但计数= 3.
当所有这些完成后,在main函数的末尾,s1对象被破坏。这将导致您尝试删除已经解除分配的内存(已在第二个临时对象的析构函数中删除)的问题。
你看到我的不同输出的原因可能是你的编译器“智能”足以消除使用默认复制构造函数构建临时对象。
希望这有帮助。