Exercises — Destructor — RAII principle
Quick vocabulary refresher, so no symbol appears unearned:
Level 1 — Recognition
Can you spot the rule?
Recall Solution 1.1
Valid: (a) and (d).
- (a) correct declaration: name
~Box, no parameters, no return type. ✅ - (b) ✗ — a destructor takes no parameters.
- (c) ✗ — a destructor has no return type, not even
void. - (d) ✅ — this is the out-of-class definition of the same destructor. Why: the standard fixes the destructor's signature exactly; you only get to write its body.
Recall Solution 1.2
False. There is exactly one destructor per class. Since it takes no parameters, there is nothing to overload on. (Contrast with constructors, which can be overloaded because they take arguments.)
Recall Solution 1.3
| Event | Destructor runs? |
|---|---|
Local object reaches } |
Yes |
delete p; |
Yes (destroys the pointee — the object at the address, defined above) |
| Raw pointer goes out of scope | No — the pointer (the address slip) dies, but the pointee it names is untouched → leak |
Temporary at the ; |
Yes — a temporary is destroyed at the end of the full expression, i.e. at the ;, not at the block's } |
| Global object at program end | Yes |
| Why the third is No: a raw pointer is just a number holding an address. Destroying the number does nothing to the memory it pointed at. That is exactly the hole RAII plugs. | |
Why the temporary is Yes and early: the compiler builds it, uses it within the expression, then destroys it immediately at the ;. This is why std::lock_guard<std::mutex>(m); (an unnamed temporary!) is a bug — it locks and instantly unlocks at the ;, protecting nothing. You must give it a name. |
Level 2 — Application
Predict the output.
Recall Solution 2.1
+a +b +c -c -b -a
Construction is top-to-bottom: a, then b, then c. Destruction is LIFO — reverse order — so c dies first, then b, then a.
Read the figure below: it draws the three objects as a stack of chalk boxes. The white arrow on the left shows construction pushing a, b, c upward (bottom to top); the pink arrow on the right shows destruction popping from the top down — c first (top, dies first), a last (bottom, dies last). That top-off-first motion is LIFO — a stack of plates: the last plate on is the first plate off.

Recall Solution 2.2
+a +b -b +c -c -a
b lives only inside the inner { }, so it is destroyed before c is even built. Then at the outer }, the survivors c and a die LIFO: c first, a last. Lesson: destruction is driven by scope exit, not by declaration order alone.
Recall Solution 2.3
Prints +h +s -s — and there is a memory leak.
s is destroyed at } (prints -s). But p is a raw pointer; leaving scope destroys the pointer, not the Loud pointee on the heap. No delete p; ⇒ ~Loud for 'h' never runs ⇒ -h never prints ⇒ leak. Fix: delete p; before }, or use std::unique_ptr<Loud>.
Level 3 — Analysis
Find the bug and explain the mechanism.
Recall Solution 3.1
Double delete[] → undefined behaviour (likely crash).
Buf b = a; uses the compiler-generated copy constructor, which does a shallow copy: it copies the pointer value d, so a.d and b.d point to the same buffer.
Read the figure below: the blue box a.d and the pink box b.d are two separate pointer variables, but both chalk arrows land on the same yellow "heap buffer int[10]" — they alias it. At }, both destructors run: ~a does delete[] and frees the buffer; ~b then does delete[] on that same, already-freed buffer — the double free the caption warns about.

std::vector<int> and write no destructor at all.
Recall Solution 3.2
Prints ~Base only — buf leaks.
delete p; looks at p's static type Base*. Because ~Base is not virtual, the call is resolved at compile time to ~Base alone; ~Derived never runs, so delete[] buf never happens.
Fix: make the base destructor virtual: virtual ~Base(). Then delete p; dispatches at runtime to ~Derived first (frees buf), then ~Base. With the fix the output is ~Derived ~Base . See Virtual Functions & Polymorphism.
Recall Solution 3.3
~Dbody ~M2 ~M1 ~Base
Destruction of one object goes: (1) the derived destructor body runs first (~Dbody), then (2) members in reverse declaration order — m2 before m1 — then (3) the base class destructor (~Base). It is LIFO applied inside a single object: the base was built first, so it dies last.
Level 4 — Synthesis
Design correct RAII code.
Recall Solution 4.1
class FileGuard {
FILE* f;
public:
explicit FileGuard(const char* path) // ACQUIRE
: f(std::fopen(path, "r")) {
if (!f) throw std::runtime_error("open failed");
}
~FileGuard() noexcept { // RELEASE — always, never throws
if (f) std::fclose(f);
}
FileGuard(const FileGuard&) = delete; // forbid copy (no two owners of one handle)
FileGuard& operator=(const FileGuard&) = delete;
FileGuard(FileGuard&& o) noexcept : f(o.f) { o.f = nullptr; } // MOVE: steal + null out source
FileGuard& operator=(FileGuard&& o) noexcept { // move-assign: close mine, steal
if (this != &o) { if (f) std::fclose(f); f = o.f; o.f = nullptr; }
return *this;
}
FILE* get() const { return f; }
};Why each piece:
- Acquire in the constructor; throw if it fails, so a
FileGuardnever holds a broken handle. - Release in the destructor, guarded by
if (f)(the handle may have been moved-from → null). Markednoexcept— see the next box. - Copy deleted: copying would make two guards own one handle → double
fclose. - Move defined: moving transfers ownership — the source's
fis set tonullptrso its destructor closes nothing. This is the move completion of the Rule of Five: once you delete copy but own a resource, you must supply move (or the type becomes immovable, which is often too restrictive). NowFileGuard g = openLog();can return a guard by value cheaply. Usagevoid read(){ FileGuard g("data.txt"); parse(g.get()); }closes the file even ifparsethrows.
Recall Solution 4.2
struct Tracker {
int& c; // reference to the shared counter
explicit Tracker(int& counter): c(counter) { ++c; } // grab
~Tracker() noexcept { --c; } // drop — cannot throw
};Each loop iteration: ++active on construction (→ 1), then --active at the closing } (→ 0). Net change per iteration is 0. After the loop, active == 0. Because the decrement lives in the destructor, it fires even if the work throws — the counter can never get stuck too high. This is the same idea as lock_guard: acquire in the constructor, release in the destructor, and scope exit does the release on every path.
Level 5 — Mastery
Trace a full program with exceptions, order, and polymorphism together.
Recall Solution 5.1
+a +x -x [caught] +b -b -a
Step by step:
+a—abuilt inouter.inner()called →+x.throwfires.ywas never constructed, so it is not destroyed. Stack unwinding destroys fully-built locals ofinnerin LIFO order → onlyx→-x.- Control lands in
catch→[caught]. - Execution continues past the
try→+b. outerends: destroybthena(LIFO) →-b -a. Key point: an object that was never fully constructed is never destroyed; only completed locals participate in unwinding. This is the guarantee that makes RAII exception-safe — provided none of those destructors throws while unwinding is already underway.
Recall Solution 5.2
Output: ~Circle ~Log ~Shape
~Shape is virtual, so delete s (a Shape*) dispatches to the most-derived destructor. Order within one object: derived body ~Circle, then members in reverse declaration order (~Log), then base ~Shape.
If ~Shape were non-virtual: delete s would call only ~Shape, printing just ~Shape — ~Circle and ~Log would be skipped. Any resource owned by Circle or its members would leak, and the behaviour of deleting a derived object through a non-virtual base pointer is actually undefined. Rule: a base used polymorphically needs a virtual destructor.
Recall Self-check score card
All L1–L2 correct? You recognise and apply the rules. ::: Solid foundation — you can read RAII code. L3 bugs spotted? ::: You understand why the Rule of Three and virtual destructors exist. L4–L5 designed/traced correctly? ::: You can write exception-safe RAII, handle move semantics, keep destructors noexcept, and reason about unwinding — mastery.
Connections
- Destructor — RAII principle (parent)
- Constructor — initialization
- Rule of Three / Five / Zero
- Smart Pointers — unique_ptr & shared_ptr
- Exception Safety & Stack Unwinding
- Virtual Functions & Polymorphism
- Move Semantics
- std::lock_guard & scope guards