JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

Smart Pointers: unique_ptr, shared_ptr, and Ownership

A smart pointer is a tiny object that owns a heap allocation and frees it for you when its life ends. Meet unique_ptr and shared_ptr, and learn to say in code exactly who owns what.

From raw new/delete to an owning object

In C you wrote `p = malloc(n); ...; free(p);` and carried the duty to free in your head. In C++ the raw equivalent is `T *p = new T; ...; delete p;`, and it has the exact same hazard: if anything between the `new` and the `delete` returns early or throws, the `delete` never runs and you have a leak. The first two guides in this rung gave us the cure — RAII ties cleanup to the end of a scope, and move semantics lets us hand a resource from one object to another without copying it. A smart pointer is simply those two ideas packaged into a pointer-shaped object you can use everywhere you used a raw owning pointer.

The key shift is one of vocabulary: stop asking "is this pointer valid?" and start asking "who owns this allocation?" Ownership means exactly one responsibility — the duty to call the destructor and release the memory exactly once. A raw `T *` says nothing about ownership; it might own the object, or it might just be looking at something someone else owns. Smart pointers make that distinction part of the type, so the question answers itself when you read the declaration.

unique_ptr: one owner, zero overhead

`std::unique_ptr<T>` is the default smart pointer and the one you will reach for ninety percent of the time. It models sole ownership: exactly one unique_ptr owns the object, and when that unique_ptr is destroyed it calls `delete` on the pointee. Because there is only ever one owner, the type cannot be copied — copying would create a second owner and a future double free. You can only move it, which transfers the raw pointer out and leaves the source holding nullptr.

std::unique_ptr<Widget> a = std::make_unique<Widget>(42);
std::unique_ptr<Widget> b = std::move(a);   // ownership moves: a is now nullptr
// auto c = b;   // ERROR: copy is deleted -- only one owner allowed

// memory layout of a unique_ptr<T> (default deleter):
//   sizeof(unique_ptr<T>) == sizeof(T*)        // just one pointer, 8 bytes on x86-64
//   address held inside, e.g. 0x55a0b3c41e90
make_unique allocates and constructs in one step; the move transfers the single owner.

Here is the part that makes C++ a systems language: with the default deleter a unique_ptr is exactly the size of one raw pointer (8 bytes on x86-64), and the generated code is identical to hand-written `new`/`delete`. There is no reference count, no hidden allocation, no runtime bookkeeping. This is the zero-overhead principle at work — you get automatic cleanup and you pay nothing for it that you would not have paid by writing the `delete` yourself. Prefer `std::make_unique<T>(args)` over a bare `new`: it is exception-safe and never leaks if a constructor argument throws.

shared_ptr: many owners, a reference count

Sometimes ownership genuinely is shared: several parts of a program each need an object to stay alive, and no single one knows which will be the last to finish with it. That is what std::shared_ptr<T> is for. Copying a shared_ptr is allowed, and each copy is a co-owner. The trick is a hidden control block holding a reference count: every copy adds one to the count, every destruction subtracts one, and when the count reaches zero the object is destroyed and freed. Whoever drops the last reference does the cleanup.

This power is not free, and being honest about the cost matters. A shared_ptr is two pointers wide (the object and the control block), so 16 bytes instead of 8. Use `std::make_shared<T>(args)` and it does a single allocation that holds both the object and the count side by side, which is faster and friendlier to the cache than allocating them separately. And because copies can happen on different threads, the count is updated with atomic operations — atomic increments and decrements are noticeably more expensive than a plain `count++`. The rule of thumb: reach for unique_ptr first, and only upgrade to shared_ptr when ownership is actually shared, not just because it feels convenient.

Cycles, weak_ptr, and non-owning views

Reference counting has one famous failure mode: a cycle. If node A holds a shared_ptr to B and B holds a shared_ptr back to A, then even after the rest of the program forgets both, each still props the other's count up at one. The count never reaches zero, the destructors never run, and you have leaked — a count-based scheme cannot collect a cycle on its own. This is the price of not using a tracing garbage collector, and it is a real, recurring bug, not a footnote.

The cure is std::weak_ptr<T>, a non-owning observer of a shared object. A weak_ptr points at the control block but does NOT raise the strong count, so it cannot keep the object alive. To use it you call `.lock()`, which atomically checks whether the object is still there and, if so, hands you a temporary shared_ptr; if the object is already gone, `.lock()` returns an empty pointer instead of dangling. Break a cycle by making exactly one direction weak — typically a child's back-pointer to its parent is a weak_ptr while the parent's pointer down to the child stays shared.

There is a third, even more common case that beginners over-engineer: a function that just needs to look at an object without owning it at all. The answer there is not a smart pointer — pass a plain reference `const T&`, a raw `T *`, or a non-owning view like a span. A raw pointer in modern C++ is perfectly respectable as long as it means "I am borrowing, not owning." Smart pointers are for the owners; everyone else should hold a borrowed view and trust that an owner is keeping it alive.

Choosing in practice

Once you name ownership out loud, the choice is mechanical. Run down this short checklist for any heap object and the right pointer type falls out almost every time. Notice that the first two answers cover the overwhelming majority of real code; shared_ptr is the exception, not the default.

  1. Does anything own this on the heap at all? If it lives on the stack or someone else clearly owns it, use a plain reference or raw pointer as a borrowed view — no smart pointer.
  2. Is there exactly one owner at a time? Use unique_ptr. Hand ownership across functions by moving it; it is the cheapest and clearest default.
  3. Is ownership genuinely shared, with no single last owner known in advance? Use make_shared and accept the 16-byte size and the atomic-count cost.
  4. Could two shared_ptrs point at each other and form a cycle? Make one direction a weak_ptr and call .lock() before each use.

Step back and see what you have gained. By choosing a pointer type you have written the program's ownership policy directly into its types, where the compiler enforces it: a uncopyable unique_ptr makes a second owner a compile error, and a shared_ptr makes "free when the last one leaves" automatic. This is the same RAII bargain from guide one, now applied to the heap — and it is exactly what the next guide builds on when we make these abstractions generic with templates at no runtime cost.