Dynamic Stack  «Prev  Next»
Lesson 6 Using a Destructor
Objective Examine the use of a destructor in the ch_stack class in C++

Using a Destructor in 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` or similar RAII container, eliminating the need to write a destructor altogether while still achieving correct and exception-safe resource management.

What a destructor is (and why ch_stack needs one)

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.

When destructors run

Destructors are almost always called implicitly, such as:

  1. Scope exit: when a local object goes out of scope (end of a block or function).
  2. Delete expressions: when you delete an object created with new (the destructor runs before memory is reclaimed).
  3. Exception unwinding: when an exception leaves a scope, local objects are destroyed automatically.
  4. Program shutdown: static storage objects (globals/statics) are destroyed when the program terminates.

Adding a destructor to ch_stack (manual ownership version)

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).

Important rule: if you define a destructor, think “Rule of Three/Five”

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:

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:

C++23 best practice: avoid raw char* ownership (Rule of Zero approach)

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.

Destructor pattern (general form)

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.


SEMrush Software