Intermediate 12 min

Destructors

Learn how destructors automatically clean up resources and prevent memory leaks when objects are destroyed

Destructors are special member functions that automatically clean up resources when objects are destroyed, preventing memory leaks and ensuring proper resource management.

A Simple Example

#include <iostream>
#include <string>

class FileLogger {
public:
    std::string filename;
    bool isOpen;

    FileLogger(std::string fname) : filename{fname}, isOpen{true} {
        std::cout << "Opening log file: " << filename << "\n";
    }

    ~FileLogger() {
        if (isOpen) {
            std::cout << "Closing log file: " << filename << "\n";
            isOpen = false;
        }
    }

    void write(const std::string& message) {
        if (isOpen) {
            std::cout << "[LOG] " << message << "\n";
        }
    }
};

int main() {
    std::cout << "Starting program..." << "\n";

    {
        FileLogger logger{"app.log"};
        logger.write("Application started");
        logger.write("Processing data");
    }  // logger destroyed here, destructor called automatically

    std::cout << "Program continuing..." << "\n";

    return 0;
}

Breaking It Down

Destructor Syntax: ~ClassName()

  • What it does: Special function called automatically when an object is destroyed
  • Syntax: Tilde (~) followed by the class name, no parameters, no return type
  • Timing: Called when object goes out of scope or is explicitly deleted
  • Remember: Every class can have at most one destructor (no overloading)

Automatic Resource Cleanup

  • What it does: Releases resources acquired during object lifetime
  • Common uses: Freeing dynamically allocated memory, closing files, releasing network connections
  • Guarantee: Destructor is always called, even if exceptions occur
  • Remember: Stack objects have destructors called automatically, heap objects need explicit delete

RAII Pattern (Resource Acquisition Is Initialization)

  • What it means: Acquire resources in constructor, release in destructor
  • Benefit: Resources are automatically managed, reducing memory leaks
  • Example: FileLogger acquires file handle in constructor, releases in destructor
  • Remember: This is the cornerstone of exception-safe C++ code

Dynamic Memory and Destructors

  • Critical rule: If you allocate with new, you MUST deallocate with delete in destructor
  • Array rule: Use delete[] for arrays allocated with new[]
  • Rule of Three: If you need a destructor, you likely need copy constructor and assignment operator
  • Remember: The compiler-generated destructor does NOT delete pointers automatically

Why This Matters

  • Every resource you acquire - memory, file handles, network connections - must be released.
  • Forgetting to clean up causes memory leaks that slowly consume all available memory.
  • Destructors guarantee cleanup happens automatically, even when exceptions occur.
  • This is the foundation of RAII (Resource Acquisition Is Initialization), one of C++'s most powerful patterns.

Critical Insight

If your class allocates resources (memory with new, file handles, network connections), it MUST have a destructor to free them. This is the "Rule of Three" - if you need a destructor, you likely also need a copy constructor and assignment operator. The compiler won't do this cleanup for you with dynamic memory!

// WITHOUT destructor - MEMORY LEAK!
class BadArray {
public:
    int* data;
    BadArray(int size) {
        data = new int[size];  // Allocated but never freed!
    }
};  // Memory leaked when object destroyed

// WITH destructor - SAFE!
class GoodArray {
public:
    int* data;
    GoodArray(int size) {
        data = new int[size];
    }
    ~GoodArray() {
        delete[] data;  // Properly freed
    }
};

Best Practices

Always match new with delete: If you allocate with new, deallocate with delete in the destructor. Use delete[] for arrays.

Follow the Rule of Three: If you define a destructor, also define a copy constructor and copy assignment operator to avoid double-deletion bugs.

Use RAII consistently: Acquire resources in constructor, release in destructor. This makes your code exception-safe automatically.

Check for null before deleting: It is safe to delete a nullptr, but checking state can prevent logic errors and make code clearer.

Common Mistakes

Forgetting to Check State in Destructor: Destructors might be called on partially constructed objects or objects that were already cleaned up. Always verify state before cleanup.

Throwing Exceptions in Destructors: Destructors are called during stack unwinding when exceptions occur. Throwing from a destructor can terminate the program. Always catch and handle exceptions inside destructors.

Not Using delete[] for Arrays: Using delete instead of delete[] for dynamically allocated arrays causes undefined behavior and memory leaks. Always match the allocation type.

Forgetting the Rule of Three: If you write a destructor for dynamic memory, you must also write a copy constructor and assignment operator, or explicitly delete them to prevent double-deletion.

Debug Challenge

This class allocates dynamic memory but has a memory leak. Click the highlighted line to add the missing destructor:

1 class DynamicArray {
2 public:
3 int* data;
4 int size;
5
6 DynamicArray(int s) : size{s} {
7 data = new int[size];
8 }
9 // Missing destructor!
10 };

Quick Quiz

  1. When is a destructor called?
Automatically when the object goes out of scope
When you explicitly call it
Only when the program ends
  1. How many destructors can a class have?
Zero or one
One or more
Exactly one
  1. What's the correct syntax for freeing an array allocated with new int[10]?
`delete[] data;`
`delete data;`
`free(data);`

Practice Playground

Time to try out what you just learned! Play with the example code below, experiment by making changes and running the code to deepen your understanding.

Lesson Progress

  • Fix This Code
  • Quick Quiz
  • Practice Playground - run once