Skip to content

Destructors — Resource Cleanup and the RAII Concept

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about Destructors. We cover key concepts, practical examples, and best practices to help you master this topic.

C++ destructors are invoked automatically when objects go out of scope, providing deterministic cleanup that makes RAII the cornerstone of correct resource management in C++.

What You'll Learn

You will write destructors that release resources like heap memory, file handles, and mutex locks, understand the RAII idiom and why it eliminates resource leaks, recognize the destructor invocation order for class members and base classes, and learn the rules for virtual destructors in polymorphic classes.

Why It Matters

In languages with Garbage Collection, you do not think about when objects are destroyed. In C++, destruction is deterministic and happens at a precise point: when the object goes out of scope. This determinism is the foundation of RAII, which ties resource lifetimes to object lifetimes. RAII is why C++ code can be exception-safe without try/finally blocks.

Learning Path

graph LR
    A["12: Constructors"] --> B["13: Destructors"]
    B --> C["14: Encapsulation"]
    C --> D["15: Inheritance"]
    style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style D fill:#4a90d9,stroke:#2c5f8a,color:#fff

The Destructor

#include <iostream>

class Resource {
private:
    int id_;
    
public:
    Resource(int id) : id_(id) {
        std::cout << "Acquire resource " << id_ << "\n";
    }
    
    ~Resource() {
        std::cout << "Release resource " << id_ << "\n";
    }
};

int main() {
    {
        Resource r1(1);
        Resource r2(2);
        std::cout << "Inside block\n";
    }  // r2 destroyed first, then r1 (reverse order)
    
    std::cout << "After block\n";
}

Expected output:

Acquire resource 1
Acquire resource 2
Inside block
Release resource 2
Release resource 1
After block

Destructors:

  • Have the same name as the class prefixed with ~
  • Take no arguments
  • Have no return type
  • Cannot be overloaded (only one destructor per class)
  • Are called automatically when an object goes out of scope
  • Are called in reverse order of construction

RAII: Resource Acquisition Is Initialization

RAII ties resource lifetime to object lifetime. Acquire the resource in the constructor, release it in the destructor.

#include <iostream>
#include <stdexcept>

class FileHandle {
private:
    FILE* file_;
    
public:
    FileHandle(const char* filename, const char* mode) {
        file_ = std::fopen(filename, mode);
        if (!file_) {
            throw std::runtime_error("Cannot open file");
        }
        std::cout << "File opened\n";
    }
    
    ~FileHandle() {
        if (file_) {
            std::fclose(file_);
            std::cout << "File closed\n";
        }
    }
    
    void write(const char* text) {
        std::fputs(text, file_);
    }
    
    // Delete copy operations
    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;
};

int main() {
    try {
        FileHandle f("test.txt", "w");
        f.write("Hello RAII\n");
        // No explicit close needed!
    } catch (const std::exception& e) {
        std::cout << e.what() << "\n";
    }
    // File is closed even if an exception occurs
}

Without RAII, every code path that uses a resource must explicitly release it, including error paths. With RAII, the destructor runs automatically when the scope is exited, whether normally or via exception.

Destructor Order

#include <iostream>

struct Member {
    int id;
    Member(int i) : id(i) { std::cout << "Member " << id << " constructed\n"; }
    ~Member() { std::cout << "Member " << id << " destroyed\n"; }
};

struct Container {
    Member m1{1};
    Member m2{2};
    Member m3{3};
    
    ~Container() {
        std::cout << "Container destroyed\n";
    }
};

int main() {
    Container c;
}

Expected output:

Member 1 constructed
Member 2 constructed
Member 3 constructed
Container destroyed
Member 3 destroyed
Member 2 destroyed
Member 1 destroyed

Destruction order is the reverse of construction:

  1. Destructor body runs
  2. Member destructors run in reverse declaration order
  3. Base class destructors run (for derived classes)

Virtual Destructors

When deleting a derived class object through a base class pointer, the destructor must be virtual to ensure the derived destructor runs.

#include <iostream>

class Base {
public:
    Base() { std::cout << "Base constructed\n"; }
    virtual ~Base() { std::cout << "Base destroyed\n"; }
};

class Derived : public Base {
private:
    int* data_;
    
public:
    Derived() : data_(new int[100]) {
        std::cout << "Derived constructed\n";
    }
    
    ~Derived() override {
        delete[] data_;
        std::cout << "Derived destroyed\n";
    }
};

int main() {
    Base* ptr = new Derived();
    delete ptr;  // calls Derived destructor then Base destructor (with virtual)
}

Without virtual ~Base(), deleting through Base* would only call ~Base(), leaking Derived's resources.

Rule: If a class has any virtual function, it should have a virtual destructor.

Pure Virtual Destructor

#include <iostream>

class AbstractBase {
public:
    virtual ~AbstractBase() = 0;  // pure virtual destructor
};

AbstractBase::~AbstractBase() {
    // Must provide a body even though it is pure virtual
}

class Concrete : public AbstractBase {
public:
    ~Concrete() override {
        std::cout << "Concrete destroyed\n";
    }
};

int main() {
    Concrete c;
}

A pure virtual destructor is a way to make a class abstract without any other pure virtual function. It still needs a body because derived class destructors call it.

Common Mistakes

Mistake 1: Non-Virtual Destructor in Base Class

class Base { ~Base(); };  // non-virtual
class Derived : public Base { ~Derived(); };
Base* p = new Derived();
delete p;  // undefined behavior: only ~Base() called

Mistake 2: Throwing in Destructors

~MyClass() {
    throw std::runtime_error("boom");  // BAD
}

Destructors must not throw. If a destructor throws during stack unwinding (when another exception is active), std::terminate is called. Always handle errors in destructors internally.

Mistake 3: Double Delete from Shallow Copy

If you use the compiler-generated copy constructor with a raw pointer member, two objects share the same pointer, and both will try to delete it. Use smart pointers or implement the rule of three/five.

Mistake 4: Forgetting to Call Base Destructor

Base class destructors are called automatically. Do not call them explicitly.

Mistake 5: Relying on Destructors for Non-Deterministic Cleanup

void func() {
    static Resource r;  // destroyed at program exit, not at end of func
}

Static and global objects are destroyed at program termination, not when leaving a scope.

Practice Questions

  1. In what order are members destroyed? Why does this order matter?
  2. Why must destructors not throw exceptions?
  3. What happens if you delete a derived object through a base pointer without a virtual destructor?
  4. Write a MutexLock RAII class that acquires a mutex in the constructor and releases it in the destructor.
  5. Can a destructor be = delete? When would you want that?

Challenge

Implement a ScopedTimer class that prints the elapsed time since construction when it goes out of scope. Use <chrono> for timing. Demonstrate it with a function that does some work (e.g., a loop).

FAQ

Can a destructor be virtual?

Yes. Virtual destructors are essential for polymorphic base classes. If a class has any virtual function, it should have a virtual destructor.

Is it safe to call virtual functions from a destructor?

No. During the destructor, the derived class part has already been destroyed. Calling a virtual function resolves to the base class version, which may not be what you expect.

When is a destructor called for a reference?

Destructors are not called on references. References are just aliases; the actual object's destructor runs when that object is destroyed.

What is the difference between delete and delete[] in destructors?

delete frees memory for a single object; delete[] frees an array. Using the wrong one is undefined behavior. Prefer std::vector and smart pointers to avoid this.

Can I prevent an object from being destroyed?

Yes. Make the destructor private or protected. Then only the class itself (or friends) can destroy instances. This is used in reference-counted designs.

What is `std::atexit`?

It registers a function to be called at program exit. It is a C library function for cleanup, but RAII is preferred in C++.

Mini Project

Build a HeapArray RAII class:

#include <iostream>

class HeapArray {
private:
    int* data_;
    size_t size_;
    
public:
    HeapArray(size_t size) : data_(new int[size]()), size_(size) {
        std::cout << "Allocated " << size << " ints\n";
    }
    
    ~HeapArray() {
        delete[] data_;
        std::cout << "Deallocated " << size_ << " ints\n";
    }
    
    HeapArray(const HeapArray&) = delete;
    HeapArray& operator=(const HeapArray&) = delete;
    
    int& operator[](size_t i) { return data_[i]; }
    const int& operator[](size_t i) const { return data_[i]; }
    size_t size() const { return size_; }
};

int main() {
    HeapArray arr(10);
    for (size_t i = 0; i < arr.size(); ++i) {
        arr[i] = static_cast<int>(i * i);
    }
    for (size_t i = 0; i < arr.size(); ++i) {
        std::cout << arr[i] << " ";
    }
    std::cout << "\n";
}  // arr destructor runs here, freeing memory automatically

What's Next

Destructors and RAII are the foundation of resource safety in C++. The next lesson covers Encapsulation in depth: public, private, protected specifiers, friend functions, and friend classes.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro