关于C++析构函数
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 个数组。 - 将
si._count
设置为3
- 将
si._name
设置为"s1"
- 将
-
然后临时(而不是
s1
(被销毁(在上面的实际输出中,您看到"s1"被销毁两次(,留下_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
。因为您不会在其_name
包含"abc"
时破坏s1
.
当s1
在关闭}
超出范围时,它会被销毁,这就是您看到第二个析构函数调用的原因。使用您的代码,这会再次调用delete[]
s1._content
(您会记得,它是临时删除的(。这可能会导致程序结束时崩溃。
我在赋值运算符中添加了" (copy)"
_name
,以帮助说明此处发生的情况。
也请看一下什么是复制和交换习语?,这是处理带有原始指针的类的一种非常简洁的方法。这也将生成您想要的输出,因为具有_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();
这将创建一个带有默认构造函数的新序列 s1(编辑:至少这是上面打印输出中发生的事情,尽管正如一些评论者指出的那样,创建一个临时序列是完全有效的,然后通过复制构造函数分配给 s1(。
s1.show();
这将在 s1 上打印数据。
s1 = Sequence(3,"s1");
这就是事情变得有点混乱的地方。 在这种情况下,发生的情况如下:
- 使用参数 3,"s1" 构造一个新的匿名序列对象
- 此匿名对象使用运算符=(复制运算符((按值(复制到 s1
- 匿名 Sequence 对象超出范围,并被删除
下一个,最后一个
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()
:默认构造的序列(不是复制构造函数(,没有临时的,没有调用析构函数。
s1.show()
:以s1._content
打印值。
s1 = Sequence(3,"s1");
:创建一个临时的,使用隐式复制构造函数将值分配给 s1。删除临时,调用析构函数,从而使指针 (_content( 在 s1
和临时中失效。
s1.show()
:未定义的行为,因为它是从无效指针打印的。
然后当 s1 超出范围时,它会尝试删除s1._content
;更多未定义的行为。
行:
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
的内存块复制到新内存块。
该代码中可能有几个新概念,因此请研究一段时间,并在不理解某些内容时提出问题。
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();
执行这一行后发生了几件事:
- S1 是使用默认 CTOR 创建的。
- 右侧的
Sequence()
还会创建一个具有默认 ctor 的未命名临时序列对象。 - 临时对象使用编译器提供的默认运算符 = 函数复制到 S1。因此,s1 的每个成员字段都包含相同的临时对象值。请注意,_content指针也会被复制,因此s1._content指向动态分配给 temp 对象的_content指针的数据。
- 然后,临时对象被销毁,因为它超出了其范围。这会导致临时对象的指针上的内存释放_content。但是,由于如 3 中所述,s1._content指向此内存块,因此此释放会导致s1._content现在指向已解除分配的内存块,这意味着您在此内存块中获得了垃圾数据。
所以到这个时候,你的输出窗口应该有:析构函数---abc
s1.show(); this shows the garbage data to the output window:
-572662307 -572662307 -572662307 -572662307 -572662307 -572662307 -572662307 -572662307 -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
出于同样的原因,第 2 s1.show()
也为您提供垃圾数据,但 count = 3。
完成所有这些操作后,在主函数结束时,s1 对象被销毁。这将导致您尝试删除已经释放的内存(已在第二个临时对象的析构函数中删除(的问题。
您看到与我的输出不同的原因可能是您的编译器足够"智能",可以消除使用默认复制构造函数构造临时对象的构造。
希望这有帮助。
- 什么时候调用组成单元对象的析构函数
- 如果C++类在类方法中具有动态分配,但没有构造函数/析构函数或任何非静态成员,那么它仍然是POD类型吗
- 内联映射初始化的动态atexit析构函数崩溃
- 什么时候调用析构函数
- 优先顺序:智能指针和类析构函数
- C++-明确何时以及如何调用析构函数
- 使用基类指针创建对象时,缺少派生类析构函数
- 在c++中使用向量时,如何调用构造函数和析构函数
- 重载运算符new[]的行为取决于析构函数
- 我需要知道编译器如何在cpp中使用析构函数
- 为什么在使用转换构造函数赋值后调用C++类的析构函数?
- 析构函数调用
- 通过引用传递-为什么要调用这个析构函数
- 对具有动态分配的内存和析构函数的类对象的引用
- 重载 -> shared_ptr 个实例中的箭头运算符<interface>,接口中没有纯虚拟析构函数
- C++成员的析构函数顺序与shared_ptr
- C++ 防止在映射中放置()时调用析构函数
- 在这种情况下显式调用时,std::cout 如何更改析构函数的行为?
- 调用析构函数以释放动态分配的内存
- 不命名构造函数和析构函数上的类型错误