Notifications

No notifications

/Phase 3

Memory & Smart Pointers

Owning the Heap, Safely 🧠

C++ gives you direct heap access (new/delete) — but in modern code you almost never write either by hand. Smart pointers in own the resource and free it automatically.

#include <memory>

auto p = make_unique<int>(42); // unique_ptr — sole owner cout << *p; // 42 // no delete needed — frees automatically when 'p' goes out of scope

The Three Smart Pointers

TypeOwnershipCost
unique_ptrSole owner — non-copyable, only movableZero overhead
shared_ptrShared owners (ref-counted) — copyableAtomic ref count
weak_ptrNon-owning observer — breaks cyclesUsed with shared_ptr

Why Smart Pointers?

// ❌ Old C++ — exception-unsafe, easy to leak
Widget* w = new Widget();
mayThrow();
delete w;            // never runs if mayThrow throws

// ✅ Modern C++ auto w = make_unique<Widget>(); mayThrow(); // even if this throws, ~unique_ptr runs and frees

Heap vs Stack — Quick Mental Model

StackHeap
Auto-managed by scopeYou own it (or smart pointer does)
Tiny (a few MB)Huge (gigabytes)
Super fastSlower (allocator)
Size known at compile timeSize can be runtime

> Default: put things on the stack. Heap-allocate (via make_unique / make_shared / containers) only when you need: dynamic size, polymorphism, or shared ownership.

On this page

Detailed Theory

Memory management is C++'s most famous footgun. Modern C++ defangs it with RAII, standard containers, and smart pointers. Master them and you'll virtually never new/delete again.

The Manual Way (and Why It's Bad)

int*    n   = new int(42);          // allocate
int*    arr = new int[100];          // allocate array
delete  n;                           // free single
delete[] arr;                        // free array — NOTE the []

Problems: 1. Leak if you forget to delete (or an exception skips it) 2. Double-free if you delete twice 3. Use-after-free if you keep using a deleted pointer 4. Wrong delete form (delete vs delete[]) → undefined behaviour

Avoid new/delete in modern code. Use make_unique/make_shared and standard containers.

RAII Recap

> *Resource Acquisition Is Initialisation* — own resources in objects, release them in destructors.

std::vector, std::string, std::ifstream, std::unique_ptr — all RAII wrappers around raw resources.

std::unique_ptr — The Default

Sole, exclusive ownership. Cannot be copied (would mean two owners) — but can be moved.

#include <memory>

auto p = make_unique<Widget>(arg1, arg2); // construct on heap p->doStuff(); (*p).field = 42;

// auto q = p; // ❌ compile error — can't copy auto q = std::move(p); // ✅ transfer ownership; p becomes nullptr

Returning From Factory Functions

unique_ptr<Shape> makeShape(string kind) {
    if (kind == "circle")    return make_unique<Circle>(5);
    if (kind == "rectangle") return make_unique<Rectangle>(3, 4);
    return nullptr;
}

Returning by value moves the unique_ptr out — no copy, no leak.

Owning Polymorphic Objects

vector<unique_ptr<Animal>> zoo;
zoo.push_back(make_unique<Dog>());
zoo.push_back(make_unique<Cat>());

for (const auto& a : zoo) a->speak(); // All animals freed when 'zoo' is destroyed

Owning Arrays

auto buf = make_unique<int[]>(100);    // 100 ints, zero-initialised
buf[0] = 42;
// (Usually prefer std::vector — more features.)

std::shared_ptr — Shared Ownership

Multiple owners, reference-counted. The object dies when the last shared_ptr to it goes away.

auto a = make_shared<Image>("photo.png");
auto b = a;                  // ref count = 2
{
    auto c = a;              // ref count = 3
}                            // c destroyed → ref count = 2
// when a and b also die → ref count = 0 → Image deleted

When To Use shared_ptr

  • A graph of objects with multiple owners
  • Async code where you can't predict which task ends last
  • Caches and multi-thread shared state

When NOT To Use shared_ptr

  • For convenience to avoid figuring out ownership — that's a code smell
  • Single-owner cases (use unique_ptr)
  • Hot loops (atomic ref counting has cost)
> Default to unique_ptr. Reach for shared_ptr only when ownership is genuinely shared.

Cyclic References & weak_ptr

Two shared_ptrs that point to each other never reach refcount 0 — memory leak.

struct Node {
    shared_ptr<Node> next;
};

auto a = make_shared<Node>(); auto b = make_shared<Node>(); a->next = b; b->next = a; // ❌ cycle — never freed

Break the cycle with weak_ptr (non-owning observer):

struct Node {
    shared_ptr<Node> next;
    weak_ptr<Node>   prev;       // doesn't keep prev alive
};

To use a weak_ptr, lock it back to a shared_ptr (returns null if already deleted):

if (auto p = wptr.lock()) {
    p->doSomething();
}

Raw Pointers — Still Have a Use

Raw T* is fine for non-owning parameters:

void inspect(const Widget* w);     // optional — caller still owns
void log(const Widget& w);         // required — caller still owns

unique_ptr<Widget> w = ...; inspect(w.get()); // pass raw pointer

Want to expressUse
"I own it; only one owner"unique_ptr
"I own it; many owners share"shared_ptr
"I observe but don't own"T* or T& (or weak_ptr for shared cases)
"It's a fixed-size array"std::array or stack array
"It's a dynamic array"std::vector

Common Bugs You'll Avoid

Old C++ BugModern Fix
Forgot to deleteunique_ptr/shared_ptr
delete twiceunique_ptr can't be copied; shared_ptr is ref-counted
Used after deleteRAII destroys at scope end, no dangling
Wrong delete vs delete[]Use vector or unique_ptr
Leak on exceptionSmart pointers' dtors run during stack unwinding

Custom Deleters (advanced)

unique_ptr accepts a custom deleter — handy for C resources:

unique_ptr<FILE, decltype(&fclose)> file(fopen("a.txt", "r"), &fclose);
// auto-closes when 'file' goes out of scope

Cheat-Sheet

NeedCode
Make on heap (sole owner)auto p = make_unique(args);
Make on heap (shared)auto p = make_shared(args);
Move ownershipauto q = std::move(p);
Get raw pointerp.get()
Reset / replacep.reset(new T) or p.reset()
Polymorphic containervector>
Break cycleweak_ptr
Dynamic arraystd::vector (preferred)
Fixed arraystd::array

You now write C++ that doesn't leak, doesn't double-free, and doesn't crash on exceptions — without ever calling delete. That's the modern C++ promise.

Next: templates — generic code that works for any type.