J Collins
J Collins

Reputation: 2170

c++ Destructors, When and Where if Ever?

I can't help reading the bulk of forum posts on destructors and getting totally confused.

Some say to call the destructor (with delete) once for each call to new. Some say the destructor automatically gets called in a variety of circumstances i.e. when the pointer is reassigned, when the object goes out of scope. Some suggest the pointer going out of scope while being a return value where the object exists as a copy of its former self, (does this then need explicit destruction as it was originally created with a new?

There seems to be some suggestion that calling the same destructor more than once will corrupt memory so all delete calls should be partnered with *pointer = NULL; to avoid corruption. If not then some more advanced object management system would require implementing, or an iron-fisted rigour of ownership.

I can't seem to make any sense of discussions on calling sequence of destructors, i.e. does the call 1) originate at the base superclass and cascade down to the specific class, calling all virtualised destructors on the way, 2) originate at the instantiated class and move up to the superclass, or 3) originate at the particular cast the class has when it goes out of scope, then traverse both toward the instantiated and base class. Do cascading destructors

Ultimately I simply don't know strictly how or when to delete objects if ever, whether objects are responsible for deleting all objects they reference, how to cleanly handle a proper object-oriented deletion routine where an object is referenced multiple times, it's just a mess in my head. As you can see I can't really formulate a single solid question to ask, am really hoping someone can offer a clean and concise discussion of if not the single 'correct' approach, at least the industry best practice to object deletion.

Upvotes: 3

Views: 1665

Answers (3)

6502
6502

Reputation: 114461

The C++ language leaves memory management in the hand of the programmer, that is the reason for which you can find that level of confusion.

Repeating what Luchian Grigore said there are three main types of memory

  • automatic storage (stack)
  • dynamic storage (heap)
  • static storage

If you allocate an object in automatic storage the the object will be destroyed once the scope is terminated; for example

 void foo() {
     MyClass myclass_instance;
     myclass_instance.doSomething();
 }

in the above case when the function terminates myclass_instance is destroyed automatically.

If you instead allocate an object in the heap with new then it's your responsibility to call the destructor with delete.

In C++ also an object can have sub-objects. For example:

class MyBiggerClass {
    MyClass x1;
    MyClass x2;
    ...
};

those sub-objects are allocated in the same memory the containing object is allocated to

void foo() {
    MyBiggerClass big_instance;
    MyBiggerClass *p = new MyBiggerClass();
    ...
    delete p;
}

in the above case the two sub-objects big_instance.x1 and big_instance.x2 will be allocated in automatic storage (stack), while p->x1 and p->x2 are allocated on the heap.

Note however that you don't need in this case to call delete p->x1; (compile error, p->x1 is not a pointer) or delete &(p->x1); (syntactically valid, but logical mistake because that it wasn't allocated explicitly on the heap, but as a sub-object of another object). Deleting the main object p is all that is needed.

Another complication is that an object may keep pointers to other objects instead of including them directly:

class MyOtherBigClass {
    MyClass *px1;
    MyClass *px2;
};

in this case it will be the constructor of MyOtherBigClass that will have to find the memory for the sub-objects and it will be ~MyOtherBigClass that will have to take care of destroying the sub-objects and freeing the memory.

In C++ destroying a raw pointer doesn't automatically destroy the content.

Base classes in simple cases can be seen just as hidden embedded sub-objects. I.e. it's like if an instance of the base object is embedded in the derived object.

class MyBaseClass {
    ...
};

class MyDerivedClass : MyBaseClass {
    MyBaseClass __base__;  // <== just for explanation of how it works: the base
                           //     sub-object is already present, you don't
                           //     need to declare it and it's a sub-object that
                           //     has no name. In the C++ standard you can find
                           //     this hidden sub-object referenced quite often.
    ...
};

This means that the destructor of the derived object doesn't need to call the destructor of the base object because this is taken care by the language automatically. The case of virtual bases is more complex, but still the invocation of base destructors is automatic.

Given that memory management is in the control of the programmer there are a few strategies that have emerged to help programmers avoiding making a mess of intricate code that always ends up in object leaks or multiple destruction.

  1. Plan carefully how you are going to handle lifetime of the instances. You cannot just leave this as an afterthought because it will be impossible to fix later. For every object instance it should be clear who creates and who destroys it.

  2. When it's impossible to plan ahead of time when an object should be destroyed then use reference counters: for every object keep track how many pointers are referencing it and destroy the object once this number reaches zero. There are smart pointers that can take care of this for you.

  3. Never keep around a pointer to an object that has already been destroyed.

  4. Use containers that are classes designed explicitly to handle the lifetime of contained objects. Examples are std::vector or std::map.

Upvotes: 3

Mats Petersson
Mats Petersson

Reputation: 129314

If your code calls new, then it should call delete as well, yes. Except if you are using smart pointers (which will call delete for you when the pointer gets destroyed). Whenever possible, you should use smart pointers and use vector or string to avoid having to manually allocate memory using new - if you don't call new, you don't need to worry about making sure delete is called -> no memory leaks, and no problems with objects being destroyed at the wrong time, etc.

Calling delete multiple times for the same instance is definitely a bad idea.

If we have this:

class A
{
   int *p;
  public:
    A() { p = new int[10]; }
    ~A() { delete [] p; }
};

class B
{
   A a;
   ~B() { ... }
   ... 
};

class C : public B 
{
   ...
   ~C() { ... }
}

...
C *cp = new C;
....
delete cp;

then the destructor of C gets called by delete. The destructor of B is called by the C destructor, and the destructor of A gets called by the B destructor. This is automatic, and the compiler will "make sure this happens".

And if we don't call new:

... 
{
    C c;
    ...
}   // Destructor for C gets called here (and B and A as describe above)

Upvotes: 0

Luchian Grigore
Luchian Grigore

Reputation: 258548

There are 3 types of allocation for which destructors are called in different ways:

Automatic allocation

These objects reside in automatic memory (trivially, the stack):

int main()
{
  A a;
  //...
}

The destructor of a is automatically called when a goes out of scope (closing }).

Dynamic allocation

Objects reside in dynamic memory (the heap). They are allocated with new and in order for the dstructor to be called, you need to call delete:

int main()
{
  A* a = new A;
  delete a;    //destructor called
}

In this case it was probably suggested you should assign NULL to a after the delete. There are two schools of thought regarding this (I personally wouldn't suggest it). The motivation would be that you could possibly call delete again on a and crash the program if you don't set it to NULL. Which is correct. But if you do call delete again, that's already a bug or something wrong in the logic, which shouldn't be masked by making the code appear to run correctly.

Static allocation

Objects reside in static memory. Regardless of where they are allocated, the destructor is automatically called when the program ends:

A a; //namespace scope

int main()
{
}

Here, As destructor is called when the program terminates, after main finishes.

Upvotes: 8

Related Questions