A constructor sets a freshly built object up. A destructor does the opposite job at the other end of the object's life: it runs automatically right before the object is destroyed and is the one place to release whatever the object was holding. For a ShoppingCart that holds heap-allocated items, a FileReceipt that owns an open file handle, or an Order that tracks a network connection, the destructor guarantees those resources get cleaned up no matter how the object goes out of scope.
Every non-trivial object has a creation side and a cleanup side. The constructor handles creation: allocate memory, open a file, increment a counter, lock a mutex. None of that work is automatic; the code does it explicitly. Cleanup has the same problem in reverse. Memory needs to be freed. Files need to be closed. Counters need to be decremented. Code that only runs cleanup in some paths leaks resources in the other paths.
Consider a cart that owns a heap-allocated array of items. The constructor allocates the array. Without a destructor, every code path that destroys the cart has to remember to free the array first, which is unreliable:
The program prints the line and exits, but the heap allocation is never released. On a long-running server, repeating this pattern leaks memory until the process gets killed. The fix is to attach cleanup directly to the object's life cycle, so that destroying the ShoppingCart automatically frees the array. That's exactly what a destructor does.
A destructor is a special member function. The name is the class name with a tilde in front, it takes no parameters, and it has no return type, not even void. A class can have only one destructor.
Three things to note. The destructor is named ~ShoppingCart, matching the class name. It has no parameters, so a destructor can't be overloaded or take arguments. And no code calls it directly. The compiler inserts the call automatically when the object's lifetime ends.
Without a written destructor, the compiler generates one. The generated destructor calls the destructor of each data member in reverse declaration order and then ends. For a class that only holds value-type members like std::string or int, the generated destructor is enough, because std::string already cleans itself up. A user-defined destructor is only needed when the class owns a resource that the compiler doesn't know how to release on its own.
A destructor runs automatically when an object's lifetime ends. There are three common situations where that happens, and they cover almost every object that any program writes.
The first is scope exit for a local variable. When a function returns or a block ends, every local variable declared in that scope is destroyed in reverse order of construction.
The destructor for mouse fires on the closing brace of processOrder, before control returns to main. The runtime doesn't wait for the program to end; the object is cleaned up the moment its scope closes.
The second case is delete on a heap-allocated object. An object created with new lives on the heap and stays there until the code frees it. delete is the operation that triggers the destructor for a heap object.
The destructor runs as part of delete. A forgotten delete means the destructor never runs and the memory leaks. Smart pointers exist to make this kind of cleanup automatic, but for now, the rule is plain: every new needs a matching delete.
The third case is a container clearing or removing elements. When a std::vector<Product> goes out of scope, the vector's destructor visits every stored Product and destroys it. The same thing happens on clear(), pop_back(), or when the vector reallocates during a push_back that exceeds its capacity (the moved-from objects get destroyed).
When the inner block ends, the vector itself is destroyed, and as part of that, each Product inside it gets its destructor called. The same pattern applies to clear(), which destroys every element without destroying the vector itself.
Destroying a std::vector<Product> runs the destructor for every element, which is O(n) in the size of the vector. For trivially destructible types (int, double, raw pointers), the compiler skips this loop entirely and the cleanup is O(1) wall time.
The full picture of where destructor calls come from:
The flowchart covers the common paths. Every C++ object ends up matching one of these four cases.
When multiple objects live in the same scope, the order they get destroyed is the reverse of the order they were created. This is the same Last-In-First-Out pattern that local variables follow in any block-structured language.
Cart was constructed first, so it is destroyed last. Order was constructed last, so it is destroyed first. The compiler tracks this order using the same stack discipline it uses for function calls.
The same rule applies to data members inside a class. When the parent object is destroyed, its members are destroyed in reverse declaration order. This matters when one member depends on another being alive during cleanup.
The construction order goes from left to right, and the destruction order goes from left to right of the bottom row, which is the reverse of construction. The shape is a stack: things go in at one end and come out at the other.
For dynamically allocated arrays, the rule still holds. delete[] products runs the destructor on every element starting from the highest index down to index zero.
This LIFO ordering is part of why C++ can guarantee deterministic cleanup. There is no runtime decision about when an object dies; the order is built into the language.
The phrase RAII stands for Resource Acquisition Is Initialization, which is awkward but describes a powerful idea: tie every acquired resource to the lifetime of an object. The constructor acquires the resource. The destructor releases it. When the object dies, the resource is released automatically, no matter how control left the scope (normal return, exception, early return, anything).
A CartItems class that owns a heap-allocated array of item IDs is the classic example. The constructor allocates the array, and the destructor frees it. Once that pairing is in place, the surrounding code doesn't have to remember to release anything.
The caller never writes delete[]. The destructor handles it. If checkout exited early through an exception or an extra return, the destructor would still fire and the array would still get freed. That's what makes RAII a property of the language rather than a convention to remember. The Memory Management section of the course covers RAII in full, including how it interacts with exceptions and how smart pointers package this pattern into reusable types.
The destructor runs every time the object is destroyed, even on the fast path. For a class that owns a heap allocation, that means one delete[] per object. The runtime cost is the cost of delete[] itself; the conceptual cost is zero, because the alternative is a leak.
The constructor-acquires, destructor-releases pairing isn't limited to memory. It works for any resource that needs cleanup: open files, network sockets, database handles, mutex locks, reference counts, anything that has a "release this when done" rule.
A class that tracks a temporary discount applied to a cart is a clean example. The constructor applies the discount, the destructor removes it, and the discount stays applied for exactly as long as the helper object is alive.
The same pattern repeats in every well-written C++ resource type: std::lock_guard locks a mutex on construction and unlocks it on destruction, std::ofstream opens a file on construction and closes it on destruction, std::unique_ptr allocates on construction and frees on destruction. Each one follows the same shape: do the acquire step in the constructor, do the release step in the destructor, and let scope handle the rest.
The takeaway is the symmetry. Whatever the constructor sets up, the destructor must tear down. If the constructor allocates memory, the destructor must delete it. If the constructor opens a connection, the destructor must close it. Breaking the pairing produces leaks; preserving it makes the resource invisible to surrounding code.
When a class serves as a base class for inheritance and a derived object can be deleted through a pointer to the base, the destructor in the base class must be marked virtual. Without virtual, only the base destructor runs, and any resources the derived class owns leak.
The rule of thumb is short: a class with any virtual function needs a virtual destructor.
For a class with no virtual functions and no intended base-class role, a virtual destructor is unnecessary overhead because every class with a virtual function carries a vtable pointer per instance.
Adding a virtual destructor to a previously non-polymorphic class makes every instance one pointer larger (the vtable pointer) and turns destruction into an indirect call. The cost is small, but it's not free, so don't add virtual to a destructor preemptively.
Destructors look simple, and most of the time they are. Two traps are worth flagging now so they're recognizable when they hit.
Throwing an exception out of a destructor is dangerous. If the destructor is already running because of an in-flight exception (the stack is unwinding), throwing a second exception causes the program to call std::terminate and die immediately. There is no way to recover.
Since C++11, destructors are noexcept by default, so any exception that leaves a destructor terminates the program. The fix is to handle errors inside the destructor (catch and log, or set a flag for later) rather than let them escape:
In production code, anything that can fail (closing a network connection, flushing a buffer) belongs in a separate close() or commit() method that callers invoke explicitly. The destructor becomes the last-resort cleanup that has to succeed without raising.
A virtual function call inside a destructor doesn't behave polymorphically. By the time the base class destructor runs, the derived part of the object has already been destroyed, so the virtual dispatch falls back to the base class's version of the function. This is a famous source of confusion when porting code from languages where the rule is different.
The fix is to not rely on virtual dispatch from a destructor. For cleanup behavior that varies by derived type, the derived destructor is the right place, because by the time the base destructor runs, the derived destructor has already finished. The Polymorphism section returns to this with a full explanation of how virtual calls are dispatched and why the rule is exactly this way.
10 quizzes