Destructors in C++

What is a Destructor?

A destructor is a special member function that is automatically called when an object is destroyed or goes out of scope. It has the same name as the class, preceded by a tilde (~) symbol, and is used to clean up resources that the object may have acquired during its lifetime.

Purpose of Destructors

The main purposes of destructors are:

  1. To free dynamically allocated memory
  2. To release resources like files, network connections, or database connections
  3. To perform cleanup operations before the object is destroyed
  4. To prevent memory leaks and resource leaks

Syntax of a Destructor

class ClassName {
private:
    // Data members
    
public:
    // Constructor and other members
    
    // Destructor
    ~ClassName() {
        // Cleanup code
    }
};

Key characteristics of destructors:

  1. Same name as the class preceded by a tilde (~)
  2. No return type (not even void)
  3. No parameters
  4. Cannot be overloaded (only one destructor per class)
  5. Automatically called when an object is destroyed

Simple Example

#include <iostream>
using namespace std;

class Counter {
private:
    int id;
    
public:
    // Constructor
    Counter(int i) {
        id = i;
        cout << "Constructor called for ID: " << id << endl;
    }
    
    // Destructor
    ~Counter() {
        cout << "Destructor called for ID: " << id << endl;
    }
    
    void display() {
        cout << "ID: " << id << endl;
    }
};

int main() {
    cout << "Creating first object" << endl;
    Counter c1(1);
    
    cout << "Creating second object" << endl;
    Counter c2(2);
    
    cout << "Creating block" << endl;
    {
        cout << "Creating third object" << endl;
        Counter c3(3);
        cout << "Leaving block" << endl;
    }  // c3's destructor is called here
    
    cout << "Back to main" << endl;
    
    return 0;  // c1 and c2's destructors are called here
}

Output:

Creating first object
Constructor called for ID: 1
Creating second object
Constructor called for ID: 2
Creating block
Creating third object
Constructor called for ID: 3
Leaving block
Destructor called for ID: 3
Back to main
Destructor called for ID: 2
Destructor called for ID: 1

When are Destructors Called?

Destructors are automatically called in the following situations:

  1. When a local object goes out of scope:

    {
        Counter c(1);
    }  // Destructor is called here
  2. When a program ends (for global and static objects):

    Counter globalObj(1);  // Global object
    
    int main() {
        // ...
        return 0;  // globalObj's destructor is called here
    }
  3. When an object created with new is deleted:

    Counter* ptr = new Counter(1);
    delete ptr;  // Destructor is called here
  4. When a temporary object’s lifetime ends:

    void func() {
        Counter();  // Temporary object - destructor called at the end of this line
    }

Destructors and Dynamic Memory

Destructors are especially important for classes that allocate memory dynamically:

#include <iostream>
using namespace std;

class DynamicArray {
private:
    int* array;
    int size;
    
public:
    // Constructor
    DynamicArray(int s) {
        size = s;
        array = new int[size];  // Allocate memory
        cout << "Memory allocated for array of size " << size << endl;
    }
    
    // Destructor
    ~DynamicArray() {
        delete[] array;  // Free allocated memory
        cout << "Memory freed for array of size " << size << endl;
    }
    
    void setValue(int index, int value) {
        if (index >= 0 && index < size) {
            array[index] = value;
        }
    }
    
    int getValue(int index) {
        if (index >= 0 && index < size) {
            return array[index];
        }
        return -1;
    }
};

int main() {
    DynamicArray arr1(5);
    arr1.setValue(0, 10);
    arr1.setValue(1, 20);
    
    {
        DynamicArray arr2(3);
        arr2.setValue(0, 30);
    }  // arr2's destructor called here
    
    cout << "Value at index 0: " << arr1.getValue(0) << endl;
    
    return 0;  // arr1's destructor called here
}

Virtual Destructors

When dealing with inheritance, it’s important to make base class destructors virtual to ensure proper cleanup:

#include <iostream>
using namespace std;

class Base {
public:
    Base() {
        cout << "Base constructor" << endl;
    }
    
    // Non-virtual destructor (problematic)
    ~Base() {
        cout << "Base destructor" << endl;
    }
};

class Derived : public Base {
private:
    int* data;
    
public:
    Derived() {
        data = new int;
        *data = 10;
        cout << "Derived constructor" << endl;
    }
    
    ~Derived() {
        delete data;
        cout << "Derived destructor" << endl;
    }
};

int main() {
    // Problem: Base pointer to Derived object
    Base* ptr = new Derived();
    delete ptr;  // Only calls Base destructor, not Derived destructor!
    
    return 0;
}

The correct approach is to use a virtual destructor in the base class:

class Base {
public:
    Base() {
        cout << "Base constructor" << endl;
    }
    
    // Virtual destructor (correct)
    virtual ~Base() {
        cout << "Base destructor" << endl;
    }
};

Common Mistakes with Destructors

  1. Forgetting to free dynamically allocated memory:

    class LeakyClass {
    private:
        int* data;
        
    public:
        LeakyClass() {
            data = new int;
        }
        
        // Missing destructor or destructor without delete
        // ~LeakyClass() {
        //     delete data;  // This is missing
        // }
    };
  2. Trying to overload the destructor:

    class Invalid {
    public:
        // Error: Can't have multiple destructors
        ~Invalid() { }
        ~Invalid(int x) { }  // Error!
    };
  3. Not making base class destructors virtual (as shown earlier)

  4. Manually calling the destructor (rarely needed and usually dangerous):

    Counter c(1);
    c.~Counter();  // Manually calling destructor - BAD PRACTICE!
    // The destructor will be called again when c goes out of scope

Best Practices for Destructors

  1. Always free resources allocated in constructors
  2. Make base class destructors virtual when inheritance is used
  3. Follow the Rule of Three: If you need a destructor, you probably need a copy constructor and assignment operator too
  4. Keep destructors simple and focused on cleanup
  5. Never throw exceptions from destructors as it can lead to program termination
  6. Make destructors robust against partially constructed objects

Summary

Destructors are essential for proper resource management in C++. They are automatically called when objects are destroyed and should free any resources that the object acquired during its lifetime. Well-designed destructors prevent memory leaks and ensure that your program handles resources correctly. Understanding when and how destructors are called is crucial for writing reliable C++ code.