繁体   English   中英

关于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开发人员来说可能是意外的):

  • 使用“s1”作为名称创建一个新的临时三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._contentdelete[] (它会在临时删除,您会记得)。 这可能会导致程序结束时发生崩溃。

我在我的赋值运算符中为_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");

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

  1. 使用参数3“s1”构造新的匿名Sequence对象
  2. 使用operator =(复制运算符)将此匿名对象(按值)复制到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() :默认构造的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();

执行此单行后发生了几件事:

  1. 使用默认ctor创建s1。
  2. 右侧的Sequence()还会创建一个具有默认ctor的未命名临时序列对象。
  3. 使用编译器提供的默认operator = function将temp对象复制到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 -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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM