5.2.7 · D5C++ Programming

Question bank — Destructor — RAII principle

1,436 words7 min readBack to topic

True or false — justify

A destructor can return a value if you declare its return type.
False — a destructor has no return type at all; you cannot even write void ~C(). The syntax simply forbids it because nothing ever collects a "return value" from an automatic cleanup call.
You may overload a destructor to handle different situations.
False — there is exactly one destructor per class. It takes no parameters, so there is nothing to overload on; different cleanup paths must be handled by branching inside the single destructor.
If you never write a destructor, your class has no destructor.
False — the compiler synthesises an implicit destructor that destroys each member and base class. It just does nothing extra; a class with no raw resources needs no custom one (Rule of Zero).
Calling delete p; twice on the same pointer is harmless because the memory is already gone.
False — the second delete is undefined behaviour (double-free): it operates on a freed block and typically corrupts the heap or crashes. This is exactly the bug a shallow-copied RAII class produces.
An exception thrown mid-function skips all cleanup between the throw and the catch.
False — it skips the rest of the code, but every fully-constructed local object between them has its destructor run during stack unwinding. That is the whole reason RAII survives exceptions.
Setting a raw pointer to nullptr after delete frees the memory.
False — delete already freed it; assigning nullptr only makes the pointer safe to delete again (deleting a null pointer is a no-op). It does not release anything.
std::lock_guard releases the mutex only if the function returns normally.
False — its destructor unlocks on every exit path, including a throw. That determinism is the point: manual lock()/unlock() is the version that leaks the lock on exception.
Two objects can safely share the same raw resource as long as both have destructors.
False — if both destructors free the same resource you get a double-free. Sharing needs reference counting (shared_ptr) or single-ownership (unique_ptr), not two independent destructors.
A unique_ptr member gives your class a correct destructor for free.
True — unique_ptr's own destructor deletes what it owns, and the compiler-generated destructor of your class calls it. This is the Rule of Zero in action.
Destructors run in the same order the objects were constructed.
False — they run in reverse (LIFO). Last constructed dies first, so anything an object depends on is still alive while it tears down.

Spot the error

class Buf { int* p; public: Buf(int n): p(new int[n]) {} ~Buf(){ delete p; } }; — what's wrong?
You allocated with new[] but free with delete (not delete[]). Mismatched forms are undefined behaviour; array allocations must use ==delete[]==.
struct Base { ~Base(){} }; Base* p = new Derived(); delete p; — what leaks?
~Base is not virtual, so delete p through a Base* calls only ~Base and ==skips ~Derived==, leaking whatever Derived owns. A polymorphic base needs a virtual destructor.
IntArray b = a; after writing only a destructor (no copy ctor) — what breaks?
The default copy is a shallow copy: a.data and b.data point to the same buffer. Both destructors free it → double-free. Rule of Three: writing the destructor obliges you to handle copy too.
A destructor body does throw std::runtime_error("fail"); — why is this dangerous?
If the destructor runs during stack unwinding from another exception, a second escaping exception calls ==std::terminate==. Destructors should not let exceptions escape.
void f(){ Lock g; return; delete_something(); } where Lock is RAII — is the lock released?
Yes — the early return ends g's scope, so ~Lock runs and releases. This is why RAII beats manual cleanup: early returns can't skip a destructor.
~Widget(){ delete child; child = new Widget(); } — spot the conceptual error.
Reviving the object inside its own destructor is meaningless: the object is being destroyed and the freshly-newed one is immediately leaked/lost. A destructor's only job is release, not resurrection.

Why questions

Why must acquisition happen in the constructor specifically, not just any init function?
Only the constructor is guaranteed to pair with the destructor: if construction completes, the destructor is guaranteed to run. A separate init could be forgotten, leaving the destructor freeing something never acquired.
Why is "manual delete on the happy path" not good enough?
The happy path is only one of many exit paths. An exception or a later-added early return jumps over the delete, leaking. RAII makes cleanup unconditional across all exit paths.
Why does the Rule of Three exist as a rule rather than case-by-case advice?
Owning a raw resource makes the default shallow copy semantically wrong. Since the default copy ctor, copy assignment, and destructor all touch that ownership, fixing one without the others leaves a latent double-free or leak.
Why does a base class destructor being virtual fix polymorphic deletion?
virtual makes the destructor call dispatch dynamically to the object's actual type, so delete base_ptr runs ~Derived first, then ~Base, cleaning up the whole object rather than just the base slice.
Why are members destroyed in reverse declaration order, not reverse initialization order?
Because members are always initialized in declaration order (regardless of the init-list order you wrote), so reverse-declaration is the true LIFO teardown that keeps dependencies alive until their users are gone.
Why does Rule of Zero often produce safer code than a hand-written destructor?
It delegates ownership to already-correct RAII types (vector, unique_ptr), so there is no raw resource to mismanage — no destructor, copy, or move to get wrong.

Edge cases

What happens to a destructor if the constructor threw before finishing?
The object was never fully constructed, so its destructor never runs — but any members and bases already constructed are destroyed. This is why partially-built objects don't leak their completed sub-parts.
Does delete on a null pointer call a destructor or crash?
Neither — delete nullptr; is a well-defined no-op. No object exists, so no destructor runs and nothing is freed.
Are static/global objects' destructors guaranteed to run, and when?
Yes, at program end (after main returns), in reverse order of construction — but only for a normal exit. Calling std::abort or std::exit semantics can skip or alter this.
If an object is created with placement new, does the destructor run automatically at scope end?
No — placement new builds an object in memory you manage, so you must call the destructor explicitly (p->~T();). Only ordinary automatic/deleted objects get automatic destruction.
For a local array Widget arr[3];, in what order are the three destructors called?
Reverse index order: arr[2], then arr[1], then arr[0] — LIFO applies to array elements too, mirroring their construction order.
Does moving from an object (move semantics) mean its destructor still runs?
Yes — a moved-from object is still a live object and its destructor still runs. A correct move leaves it in a valid, resource-empty state so that destructor releases nothing (no double-free).

Recall One-line self-test before you close this page

Cover this and answer aloud: "Name three exit paths a manual delete misses but a destructor catches." ::: Thrown exception (stack unwinding), an early return added later, and any other non-happy control flow such as a break/goto out of the scope — all trigger the destructor but bypass a hand-placed delete.


Connections