C++ Destructors

Special methods for object cleanup

🧹 What are Destructors?

Destructors are special methods automatically called when objects are destroyed. They clean up resources, free memory, and perform final operations before object deletion.


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

MyClass obj;  // Constructor called
// Destructor called automatically when obj goes out of scope
                                    

Output:

Constructor called
Destructor called

Destructor Characteristics

🔄

Automatic Call

Called automatically when object is destroyed

{
    MyClass obj;
}  // Destructor called here
🚫

No Parameters

Destructors cannot take parameters

~MyClass() {
    // No parameters allowed
}
🔧

Resource Cleanup

Free memory and close files

~MyClass() {
    delete[] data;
    file.close();
}
🎯

One Per Class

Only one destructor per class

class MyClass {
    ~MyClass() { }
    // Only one allowed
};

🔹 Basic Destructor Example

Destructors automatically clean up resources when an object goes out of scope, ensuring proper memory management. In C++, the destructor is named ~ClassName() and is called implicitly upon object destruction. For a Student object, the destructor might release dynamically allocated grades and log a destruction message. The sequence shown—creation, usage, and cleanup—demonstrates RAII (Resource Acquisition Is Initialization), where resource lifetime is tied to object lifetime. This prevents leaks, as shown by the logged messages "Student Alice destroyed" and "Student object destroyed" after the object's scope ends.

#include <iostream>
#include <string>
using namespace std;

class Student {
public:
    string name;
    int* grades;
    int numGrades;
    
    // Constructor
    Student(string n, int num) {
        name = n;
        numGrades = num;
        grades = new int[numGrades];  // Allocate memory
        cout << "Student " << name << " created" << endl;
    }
    
    // Destructor
    ~Student() {
        delete[] grades;  // Free allocated memory
        cout << "Student " << name << " destroyed" << endl;
    }
    
    void setGrade(int index, int grade) {
        if (index >= 0 && index < numGrades) {
            grades[index] = grade;
        }
    }
    
    void displayInfo() {
        cout << "Student: " << name << endl;
        cout << "Grades: ";
        for (int i = 0; i < numGrades; i++) {
            cout << grades[i] << " ";
        }
        cout << endl;
    }
};

int main() {
    cout << "Creating student..." << endl;
    
    {
        Student s1("Alice", 3);
        s1.setGrade(0, 85);
        s1.setGrade(1, 92);
        s1.setGrade(2, 78);
        s1.displayInfo();
    }  // Destructor called here when s1 goes out of scope
    
    cout << "Student object destroyed" << endl;
    
    return 0;
}

Output:

Creating student...
Student Alice created
Student: Alice
Grades: 85 92 78
Student Alice destroyed
Student object destroyed

🔹 Destructor with File Handling

Destructors are crucial for managing external resources like files, ensuring they are properly closed even if an exception occurs. A FileManager class can open a file in its constructor and close it in the destructor using std::ofstream. This guarantees that the file handle is released when the FileManager object is destroyed, preventing data corruption or resource leaks. The example shows writing "Hello, World!" to output.txt, followed by automatic closure logged as "File output.txt closed". This RAII pattern is essential for robust, exception-safe file I/O operations in C++.

#include <iostream>
#include <fstream>
#include <string>
using namespace std;

class FileManager {
private:
    ofstream* outputFile;
    string filename;
    
public:
    // Constructor
    FileManager(string fname) {
        filename = fname;
        outputFile = new ofstream(filename);
        if (outputFile->is_open()) {
            cout << "File " << filename << " opened successfully" << endl;
        } else {
            cout << "Failed to open file " << filename << endl;
        }
    }
    
    // Method to write to file
    void writeToFile(string content) {
        if (outputFile && outputFile->is_open()) {
            *outputFile << content << endl;
            cout << "Written to file: " << content << endl;
        }
    }
    
    // Destructor
    ~FileManager() {
        if (outputFile) {
            if (outputFile->is_open()) {
                outputFile->close();
                cout << "File " << filename << " closed" << endl;
            }
            delete outputFile;
            cout << "FileManager destroyed" << endl;
        }
    }
};

int main() {
    cout << "Creating FileManager..." << endl;
    
    {
        FileManager fm("output.txt");
        fm.writeToFile("Hello, World!");
        fm.writeToFile("This is a test file.");
    }  // Destructor called here
    
    cout << "FileManager object destroyed" << endl;
    
    return 0;
}

Output:

Creating FileManager...
File output.txt opened successfully
Written to file: Hello, World!
Written to file: This is a test file.
File output.txt closed
FileManager destroyed
FileManager object destroyed

🔹 Multiple Objects Example

Destructors are called in reverse order of construction when multiple objects go out of scope, maintaining proper cleanup sequences. A static counter can track the total number of live objects. In the example, three Counter objects are created, incrementing the total. When they exit their respective scopes, destructors log decrements, showing "Counter 3 destroyed (Remaining: 2)" and so on. This reverse destruction order ensures dependencies are resolved correctly—objects created later (and potentially dependent on earlier ones) are destroyed first. It highlights how C++ manages object lifetimes automatically in stack-unwinding.

#include <iostream>
#include <string>
using namespace std;

class Counter {
private:
    static int objectCount;
    int id;
    
public:
    // Constructor
    Counter() {
        objectCount++;
        id = objectCount;
        cout << "Counter " << id << " created (Total: " << objectCount << ")" << endl;
    }
    
    // Destructor
    ~Counter() {
        cout << "Counter " << id << " destroyed (Remaining: " << objectCount - 1 << ")" << endl;
        objectCount--;
    }
    
    void displayInfo() {
        cout << "I am Counter " << id << endl;
    }
    
    static int getObjectCount() {
        return objectCount;
    }
};

// Initialize static member
int Counter::objectCount = 0;

int main() {
    cout << "Creating counters..." << endl;
    
    Counter c1;
    Counter c2;
    
    {
        Counter c3;
        c1.displayInfo();
        c2.displayInfo();
        c3.displayInfo();
        cout << "Total objects: " << Counter::getObjectCount() << endl;
    }  // c3 destructor called here
    
    cout << "After inner scope" << endl;
    cout << "Total objects: " << Counter::getObjectCount() << endl;
    
    return 0;
}  // c1 and c2 destructors called here

Output:

Creating counters...
Counter 1 created (Total: 1)
Counter 2 created (Total: 2)
Counter 3 created (Total: 3)
I am Counter 1
I am Counter 2
I am Counter 3
Total objects: 3
Counter 3 destroyed (Remaining: 2)
After inner scope
Total objects: 2
Counter 2 destroyed (Remaining: 1)
Counter 1 destroyed (Remaining: 0)

🧠 Test Your Knowledge

What symbol is used to define a destructor?