Home C++ Destructors in C++ tutorial with code examples

Destructors in C++ tutorial with code examples

In C++, a destructor is a special member function that is called automatically when an object goes out of scope or is explicitly deleted.

The primary role of destructors is to free up resources (like memory) that were allocated during the lifetime of the object.

Destructors have the following characteristics:

Key Characteristics of Destructors

  • Same name as the class, preceded by a tilde ~.
  • No parameters and no return type, not even void.
  • Only one destructor per class (no overloading).
  • Called automatically when an object is destroyed, either when it goes out of scope or when delete is used on a dynamically allocated object.

1. Basic Destructor Example

A simple example to demonstrate when a destructor is called.

#include <iostream>
using namespace std;

class Car {
public:
    // Constructor
    Car() {
        cout << "Constructor called!" << endl;
    }

    // Destructor
    ~Car() {
        cout << "Destructor called!" << endl;
    }
};

int main() {
    Car car1; // Constructor is called
    cout << "Main function" << endl;
    return 0; // Destructor is called as car1 goes out of scope
}

Explanation:

  • When Car car1; is declared, the constructor is called.
  • When main finishes, the destructor is called automatically as car1 goes out of scope.

Output:

Constructor called!
Main function
Destructor called!

2. Destructor and Dynamic Memory Allocation

Destructors are essential when using dynamic memory allocation to ensure that memory is freed when the object is destroyed.

#include <iostream>
using namespace std;

class Array {
private:
    int* data;
    int size;

public:
    // Constructor that allocates memory
    Array(int s) {
        size = s;
        data = new int[size];
        cout << "Array of size " << size << " created." << endl;
    }

    // Destructor to release allocated memory
    ~Array() {
        delete[] data;
        cout << "Array memory freed." << endl;
    }
};

int main() {
    Array arr(5); // Constructor allocates memory for array of size 5

    // Array destructor is called here, releasing the allocated memory
    return 0;
}

Explanation:

  • The constructor allocates memory using new, and the destructor releases it using delete[].
  • When arr goes out of scope at the end of main, the destructor is called, freeing the allocated memory.

Output:

Array of size 5 created.
Array memory freed.

3. Destructor for Classes with Multiple Objects

Destructors are called for each object individually when multiple objects of the same class go out of scope.

#include <iostream>
using namespace std;

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

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

int main() {
    Car car1;
    Car car2;

    cout << "Main function ends" << endl;
    return 0;
}

Explanation:

  • The destructors are called for car1 and car2 in the reverse order of their creation, following the last in, first out (LIFO) rule.

Output:

Constructor called!
Constructor called!
Main function ends
Destructor called!
Destructor called!

4. Destructor in a Class with a Pointer Member

If a class has a pointer member, the destructor should ensure that any dynamically allocated memory is freed when the object is destroyed.

#include <iostream>
using namespace std;

class Person {
private:
    string* name;

public:
    // Constructor
    Person(const string& n) {
        name = new string(n);
        cout << "Constructor called for " << *name << endl;
    }

    // Destructor
    ~Person() {
        cout << "Destructor called for " << *name << endl;
        delete name; // Free dynamically allocated memory
    }
};

int main() {
    Person person1("Alice");
    Person person2("Bob");

    cout << "End of main function" << endl;
    return 0;
}

Explanation:

  • name is dynamically allocated in the constructor.
  • The destructor deletes name to free memory.

Output:

Constructor called for Alice
Constructor called for Bob
End of main function
Destructor called for Bob
Destructor called for Alice

5. Destructors in Classes with Inheritance

In inheritance, destructors are called in the reverse order of constructor calls, starting from the most derived class back up the inheritance chain.

#include <iostream>
using namespace std;

class Base {
public:
    Base() {
        cout << "Base class constructor called!" << endl;
    }

    virtual ~Base() {
        cout << "Base class destructor called!" << endl;
    }
};

class Derived : public Base {
public:
    Derived() {
        cout << "Derived class constructor called!" << endl;
    }

    ~Derived() {
        cout << "Derived class destructor called!" << endl;
    }
};

int main() {
    Derived d; // Calls Base and Derived constructors
    return 0;  // Calls Derived and Base destructors
}

Explanation:

  • Derived constructor calls Base constructor first.
  • Destructors are called in reverse order: first Derived, then Base.

Output:

Base class constructor called!
Derived class constructor called!
Derived class destructor called!
Base class destructor called!

6. Destructor with Polymorphism and Virtual Destructors

When using polymorphism, if a base class pointer points to a derived class object, the destructor should be virtual to ensure proper cleanup.

#include <iostream>
using namespace std;

class Base {
public:
    Base() {
        cout << "Base constructor called!" << endl;
    }

    virtual ~Base() { // Virtual destructor
        cout << "Base destructor called!" << endl;
    }
};

class Derived : public Base {
public:
    Derived() {
        cout << "Derived constructor called!" << endl;
    }

    ~Derived() {
        cout << "Derived destructor called!" << endl;
    }
};

int main() {
    Base* ptr = new Derived(); // Base pointer to Derived object
    delete ptr; // Correctly calls both Derived and Base destructors
    return 0;
}

Explanation:

  • Declaring the base class destructor as virtual ensures that both Derived and Base destructors are called when delete ptr is used.
  • Without virtual, only Base destructor would be called, causing a potential memory leak.

Output:

Base constructor called!
Derived constructor called!
Derived destructor called!
Base destructor called!

7. Destructor with Static Data Members

Static members are shared by all objects of a class and are not destroyed with each object. Thus, destructors are not responsible for cleaning up static members.

#include <iostream>
using namespace std;

class Counter {
public:
    static int count;

    Counter() {
        count++;
        cout << "Constructor called. Count = " << count << endl;
    }

    ~Counter() {
        count--;
        cout << "Destructor called. Count = " << count << endl;
    }
};

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

int main() {
    Counter c1;
    Counter c2;
    {
        Counter c3;
    } // c3 goes out of scope, so its destructor is called here
    cout << "End of main function" << endl;
    return 0;
}

Explanation:

  • count is a static member, shared among all objects.
  • Each time an object is created, count is incremented in the constructor and decremented in the destructor.

Output:

Constructor called. Count = 1
Constructor called. Count = 2
Constructor called. Count = 3
Destructor called. Count = 2
End of main function
Destructor called. Count = 1
Destructor called. Count = 0

8. Destructor in Classes with File Handling

When dealing with files, a destructor can be used to close an open file to ensure no data is lost.

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

class FileWriter {
private:
    ofstream file;

public:
    // Constructor opens file
    FileWriter(const string& filename) {
        file.open(filename);
        if (file.is_open()) {
            cout << "File opened successfully." << endl;
        }
    }

    // Destructor closes file
    ~FileWriter() {
        if (file.is_open()) {
            file.close();
            cout << "File closed successfully." << endl;
        }
    }

    void write(const string& data) {
        if (file.is_open()) {
            file << data << endl;
        }
    }
};

int main() {
    FileWriter writer("example.txt");
    writer.write("Hello, World!");
    return 0; // FileWriter destructor closes the file
}

Explanation:

  • The constructor opens a file, and the destructor closes it, ensuring data is saved and resources are freed.

Output:

File opened successfully.
File closed successfully.

Summary Table of Destructor Use Cases

Use Case Description
Basic Destructor Automatically called when an object goes out of scope
Dynamic Memory Allocation Frees dynamically allocated memory
Multiple Objects Each object has its destructor called individually
Pointer Members Frees dynamically allocated pointer members
Inheritance Destructors called in reverse order of inheritance hierarchy
Virtual Destructors Ensures proper cleanup in polymorphic classes
Static Members Not affected by destructors, shared by all objects
File Handling Closes files or releases resources when an object goes out of scope

Complete Example

This example demonstrates a class using destructors to manage dynamically allocated memory and file handling.

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

class ResourceManager {
private:
    int* data;
    ofstream file;

public:
    ResourceManager(const string& filename, int size) {
        // Allocate memory
        data = new int[size];
        for (int i = 0; i < size; i++) {
            data[i] = i + 1;
        }
        cout << "Memory allocated." << endl;

        // Open file
        file.open(filename);
        if (file.is_open()) {
            cout << "File opened." << endl;
            for (int i = 0; i < size; i++) {
                file << data[i] << " ";
            }
            file << endl;
        }
    }

    // Destructor to release resources
    ~ResourceManager() {
        delete[] data;
        cout << "Memory deallocated." << endl;

        if (file.is_open()) {
            file.close();
            cout << "File closed." << endl;
        }
    }
};

int main() {
    ResourceManager manager("data.txt", 5);
    return 0;
}

Explanation:

  • The constructor allocates memory and opens a file.
  • The destructor deallocates memory and closes the file to ensure all resources are properly managed.

Sample Output:

Memory allocated.
File opened.
Memory deallocated.
File closed.

Destructors in C++ play a crucial role in resource management, especially with dynamically allocated memory, file handling, and complex class hierarchies.

You may also like