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:
- To free dynamically allocated memory
- To release resources like files, network connections, or database connections
- To perform cleanup operations before the object is destroyed
- 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:
- Same name as the class preceded by a tilde (~)
- No return type (not even void)
- No parameters
- Cannot be overloaded (only one destructor per class)
- 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:
-
When a local object goes out of scope:
{ Counter c(1); } // Destructor is called here -
When a program ends (for global and static objects):
Counter globalObj(1); // Global object int main() { // ... return 0; // globalObj's destructor is called here } -
When an object created with
newis deleted:Counter* ptr = new Counter(1); delete ptr; // Destructor is called here -
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
-
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 // } }; -
Trying to overload the destructor:
class Invalid { public: // Error: Can't have multiple destructors ~Invalid() { } ~Invalid(int x) { } // Error! }; -
Not making base class destructors virtual (as shown earlier)
-
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
- Always free resources allocated in constructors
- Make base class destructors virtual when inheritance is used
- Follow the Rule of Three: If you need a destructor, you probably need a copy constructor and assignment operator too
- Keep destructors simple and focused on cleanup
- Never throw exceptions from destructors as it can lead to program termination
- 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.