Destructors in C++



A destructor is a special member function of a class that is executed automatically whenever an object of its class goes out of scope or whenever the delete expression is applied to a pointer to the object of that class.

Note: In C++, the static keyword has a different meaning. In this chapter, we're using static memory allocation for stack-based / non-dynamic memory allocation.

The syntax of defining a destructor manually is given below −

~class_name() {
   // body 
}

Here's the syntax to define a destructor outside the class, but first we need to declare the destructor in the class −

class_name {
   public:
      // Destructor Declaration
     ~class_name(); 
}

// Defining Destructor
class_name :: ~class_name() {
   // Body
}

Defining a Destructor Inside a Class

In this example, we have used the above syntax for defining a destructor manually inside a class. Here, destructor is not needed, we have used destructor just to illustrate how a destructor is defined inside a class −

#include <iostream>
using namespace std;

class Example{
   public:
      // Constructor called
      Example(void)
      {
         cout << "Constructor is called" << endl;
      }

      // Defining destructor inside class
      ~Example(void)
      {
         cout << "Destructor is called" << endl;
      }
};

int main(){
   Example ex;
   return 0;
}

The output of the above code is given below −

Constructor is called
Destructor is called

Defining a Destructor Outside a Class

The example below demonstrates how to define a destructor(Example) outside the class using scope resolution operator. But first, it needs to be declared inside the class.

#include <iostream>
using namespace std;

class Example {
   public:
      // Constructor called
      Example(void)
      {
         cout << "Constructor is called" << endl;
      }
      // Declaring destructor inside class
      ~Example();
};

// Defining destructor outside class using scope resolution operator
Example :: ~Example(void)
{
   cout << "Destructor is called" << endl;
}

int main(){
   Example ex;
   return 0;
}

The output of the above code is given below −

Constructor is called
Destructor is called

Why Do We Need Custom Destructors?

C++ provides a default destructor for every class as the classes are created. The default destructor cleans up the statically allocated memory. But, if we have used a pointer for dynamic memory allocation using new, then we need to define a custom destructor with delete operator to clean up the memory.

In this example, the destructor Example1 is just used for displaying the message and not to free the memory as the memory of class Example1 gets freed automatically. But the destructor Example2 is used for freeing the memory using delete operator.

#include <iostream>
using namespace std;

class Example1 {
   public:
      // Constructor called
      Example1()
      {
         cout << "Constructor of Example1 is called" << endl;
      }

      // Destructor called
      ~Example1()
      {
         cout << "Destructor of Example1 is called" << endl;
      }
};

class Example2 {
   private:
      int *ptr;

   public:
      Example2()
      {
         ptr = new int; // dynamically allocated memory
         *ptr = 42;
         cout << "Constructor of Example2 is called with ptr Value = " << *ptr << endl;
      }

      // Destructor called to free dynamically allocated memory
      ~Example2()
      {
         delete ptr; // dynamically allocated memory is freed
         cout << "Destructor of Example2 is called" << endl;
      }
};

int main(){
   Example1 ex1; // object with static memory allocation
   Example2 ex2; // object with dynamic memory allocation

   return 0;
}

The output of the above code is given below −

Constructor of Example1 is called
Constructor of Example2 is called with ptr Value = 42
Destructor of Example2 is called
Destructor of Example1 is called

Properties of Destructors in C++

Destructors in C++ have the following properties −

  • Destructors are automatically called when an object goes out of scope.
  • Destructor has the same name as the class and is denoted using tilde(~) sign.
  • It is automatically created when a class is created.
  • Destructors do not have any return type and do not accept any parameters/arguments.
  • You can not define more than one destructor.
  • Destructors cannot be inherited.
  • Destructors cannot be overloaded.

Automatic Destructor Call for Statically Allocated Objects

For objects that are created using static memory allocation, destructors are automatically called. Below is an example −

#include <iostream>
using namespace std;

class MyClass {
   public:
      MyClass() {
         cout << "Constructor called" << endl;
      }

      ~MyClass() {
         cout << "Destructor called automatically" << endl;
      }
};

int main() {
   cout << "Creating an object using static memory allocation" << endl;
   {
      MyClass obj;
      cout << "Object created" << endl;
   }  // Object goes out of scope here,
   //destructor called automatically
   cout << "Object destroyed" << endl;
   return 0;
}

The output of the above code is given below −

Creating an object using static memory allocation
Constructor called
Object created
Destructor called automatically
Object destroyed

C++ Destructor for Dynamic Objects

For objects that are created using dynamic memory allocation, you need to manually call the destructor using delete. In the example below, we have manually deleted the object using the delete operator:

#include <iostream>
using namespace std;

class MyClass
{
   public:
      MyClass()
      {
          cout << "Constructor called" << endl;
      }

      ~MyClass()
      {
         cout << "Destructor called" << endl;
      }
};

int main() {
   cout << "Creating an object using dynamic memory allocation:" << endl;

   // Dynamically creating an object
   MyClass *obj = new MyClass();

   cout << "Object created" << endl;
   // Manually invoking destructor
   delete obj;
   cout << "Object destroyed manually using delete" << endl;
   return 0;
}

The output of the above code is given below −

Creating an object using dynamic memory allocation:
Constructor called
Object created
Destructor called
Object destroyed manually using delete

Destructor Call Order for Multiple Objects

Destructors are called in reverse order of construction for multiple objects. In the example below, we can see that the destructors are called in the reverse order −

#include <iostream>
using namespace std;

class MyClass
{
   private:
      int id;

   public:
      MyClass(int num) : id(num)
      {
         cout << "Constructor called for object " << id << endl;
      }

     ~MyClass()
      {
          cout << "Destructor called for object " << id << endl;
      }
};

int main() {
	cout << "Creating 3 objects:" << endl;

	{
		MyClass obj1(1); // First object
		MyClass obj2(2); // Second object
		MyClass obj3(3); // Third object
		cout << "All objects created" << endl;

	} // All objects go out of scope here

	cout << "All objects destroyed" << endl;
	return 0;
}

The output of the above code is given below −

Creating 3 objects:
Constructor called for object 1
Constructor called for object 2
Constructor called for object 3
All objects created
Destructor called for object 3
Destructor called for object 2
Destructor called for object 1
All objects destroyed

Destructors with Arrays

To clean the memory after an array gets deleted or gets out of scope, we need to call destructor for each array item to avoid any memory leak. There can be three different types of scenarios while using a destructor with an array.

  • Using Destructor with Static Array
  • Using Destructor with a Dynamic Array
  • Using Destructor with an Array of Pointers to Objects

Using a Destructor with a Static Array

In this example, we have a static array of objects, and the memory is freed automatically by the compiler. Here, the destructor is used just for displaying the message.

#include <iostream>
using namespace std;

class Example
{
   private:
      int id;

   public:
      Example(int i) : id(i)
      {
         cout << "Constructor called for object " << id << endl;
      }

     ~Example()
      {
         cout << "Destructor called for object " << id << endl;
      }
};

int main(){
   cout << "Creating array of 3 objects:" << endl;
   Example arr[3] = {Example(1), Example(2), Example(3)};

   cout << "Array created" << endl;

   return 0;
   // Destructors called automatically
}

The output of the above code is given below −

Creating array of 3 objects:
Constructor called for object 1
Constructor called for object 2
Constructor called for object 3
Array created
Destructor called for object 3
Destructor called for object 2
Destructor called for object 1

Using a Destructor with a Dynamic Array

In this example, a dynamic array of objects is created using new[] operator. Here, the destructor is used with delete[] operator to clear the memory.

#include <iostream>
using namespace std;

class Student
{
   private:
      int rollNo;

   public:
      Student(int r = 0) // constructor with default argument
      {
         rollNo = r;
         cout << "Constructor called for roll no: " << rollNo << endl;
      }

      ~Student()
      {
         cout << "Destructor called for roll no: " << rollNo << endl;
      }
};

int main() {
   cout << "Creating dynamic array of 3 students:" << endl;

   Student *students = new Student[3] {101, 102, 103}; // initialize with roll numbers

   cout << "\nDeleting array:" << endl;
   delete[] students; // destructor for all 3 objects

   return 0;
}

The output of the above code is given below −

Creating dynamic array of 3 students:
Constructor called for roll no: 101
Constructor called for roll no: 102
Constructor called for roll no: 103

Deleting array:
Destructor called for roll no: 103
Destructor called for roll no: 102
Destructor called for roll no: 101

Using a Destructor with an Array of Pointers to Objects

In this example, an array of pointers to objects is created. Here, the destructor is used with the delete operator to free the memory of each object individually as each pointer is pointing to a uniquely allocated object.

#include <iostream>
using namespace std;

class Book {
   private:
      string title;

   public:
	
      Book(string t) : title(t)
      {
         cout << "Book created: " << title << endl;
      }

     ~Book()
     {
        cout << "Book destroyed: " << title << endl;
     }
};

int main(){
   cout << "Creating array of 3 books:" << endl;
    Book *library[3];

   // Creating individual book objects
   library[0] = new Book("C++ Basics");
   library[1] = new Book("CSS Basics");
   library[2] = new Book("Javascript Basics");

   cout << "\nDeleting books:" << endl;

   // Deleting each object individually
   for (int i = 0; i < 3; i++){
      delete library[i];
   }

   return 0;
}

The output of the above code is given below −

Creating array of 3 books:
Book created: C++ Basics
Book created: CSS Basics
Book created: Javascript Basics

Deleting books:
Book destroyed: C++ Basics
Book destroyed: CSS Basics
Book destroyed: Javascript Basics

Common Mistakes While Working with Destructors

Some of the common beginner mistakes while working with destructors are listed below −

  • Using destructor for statically allocated memory. It gets deleted automatically, so there is no need to use a destructor.
  • For dynamically allocated memory, forgetting to use the delete.
  • In inheritance, not declaring the destructor as virtual in the base class.
  • For arrays, use delete[] instead of delete.
  • Trying to delete an already-deleted memory twice, also known as double deletion. Set the pointer to nullptr after deleting it, and check the pointer for nullptr before deletion.

Conclusion

Destructors are special member functions to clean up memory when objects go out of scope or program is executed. In C++, there is a default destructor for statically allocated memory, and custom destructors are used for clearing the dynamically allocated memory using the new operator.

Advertisements