简体   繁体   中英

About C++ destructors

I have some java experience and am a beginner on C++.

below is my code, its output is:

0 1 2 3 4 5 6 7 8 9
destructor ---s1
8791616 8785704 2
destructor ---s1

I expected the following output:

0 1 2 3 4 5 6 7 8 9
destructor ---abc
0 1 2
destructor ---s1

I can't understand why the destructor releases the first object's resource. How can I print the output I expected?

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

If you increase the warning level on your compiler, you'll get a hint that your class contains pointers but you're not defining Sequence(const Sequence&) or operator=(const Sequence&) (see What is The Rule of Three? ).

Because you don't provide the copy constructor or assignment operator, the compiler provides these for you, which perform member-wise assignment.

When you call s1 = Sequence(3,"s1"); , you are doing the following (this may be unexpected to a Java developer):

  • Creating a new, temporary, Sequence of three with "s1" as its name
  • Assigning this to s1 , which:
    • sets si._content to be the a pointer to the new array of three ints just created, leaking the old one of 10.
    • sets si._count to 3
    • sets si._name to "s1"
  • The temporary (and not s1 ) is then destroyed (in your actual output above, you see "s1" being destroyed twice), leaving _content pointing to free'd memory (which is why you see garbage on the second call to s1.show() ).

If you declare an assignment operator like this, you'll get something closer to your expected output:

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;
}

You won't, however, see:

destructor ---abc

...because you don't destroy s1 while its _name contains "abc" .

s1 is destroyed when it goes out of scope at the closing } , which is why you see the second destructor call. With your code, this calls delete[] on s1._content a second time (it was deleted under the temporary, you'll recall). This is likely to result in a crash right at the end of your program.

I added " (copy)" to _name in my assignment operator to help to illustrate what is happening here.

Please also take a look at What is the copy-and-swap idiom? , which is a very neat way to deal with classes with raw pointers. This will also generate the output you desire as the instance of s1 with _name of "abc" gets swap ped out and destroyed. I've implemented this here , along with a few other little improvements so that you can see it working.

NB : The canonical way of creating an instance of a class is:

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++ objects are rather different from Java objects, and you're running into a common point of confusion among those new to C++. Here is what's happening:

Sequence s1 = Sequence();

This creates a new Sequence, s1, with the default constructor (EDIT: at least that's what's happening in the printout above, although as several commenters have pointed out, it's perfectly valid for this to create a temporary Sequence which is then assigned to s1 via the copy constructor instead).

s1.show();

This prints the data on s1.

s1 = Sequence(3,"s1");

This is where things get a bit confusing. In this case, what happens is the following:

  1. A new anonymous Sequence object is constructed with the parameters 3,"s1"
  2. This anonymous object is copied (by value) to s1, using operator= (the copy operator)
  3. The anonymous Sequence object falls out of scope, and is deleted

Next, the last

s1.show();

calls show() on the original s1 object again, but its data is now a copy of the anonymous data.

Finally, s1 falls out of scope, and is deleted.

If you want objects that behave more like Java objects, you need to handle them as pointers, eg

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;

If you want to make the memory management a bit easier, look into boost::shared_ptr, which provides reference-counted (rather than garbage-collected) automatic memory management.

As simple as I can:

Sequence s1 = Sequence() : Default constructed Sequence (not copy constructor), no temporary, no destructor called.

s1.show() : Prints the values in s1._content .

s1 = Sequence(3,"s1"); : Creates a temporary, uses the implicit copy constructor to assign the values to s1. Deletes the temporary, calling the destructor, and hence invalidating the pointer (_content) in s1 and the temporary.

s1.show() : Undefined behavior, as it is printing from an invalid pointer.

Then as s1 goes out of scope, it attempts to delete s1._content ; more undefined behavior.

The line:

Sequence s1 = Sequence();

Constructs a temporary object, and, using Sequence 's copy constructor, copies it to s1 . It then calls the temporary's destructor. Since you don't have a copy constructor written, the bytes of the members of the anonymous object are copied into a new one, which is s1 . Then the temporary object goes out of scope and the destructor is called. The destructor prints the name and deletes the memory, which s1 also owns, so now s1 owns some deleted[] ed memory.

Then you do

s1 = Sequence(3,"s1");

Which uses the assignment operator to assign an anonymous Sequence to s1 . Again here, the anonymous object goes out of scope and the destructor is called, and s1 still owns a pointer to the destroyed memory.

To fix this problem, you need to define a copy constructor and an assignment operator:

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;
}

The reason for that is that when you make a copy of a Sequence , the new Sequence needs to not make a copy of the pointer that the old Sequence held (and point to the same block of memory) but create a new block of memory for itself and copy all the data from the old Sequence s block of memory to the new one.

There are probably several new concepts for you in that code, so study it a while and ask questions when you don't understand something.

Sequence s1 = Sequence();

That creates two Sequence objects. The first one is created by Sequence() . The second is created (by copy-construction) by Sequence s1 . Or, to put it another way, this equivalent to:

const Sequence &temp = Sequence();
Sequence s1 = temp;

Sequence s1 doesn't create a reference to an object. It creates an object . Fully formed. You can do:

Sequence s1;
s1.show();

And that's perfectly fine.

If you want to call a non-default constructor, just do this:

Sequence s2(3,"s1");

To understand where the problem comes from, look back at this version:

const Sequence &temp = Sequence();
Sequence s1 = temp;

You create a Sequence object. This causes the constructor to allocate an array with new . Fine.

The second line takes the temporary Sequence object and copies it into s1 . This is called "copy assignment".

Since you did not define a copy assignment operator, this means that C++ will use the default copy algorithm. And that is simply a byte copy (it also triggers the copy assignment of the members of the class). So instead of Sequence having its constructor called, it gets data copied into it from the temporary temp .

Here's the problem. In your original code, the temporary you create with Sequence() ? It is destroyed when that statement ends. It exists long enough for its contents to be copied into s1 , then it is destroyed .

Destruction means that its destructor is called. Its destructor will delete the array.

Now think about what has happened. The temporary came into existence and allocated an array. The pointer to this array was copied into s1 . Then the temporary was destroyed, causing the array to be deallocated.

This means that s1 now holds a pointer to a deallocated array . This is why naked pointers are bad in C++. Use a std::vector instead.

Also, do not use copy initialization like that. If you just want a Sequence s1 , create it simply:

Sequence s1;

Let me explain what happens in your main function:

Sequence s1 = Sequence();

Several things happened after execution this single line:

  1. s1 is created with the default ctor.
  2. Sequence() on the right also creates an unnamed temporary sequence object with default ctor.
  3. the temp object is copied to s1 with the default operator= function provided by compiler. So each member field of s1 contains the same values of the temp object. Note that the _content pointer also get copied, so s1._content points to the data dynamically allocated for the _content pointer of temp object.
  4. Then the temp object is destructed because it's out of its scope. And this causes the memory deallocation on _content pointer of temp object. However, because as mentioned in 3, s1._content points to this memory block, this deallocation causes s1._content now points to a memory block already deallocated, which means you got garbage data in this memory block.

So by this time, your output window should have: destructor ---abc

s1.show(); this shows the garbage data to the output window:

-572662307 -572662307 -572662307 -572662307 -572662307 -572662307 -572662307 -57 2662307 -572662307 -572662307

Similarly, s1 = Sequence(3,"s1"); also creates a temp object and copied all the data into s1. Now s1._name is "s1", s1._count is 3 and s1._content points to the memory block allocated for the _content pointer of temp object.

And by this time, you will have:

destructor ---abc  // first temp object
-572662307 -572662307 -572662307 -572662307 -572662307 -572662307 -572662307 -57
2662307 -572662307 -572662307  // first s1.show()
destructor ---s1  // second temp object

Due to the same reason, 2nd s1.show() also gives you garbage data but with count = 3.

When all these done, at the end of the main function, the s1 object gets destructed. And this will cause the problem that you're trying to delete a memory which already get deallocated (already deleted in destructor of 2nd temp object).

The reason why you saw different output from mine might be your compiler is "smart" enough to eliminate the construction of a temp object with a default copy constructor.

Hope this helps.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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