8

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();
}
fhcimolin
  • 565
  • 1
  • 5
  • 22
zhangcheng
  • 151
  • 2
  • 7
  • 5
    If you're a beginner at C++, please consider picking up [a good introductory C++ book](http://stackoverflow.com/questions/388242/the-definitive-c-book-guide-and-list), which will cover questions like these and much more. – In silico Aug 07 '11 at 03:11
  • Just asking out of curiosity...why moving from Java to C++ ? Any specific reason ? – Arunmu Aug 07 '11 at 05:40
  • 2
    thanks, I am reading a book about c++,and I'm not moving from Java to C++.C++ is learned for my interest . – zhangcheng Aug 07 '11 at 06:34
  • 2
    For a beginner, I would recommend learning C++ instead of some C soup with objects. For example: do not use a dynamically allocated array, use `std::vector`. Sound, correct, efficient: anything you could wish for :) – Matthieu M. Aug 07 '11 at 10:59

6 Answers6

6

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 swapped out and destroyed. I've implemented this here, along with a few other little improvements so that you can see it working.

N.B: 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
Community
  • 1
  • 1
Johnsyweb
  • 121,480
  • 23
  • 172
  • 229
  • When do you think destructor gets called ? In the end ? or during s1 = Sequence(3,"s1"); ? – Shash316 Aug 07 '11 at 03:22
  • 2
    Stack-allocated objects in C++ get destroyed when they fall out of scope. What this means is that if they are declared as a variable, they are destroyed when the block they were declared in ends, and if they are an anonymous/temporary, they are destroyed when the statement they were created inside ends. – fluffy Aug 07 '11 at 03:34
  • 2
    As per my answer, the unnamed temporary is destroyed immediately after the assignment. `s1` is destoryed at the end of the scope. – Johnsyweb Aug 07 '11 at 03:36
  • Some other answers said there is a temporary created when "Sequence s1 = Sequence();", and temporary will be destroyed before "s1 = Sequence(3."s1");", destructor will be called three times. So is there really a temporary is created when "Sequence s1 = Sequence();" is executed? – zhangcheng Aug 07 '11 at 07:14
  • 1
    @zhangcheng: Good question! `Sequence s1 = Sequence();` is likely to be optimised to `Sequence s1`, which is why you probably didn't see a third call to the destructor. – Johnsyweb Aug 07 '11 at 07:17
3

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, e.g.

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.

fluffy
  • 4,460
  • 28
  • 58
  • -1: `Sequence s1 = Sequence()` doesn't use the default constructor for `s1`, it constructs a temporary object with the default constructor and uses the copy constructor for `s1`. Because he doesn't have a copy constructor defined, it does a bytewise copy then calls the temporaries' destructor. – Seth Carnegie Aug 07 '11 at 03:08
  • 1
    That is completely untrue, and provable even by this example, as if that were the case then the destructor would be printed another time at the very beginning, as the anonymous temporary gets destroyed. In C++, MyType foo(bar) and MyType foo=MyType(bar) are equivalent. It's a special case. – fluffy Aug 07 '11 at 03:18
  • 4
    @fluffy: [Not necessarily](http://stackoverflow.com/questions/5999522/c-default-constructor/5999549#5999549). Technically a temporary may be produced, but most compilers optimize it away. It's not the case in general. – In silico Aug 07 '11 at 03:21
  • Ah, thanks, I guess the answer is more complicated than I thought. Still, that seems like a fairly minor sticking point regarding my answer as a whole. – fluffy Aug 07 '11 at 03:31
  • @fluffy: I would still avoid allocating the memory dynamically, people coming from Java should be shown NOT to use `new` to create objects. – Matthieu M. Aug 07 '11 at 10:58
  • It's kind of hard to know where to start answering this question in a way that's useful, correct, and not going to make the asker run away screaming. I decided to take a baby-steps approach. Perhaps the only winning move was not to play. – fluffy Aug 07 '11 at 22:13
2

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.

deceleratedcaviar
  • 4,619
  • 2
  • 36
  • 61
1

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 Sequences 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.

Seth Carnegie
  • 70,115
  • 19
  • 169
  • 239
  • if it calls the temporary's destructor before s1=Sequence(3,"s1"),why destructor didn't print "destructor ---abc" – zhangcheng Aug 07 '11 at 03:30
  • 1
    @zhangcheng most compilers will optimize the copy away. However, in some situations they will not, so make sure you **always** define the "big 3": Default constructor, Copy constructor, and Assignment operator. – Seth Carnegie Aug 07 '11 at 03:35
  • 1
    Thanks, what I confused is should I always define the "big 3",because this is different from Java, and now I know the answer is yes. – zhangcheng Aug 07 '11 at 03:48
  • 2
    @zhangcheng: Only classes which contain pointers need to define copy constructor, assignment operator, and destructor. For most types, the ones generated by the compiler are just fine. Also read about RAII and the Single Responsibility Principle. – Ben Voigt Aug 07 '11 at 03:52
  • @Ben wouldn't it be more acurate to say "any class which contains any non-automatically managed resource needs to define a ..."? What you say about pointers is true though, because pointers make up a lot of the "non-automatically managed resource" category. – Seth Carnegie Aug 07 '11 at 03:54
  • 1
    @Seth: I wouldn't use the term "managed resource" because that already has meaning from languages like Java and C#. But yes, things like native OS file descriptors should be wrapped the same way. All that should be covered if he reads about RAII and SRP as I suggested. – Ben Voigt Aug 07 '11 at 04:01
1
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;
Nicol Bolas
  • 378,677
  • 53
  • 635
  • 829
  • 1
    The person asking the question is new to C++, and this is already an incredibly complicated subject. That level of pedantry only would serve to confuse him. I tried to explain it in a way that would make sense to him while introducing as few unexpected concepts as possible. Oh, and it's perfectly legal C++ to create a const reference to a temporary, even in C++98. – Nicol Bolas Aug 07 '11 at 03:19
  • it would confuse him more when different functions than he thought should be called are called. Plus the reference to a temporary. – Seth Carnegie Aug 07 '11 at 03:20
  • He already has different functions than he thought being called. The key is to get him to stop using Java-style programming, and to explain why naked pointers in objects like that are bad. Not to explain what copy constructors are, what copy assignment is, when they get called, when the compiler substitutes construction for assignment, and other esoteric C++-isms that he is not even remotely prepared for. The mark of a good teacher is knowing what _not_ to say. – Nicol Bolas Aug 07 '11 at 03:24
  • 1
    another mark of a good teacher is simplifying things in ways that aren't wrong. Maybe you could think of another example that couldn't cause confusion, I'll definitely remove my downvote when it's fixed. – Seth Carnegie Aug 07 '11 at 03:27
  • +1 because this is the clearest answer; and although true, I still think for a newbie the `Sequence s1 = temp` is a good way to explain the concept, even though the syntax is not correct. – deceleratedcaviar Aug 07 '11 at 03:53
  • @Seth: Your first comment is total rubbish. Please read about the difference between direct initialization, copy initialization, and assignment, and also about lifetime extension by const references. `Sequence s1 = temp;` is copy initialization, and does NOT use the assignment operator. – Ben Voigt Aug 07 '11 at 03:55
  • @Nicol: They aren't equivalent, because in `Sequence s1 = Sequence();`, the temporary (if not optimized away) is destroyed at the end of the statement. But with `const Sequence& temp = Sequence(); Sequence s1(temp);`, `temp` is destroyed after `s1` (according to the reverse order of construction rule). – Ben Voigt Aug 07 '11 at 03:57
  • @Ben thank you very much for telling me about lifetime extension by const references. I didn't see Nicol's edit to his first comment. I don't see how I was wrong about the difference between direct initialization, copy initialization, and assignment though. Could you go into that? – Seth Carnegie Aug 07 '11 at 04:00
  • @Seth: Just looking at your comment, you said "one uses the copy constructor and the other uses the assignment operator." Which do you think uses the assignment operator? – Ben Voigt Aug 07 '11 at 04:02
  • @Ben this always happens when I don't read something long enough. I misread `Sequence s1 = temp;` somehow as `s1 = temp`. So the example isn't equivalent, but it's unequivalent in a different way than I misread. I'll go ahead and remove my downvote, thought I hope Nicol considers changing the example slightly. – Seth Carnegie Aug 07 '11 at 04:05
  • @Ben but I had no idea about lifetime extension by const reference, thanks again for that. – Seth Carnegie Aug 07 '11 at 04:06
  • @Nicol it won't let me remove my downvote until you edit the answer. Please @ me when you do it – Seth Carnegie Aug 07 '11 at 04:07
  • @Seth: Fair enough. I've certainly skimmed over important details before myself. – Ben Voigt Aug 07 '11 at 04:12
1

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.

BrandonSun
  • 129
  • 3