Memory Management and Resource Lifetime

Learning Objectives

Describe automatic, dynamic, static and thread-local storage.

Apply RAII and choose between unique_ptr, shared_ptr and weak_ptr.

Explain tracing garbage collection and its limits.

Recognise leaks: shared-ownership cycles, event-handler leaks, closures.

Prerequisites: Module 12.

Intuition and Definition

until the enclosing block ends local variables ("the stack") until explicitly released / no longer owned new, make_unique ("the heap") globals, static members and locals lifetime of the thread thread_local variables

"Stack" and "heap" are simplified models; the language rules are about *lifetime*.

RAII (Resource Acquisition Is Initialization): a resource (memory, file, lock, socket) is acquired in a constructor and released in the destructor. Because destructors run deterministically when objects go out of scope — including during exception unwinding — resources are never leaked.

exclusive, move-only same as a raw pointer shared, reference-counted control block + atomic count updates non-owning observer of a shared_ptr checks liveness with lock()

Garbage collection (Java, C#, Python, JavaScript) frees objects that are no longer *reachable* from roots (stacks, globals). Collection timing is non-deterministic, so non-memory resources (files, sockets) still need explicit release: try-with-resources, using, with, try/finally.

Why It Matters

Memory and resource bugs — leaks, use-after-free, double free, exhausted file handles — are among the most costly defects, and in C++ many are security vulnerabilities. Clear ownership prevents all of them.

Syntax and Language Notes

auto item = std::make_unique<Widget>();          // sole owner
auto shared = std::make_shared<Widget>();        // shared owner
std::weak_ptr<Widget> watcher = shared;          // observer
if (auto p = watcher.lock()) { /* still alive */ }
using var file = File.OpenRead("log.bin");       // Dispose() called at end of scope
with open("log.bin", "rb") as f:                 # __exit__ closes the file
    data = f.read()
// TypeScript 5.2+ explicit resource management (when the runtime supports Symbol.dispose)
// using conn = openConnection();
try { /* use resource */ } finally { /* release resource */ }

Complete Example

With a shared_ptr in both directions the nodes are never freed — a reference cycle leak:

Python's reference counting frees most objects immediately; its cycle collector handles cycles later:

#include <iostream>
#include <memory>
#include <stdexcept>
#include <string>

class FileHandle {                       // RAII wrapper around a pretend OS handle
    std::string name_;
public:
    explicit FileHandle(std::string n) : name_(std::move(n)) { std::cout << "  open  " << name_ << "\n"; }
    ~FileHandle() { std::cout << "  close " << name_ << "\n"; }
    FileHandle(const FileHandle&) = delete;              // a handle has one owner
    FileHandle& operator=(const FileHandle&) = delete;
};

void process(bool fail) {
    FileHandle log("log.txt");
    auto cfg = std::make_unique<FileHandle>("config.ini");
    if (fail) throw std::runtime_error("parse error");
    std::cout << "  processed\n";
}   // both closed here, in reverse order — also when an exception leaves the function

struct Node {
    std::string name;
    std::shared_ptr<Node> next;          // owning link
    std::weak_ptr<Node> prev;            // non-owning back link breaks the cycle
    explicit Node(std::string n) : name(std::move(n)) {}
    ~Node() { std::cout << "  free node " << name << "\n"; }
};

int main() {
    std::cout << "normal run:\n";
    process(false);
    std::cout << "exception run:\n";
 
…
normal run:
  open  log.txt
  open  config.ini
  processed
  close config.ini
  close log.txt
exception run:
  open  log.txt
  open  config.ini
  close config.ini
  close log.txt
  caught: parse error
shared/weak:
  use_count(A)=1 use_count(B)=2
  B's prev is A
  free node A
  free node B
end of main
#include <iostream>
#include <memory>
struct Node {
    std::shared_ptr<Node> other;
    ~Node() { std::cout << "freed\n"; }
};
int main() {
    {
        auto a = std::make_shared<Node>();
        auto b = std::make_shared<Node>();
        a->other = b;
        b->other = a;                    // cycle: each keeps the other alive
        std::cout << "use_count(a)=" << a.use_count() << "\n";
    }
    std::cout << "scope left - no 'freed' lines above: both nodes leaked\n";
}
use_count(a)=2
scope left - no 'freed' lines above: both nodes leaked
import gc, weakref

class Node:
    def __init__(self, name): self.name = name; self.other = None

a, b = Node("a"), Node("b")
a.other, b.other = b, a              # a reference cycle
probe = weakref.ref(a)
del a, b
print("alive after del (cycle keeps it):", probe() is not None)
gc.collect()                          # the cycle collector finds unreachable cycles
print("alive after gc.collect():", probe() is not None)
alive after del (cycle keeps it): True
alive after gc.collect(): False

Step-by-Step Walkthrough

process(false) opens two handles; on return their destructors run in reverse order of construction.

process(true) throws after both are open; stack unwinding still runs both destructors before the exception reaches main — no leak, no finally needed.

In the Node example A is owned only by the local a (B's back link is a weak_ptr), while B is owned by b *and* by A::next (use count 2). Locals die in reverse order: b goes first, but B survives through A::next; then a goes, A is freed, and freeing A releases its next, so B is freed right after.

In m13_cycle both links are owning: each count stays at 1 after the locals die, so neither destructor ever runs.

Python's del removes names, but the cycle keeps the objects reachable from each other until the cycle collector runs.

Common Mistakes

new without a matching owner — use make_unique/make_shared.

shared_ptr by default. Most objects have one owner; shared ownership hides who controls lifetime and costs atomic operations.

Cycles of shared_ptr (parent↔child, observer↔subject). Make back-references weak_ptr or raw non-owning pointers with documented lifetime.

Dangling references/views: returning a std::string_view or std::span to a local; capturing locals by reference in a lambda that outlives them.

GC languages: event-handler leaks. A long-lived publisher holding subscriber callbacks keeps every subscriber alive; unsubscribe or use weak references.

Relying on finalizers (finalize, __del__, C# finalizers) for timely cleanup — they may run late or never.

Trade-offs and Alternatives

Object pools and arenas reduce allocation cost and fragmentation for many short-lived objects of the same type — but complicate lifetimes and are only justified after measurement (Modules 32, 70).

Tracing GC removes most memory bugs but adds pauses (mitigated by generational and concurrent collectors) and memory overhead; deterministic release must still be explicit.

Ownership and borrowing (Rust) checks lifetimes at compile time; C++ approximates it with conventions: owners hold unique_ptr/values, borrowers take references, views or raw non-owning pointers.

Real-World Use

Mutex locks (std::lock_guard), file streams (std::ifstream), database transactions that roll back in the destructor unless committed, and hardware register locks in drivers all rely on RAII.

Practice

Q1. (Easy) Which smart pointer for a single owner?

Answer: std::unique_ptr.

Q2. (Medium) A Parent owns children through shared_ptr<Child>; each child needs to reach its parent. Which pointer type for the back link and why?

Answer: std::weak_ptr<Parent> (or a raw non-owning pointer if the parent always outlives the children); a shared_ptr back link would create a cycle that never frees.

Q3. (Advanced) Write a ScopedTimer-style RAII class that prints "begin X" on construction and "end X" on destruction, and show it works across an early return.

#include <iostream>
#include <string>
class Scoped {
    std::string what_;
public:
    explicit Scoped(std::string w) : what_(std::move(w)) { std::cout << "begin " << what_ << "\n"; }
    ~Scoped() { std::cout << "end " << what_ << "\n"; }
    Scoped(const Scoped&) = delete;
    Scoped& operator=(const Scoped&) = delete;
};
int check(int x) {
    Scoped s("check(" + std::to_string(x) + ")");
    if (x < 0) return -1;          // early return: destructor still runs
    return x * 2;
}
int main() {
    std::cout << check(5) << "\n";
    std::cout << check(-1) << "\n";
}
begin check(5)
end check(5)
10
begin check(-1)
end check(-1)
-1

Interview Question

Q1. "When is shared_ptr the wrong choice?" Answer: When ownership is not genuinely shared. It hides who controls the lifetime, adds a control-block allocation (unless make_shared) and atomic reference-count updates on every copy, can create cycles that leak, and makes destruction timing hard to predict (the last owner might be on another thread). Prefer values, then unique_ptr, and pass non-owning references or raw pointers to functions that merely use an object. Use shared_ptr for truly shared lifetimes such as caches handed to several independent consumers, and add weak_ptr for observers.

Quick Quiz

Q1. What does RAII guarantee during exceptions?

Answer: Destructors of fully constructed automatic objects run during stack unwinding, so resources are released.

Q2. What does weak_ptr::lock() return if the object is gone?

Answer: An empty shared_ptr.

Q3. Why is GC not enough for files and sockets?

Answer: Collection is non-deterministic; the OS resource could stay open indefinitely. Release them explicitly (using, with, try-with-resources).

Summary

Lifetime categories: automatic, dynamic, static, thread-local.

RAII ties resources to object lifetime; smart pointers express ownership.

Avoid shared_ptr cycles with weak_ptr; GC languages still need explicit cleanup for non-memory resources.

Related: Module 12, Module 14 (exception safety), Module 32 (Object Pool), Module 70 (performance).