关于C++析构函数

About C++ destructors

本文关键字:析构函数 C++ 关于      更新时间:2023-10-16

我有一些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");

这就是事情变得有点混乱的地方。 在这种情况下,发生的情况如下:

  1. 使用参数 3,"s1" 构造一个新的匿名序列对象
  2. 此匿名对象使用运算符=(复制运算符((按值(复制到 s1
  3. 匿名 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();

执行这一行后发生了几件事:

  1. S1 是使用默认 CTOR 创建的。
  2. 右侧的Sequence()还会创建一个具有默认 ctor 的未命名临时序列对象。
  3. 临时对象使用编译器提供的默认运算符 = 函数复制到 S1。因此,s1 的每个成员字段都包含相同的临时对象值。请注意,_content指针也会被复制,因此s1._content指向动态分配给 temp 对象的_content指针的数据。
  4. 然后,临时对象被销毁,因为它超出了其范围。这会导致临时对象的指针上的内存释放_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 对象被销毁。这将导致您尝试删除已经释放的内存(已在第二个临时对象的析构函数中删除(的问题。

您看到与我的输出不同的原因可能是您的编译器足够"智能",可以消除使用默认复制构造函数构造临时对象的构造。

希望这有帮助。