| Lesson 6 | Using a Destructor |
| Objective | Examine the use of a destructor in the ch_stack class in C++ |
ch_stack (C++23)
In C++23, a destructor for the `ch_stack` class is essential when the class manually manages a dynamically allocated character buffer via `new[]`, as it ensures that the owned memory is properly released with `delete[]` to prevent memory leaks. The destructor, declared as `~ch_stack() noexcept`, runs automatically when a `ch_stack` object goes out of scope, is explicitly deleted, or is destroyed during exception unwinding or program termination. By including `delete[] s;` inside the destructor body — where `s` is the raw pointer member — every instance reliably cleans up its heap-allocated storage, even if the pointer is `nullptr` (which `delete[]` safely handles). However, the presence of a user-defined destructor signals manual resource ownership, which in turn activates the Rule of Three/Five and requires careful implementation of the copy constructor, copy assignment operator, (and preferably move operations) to avoid double-deletion or shallow-copy bugs. In modern C++23 practice, the preferred approach follows the Rule of Zero by replacing the raw `char*` with a `std::vector
A destructor is a special member function whose job is to perform cleanup when an object’s lifetime ends.
Its name is the class name prefixed with a tilde (~), it takes no parameters, and it returns no value.
If ch_stack allocates memory dynamically (for example, s = new char[size];),
then the class owns that memory. Ownership means the class must also release it.
That’s exactly what the destructor is for.
Destructors are almost always called implicitly, such as:
delete an object created with new (the destructor runs before memory is reclaimed).
Here is a corrected, coherent ch_stack sketch showing how a destructor fits into the design.
This version matches the learning intent of the module: you can “see” ownership by using new[] and delete[].
// ch_stack with constructors and destructor (educational version)
// NOTE: In production C++23, prefer RAII containers to avoid manual delete[].
class ch_stack {
public:
ch_stack(); // default constructor
explicit ch_stack(int size); // capacity constructor
ch_stack(const ch_stack& other); // copy constructor
ch_stack(const char* c); // C-string constructor (from prior exercise)
~ch_stack() noexcept { // destructors should not throw
delete[] s; // safe even if s == nullptr
}
// ... push/pop/empty/etc ...
private:
enum { EMPTY = -1 };
char* s = nullptr; // owned buffer
int max_len = 0; // capacity
int top = EMPTY; // index of top element
};
With the destructor present, each ch_stack returns its heap-allocated buffer when the object is destroyed.
Without a destructor, every constructed stack would leak memory (unless the program ends immediately and the OS reclaims it).
A destructor is only one part of correct manual ownership. If your class owns a raw pointer, you must also decide how copying works. Otherwise you can easily create a double-delete bug:
delete[] on the same pointer → undefined behavior.That’s why, in earlier lessons, you added a copy constructor. In modern C++23 you would also consider move operations. The modern guidance is:
In modern C++23, you typically represent a stack buffer with a standard container that manages memory automatically. When you do this, you usually don’t write a destructor at all:
#include <vector>
class ch_stack {
public:
explicit ch_stack(int size) : s(static_cast<std::size_t>(size)), top(EMPTY) {}
// No destructor needed (Rule of Zero).
// Copying and moving are correct by default.
private:
static constexpr int EMPTY = -1;
std::vector<char> s; // owns storage safely
int top = EMPTY;
};
The educational value of the manual version is that it makes ownership explicit and forces you to learn why destructors, copy constructors, and assignment operators matter. The production value of the RAII version is that it eliminates whole classes of bugs.
ClassName::~ClassName() noexcept
{
// release resources owned by the object
}
// Example (legacy style):
String::~String()
{
delete[] buffer;
}
Purpose: Perform housekeeping that must happen before an object’s resources are released.