Wrox Press C++ Tutorial


Class Destructors

Although this section heading refers to destructors, it's also about dynamic memory allocation. Allocating memory in the free store for class members can only be managed with the aid of a destructor, in addition to a constructor of course, and, as you'll see, using dynamically allocated class members will require you to write your own copy constructor.

What is a Destructor?

A destructor is a function that destroys an object when it is no longer required or when it goes out of scope. It's called automatically when an object goes out of scope. Destroying an object involves freeing the memory occupied by the data members of the object (except for static members, which continue to exist even when there are no class objects in existence). The destructor for a class is a member function with the same name as the class, preceded by a tilde ~. The class destructor doesn't return a value and doesn't have parameters defined. For the class Box, the prototype of the class destructor is:

~Box();            // Class destructor prototype

It's an error to specify a return value or parameters for a destructor.

The Default Destructor

All the objects that we have been using up to now have been destroyed automatically by the default destructor for the class. This is generated by the compiler in the absence of any explicit destructor being provided with a class. The default destructor doesn't delete objects or object members that have been allocated in the free store by the operator new. You must explicitly use the delete operator to destroy objects that have been created using the operator new, just as you would with ordinary variables. If you decide to allocate memory for members of an object dynamically, you must use the operator delete to implement a class destructor which frees any memory that was allocated by the operator new.

Try It Out - A Simple Destructor

We need some practice in writing our own destructor. First, to show when the destructor is called, we can include a destructor in the class Box. The class definition in this example is based on the last example in the previous chapter, Ex6_12.cpp.

// Ex7_01.cpp
// Class with an explicit destructor
#include <iostream>
using namespace std;

class Box              // Class definition at global scope
{
   public:
      // Destructor definition
      ~Box()
      { cout << "Destructor called." << endl; }

      // Constructor definition
      Box(double lv=1.0, double bv=1.0, double hv=1.0)
      {
         cout << endl << "Constructor called.";
         length = lv;  // Set values of
         breadth = bv; // data members
         height = hv;
      }

      // Function to calculate the volume of a box
      double Volume()
      {
         return length * breadth * height;
      }

      // Function to compare two boxes which 
      // returns true if the first is greater 
      // than the second, and false otherwise
      int compare(Box* pBox)
      {
         return this->Volume() > pBox->Volume();
      }

   private:
      double length;    // Length of a box in inches
      double breadth;   // Breadth of a box in inches
      double height;    // Height of a box in inches
};

int main()
{
   Box Boxes[5];             // Array of Box objects declared
   Box Cigar(8.0, 5.0, 1.0); // Declare Cigar box
   Box Match(2.2, 1.1, 0.5); // Declare Match box

   // Initialize pointer to Cigar object address
   Box* pB1 = &Cigar; 
   Box* pB2 = 0;            // Pointer to Box initialized to null

   cout << endl 
        << "Volume of Cigar is "
        << pB1->Volume();    // Volume of obj. pointed to

   pB2 = Boxes;                     // Set to address of array
   Boxes[2] = Match;                // Set 3rd element to Match

   cout << endl                     // Now access thru pointer
        << "Volume of Boxes[2] is " << (pB2 + 2)->Volume();

   cout << endl;
   return 0;
}

How It Works

The only thing that our destructor does is to display a message showing that it was called. The output is:

We get one call of the destructor at the end of the program for each of the objects that exist. For each constructor call that occurred, there's a matching destructor call. We don't need to call the destructor explicitly - when an object of a class goes out of scope, the compiler will arrange for the destructor for the class to be called automatically. In our example, the destructors are called after main() has finished executing, so if there's an error in a destructor, it's quite possible for a program to crash after main() has safely terminated.

Destructors and Dynamic Memory Allocation

You will find that you often want to allocate memory for class data members dynamically. We can use the operator new in a constructor to allocate space in memory for an object member. In such a case, we must assume responsibility for deleting this space by providing a suitable destructor. Let's first define a simple class where we can do this.

Suppose we want a class where each object is a message of some description, for example, a text string. We want the class to be as memory efficient as possible, so, rather than defining a data member as a char array big enough to hold the maximum length string that we might require, we'll allocate memory on the free store for a message when an object is created. Here's the class definition:

//Listing 02_01
class Message
{
   private:
      char* pmessage;    // Pointer to object text string

   public:
      // Function to display a message
      void ShowIt(void)
      {
         cout << endl << pmessage;
      }

      // Constructor definition
      Message(const char* text = "Default message")
      {
         // Allocate space for text
         pmessage = new char[strlen(text)+1];
         // Copy text to new memory
         strcpy(pmessage, text);
      }

      ~Message();        // Destructor prototype
};

This class has only one data member defined, pmessage, which is a pointer to a text string. This is defined in the private section of the class, so that it can't be accessed from outside the class.

In the public section, we have a function ShowIt(), which will output a Message object to the screen. We also have the definition of a constructor and we have the prototype for the class destructor, ~Message(), which we'll come to in a moment.

The constructor for the class requires a string as an argument, but if none is passed, it uses the default value specified. The constructor obtains the length of the string supplied as an argument, excluding the terminating NULL, using the function strlen(). For the constructor to use this library function, there must be a #include statement for the header file string. By adding 1 to the value that the function strlen() returns, the constructor defines the number of bytes of memory necessary to store the string in the free store.

We're assuming that we have our own function to handle out-of-memory conditions, so we don't bother to test the pointer returned for NULL. (See Chapter 5 for information on handling out-of-memory conditions.)

Having obtained the memory for the string using the operator new, we use the strcpy() function, which is also declared in the header file string, to copy the string supplied as an argument into the memory allocated for it. This function copies the string specified by the second pointer argument to the address contained in the first pointer argument.

We now need to write a class destructor that will free up the memory allocated for a message. If we don't provide this, there's no way to delete the memory allocated for an object. If we use this class in a program, where a large number of Message objects are created, the free store will be gradually eaten away until the program fails. It's easy for this to happen almost invisibly. For example, if you create a temporary Message object in a function which is called many times in a program, you might assume that the objects are being destroyed at the return from the function. You'd be right about that, of course, but the free store memory will not be released.

The code for the destructor is as follows:

// Listing 02_02
// Destructor to free memory allocated by new
Message::~Message()
{
   cout << "Destructor called." // Just to track what happens
        << endl;
   delete[] pmessage;         // Free memory assigned to pointer
}

Because we're defining it outside of the class definition, we need to qualify the name of the destructor with the class name, Message, and the scope resolution operator. All the destructor does is display a message so that we can see what's going on and use the operator delete to free the memory pointed to by the member pmessage. Note that we must include the square brackets with delete because we're deleting an array (of type char).

Try It Out - Using the Message Class

We can exercise this class with a little example:

// Ex7_02.cpp
// Using a destructor to free memory
#include <iostream>               // For stream I/O
#include <string>                 // For strlen() and strcpy()
using namespace std;

// Put the Message class definition here (Listing 02_01)

// Put the destructor definition here (Listing 02_02)

int main()
{
   // Declare object
   Message Motto("A miss is as good as a mile.");

   // Dynamic object
   Message* pM = new Message("A cat can look at a queen.");

   Motto.ShowIt();    // Display 1st message
   pM->ShowIt();      // Display 2nd message
   cout << endl;

   // delete pM;      // Manually delete object created with new

   return 0;
}

How It Works

At the beginning of main(), we declare and define an initialized Message object, Motto, in the usual manner. In the second declaration we define a pointer to a Message object, pM, and allocate memory for the Message object pointed to by using the operator new. The call to new invokes the Message class constructor, which has the effect of calling new again to allocate space for the message text pointed to by the data member pmessage. If you build and execute this example, it will produce this:

We have only one destructor call, even though we created two message objects. We said earlier that the compiler doesn't take responsibility for objects created in the free store. The compiler arranged to call our destructor for the object Motto because this is a normal automatic object, even though the memory for the data member was allocated in the free store by the constructor. The object pointed to by pM is different. We allocated memory for the object in the free store, so we have to use delete to remove it. You need to uncomment the statement,

// delete pM;     // Manually delete object created with new

which appears just before the return statement in main(). If you run the code now, it will produce this:

Now we get an extra call of our destructor. This is surprising in a way. Clearly, delete is only dealing with the memory allocated by the call to new in the function main(). It only freed the memory pointed to by pM. Since our pointer to pM is a pointer to a Message object (for which a destructor has been defined), delete also calls our destructor to allow us to clean up the details of the members of the object. So when you use delete for an object created dynamically with new, it will always call the destructor for the object allocated on the free store.

Implementing a Copy Constructor

When you allocate space for class members dynamically, there are demons lurking in the free store. For our class Message, the default copy constructor is woefully inadequate. If we have these statements,

Message Motto1("Radiation fades your genes.");
Message Motto2(Motto1);

the effect of the default copy constructor will be to copy the address in the pointer member from Motto1 to Motto2. Consequently, there will be only one text string shared between the two objects, as illustrated in the diagram below:

If the string is changed from either of the objects, it will be changed for the other as well. If Motto1 is destroyed, the pointer in Motto2 will be pointing at a memory area which may now be used for something else, and chaos will surely ensue. Of course, the same problem arises if Motto2 is deleted; Motto1 would then contain a member pointing to a nonexistent string.

The solution is to supply a class copy constructor to replace the default version. This could be implemented in the public section of the class as follows:

Message(const Message& initM)     // Copy Constructor definition
{
   // Allocate space for text
   pmessage = new char[ strlen(initM.pmessage) + 1 ];
   // Copy text to new memory
   strcpy(pmessage, initM.pmessage);
}

You will remember from the previous chapter that, to avoid an infinite spiral of calls to the copy constructor, the parameter must be specified as a const reference. This copy constructor first allocates enough memory to hold the string in the object initM, storing the address in the data member of the new object, and then copies the text string from the initializing object. Now, our new object will be identical to, but quite independent of, the old one.

Just because you don't initialize one Message class object with another, don't think that you're safe and need not bother with the copy constructor. Another monster lurks in the free store. Consider the following statements,

Message Thought("Eye awl weighs yews my spell checker.");
DisplayMessage(Thought); // Call a function to output a message

where the function DisplayMessage() is defined as:

void DisplayMessage(Message LocalMsg)
{
   cout << endl << "The message is: ";
   LocalMsg.ShowIt ();
   return;
}

Looks simple enough doesn't it? What could be wrong with that? A catastrophic error, that's what! What the function DisplayMessage() does is actually irrelevant. The problem lies with the argument. The argument is a Message object, which is passed by value. With the default copy constructor, the sequence of events is as follows:

  1. The object Thought is created with the space for the message "Eye awl weighs yews my spell checker" allocated in the free store.

  2. The function DisplayMessage() is called and, because the argument is passed by value, a copy, LocalMsg, is made using the default copy constructor. Now the pointer in the copy points to the same string in the free store as the original object.

  3. At the end of the function, the local object goes out of scope, so the destructor for the Message class is called. This deletes the local object (the copy) by deleting the memory pointed to by the pointer pmessage.

  4. On return from the function DisplayMessage(), the pointer in the original object, Thought, still points to the memory area that has just been deleted. Next time you try to use the original object (or even if you don't, since it will need to be deleted sooner or later) your program will behave in weird and mysterious ways.

Any call to a function that passes by value an object of a class that has a member defined dynamically will cause problems. So, out of this, we have an absolutely 100 percent, 24 carat golden rule:

If you allocate space for a class member dynamically, always implement a copy constructor.


© 1998 Wrox Press