关于C ++析构函数

时间:2011-08-07 02:57:36

标签: c++ destructor

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

6 个答案:

答案 0 :(得分:6)

如果你提高编译器的警告级别,你会得到一个暗示,你的类包含指针,但你没有定义Sequence(const Sequence&)operator=(const Sequence&)(参见What is The Rule of Three?)。

因为您没有提供复制构造函数或赋值运算符,所以编译器会为您提供这些操作,它们执行成员分配。

当您调用s1 = Sequence(3,"s1");时,您正在执行以下操作(这对Java开发人员来说可能是意外的):

  • 使用“s1”作为名称
  • 创建一个新的,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");

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

  1. 使用参数3“s1”
  2. 构建新的匿名Sequence对象
  3. 使用operator =(复制运算符)
  4. 将此匿名对象(按值)复制到s1
  5. 匿名Sequence对象超出范围,并被删除
  6. 接下来,最后一次

    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();

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

  1. s1是使用默认的ctor。
  2. 创建的 右侧的
  3. Sequence()还会创建一个带有默认ctor的未命名临时序列对象。
  4. 使用编译器提供的默认operator = function将temp对象复制到s1。因此,s1的每个成员字段包含临时对象的相同值。请注意,_content指针也会被复制,因此s1._content指向为temp对象的_content指针动态分配的数据。
  5. 然后,临时对象被破坏,因为它超出了它的范围。这导致临时对象的_content指针上的内存释放。但是,正如3中所提到的,s1._content指向此内存块,此释放导致s1._content现在指向已释放的内存块,这意味着您在此内存块中获得了垃圾数据。
  6. 所以到目前为止,你的输出窗口应该有: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对象被破坏。这将导致您尝试删除已经解除分配的内存(已在第二个临时对象的析构函数中删除)的问题。

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

    希望这有帮助。