Smart Pointers (unique_ptr, shared_ptr, weak_ptr)

Manual new and delete are the source of C++'s scariest bugs: memory leaks, dangling pointers, and double-frees. Smart pointers fix this by tying an object's lifetime to a variable's scope, so memory is freed automatically . By the end of this lesson you'll own heap objects safely with unique_ptr , share them with shared_ptr , and break cycles with weak_ptr — and you'll almost never write new again.

Learn Smart Pointers (unique_ptr, shared_ptr, weak_ptr) in our free C++ course — a beginner-friendly interactive lesson with worked examples, a practice…

Part of the free C++ course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

Think of a heap object as a library book . A unique_ptr is a book checked out to exactly one person — when they leave, the book is returned automatically. A shared_ptr is a book on a shared desk with a tally sheet: every reader signs in, and the book is only re-shelved when the last reader signs out (the reference count hits zero). A weak_ptr is someone who knows the call number but hasn't checked the book out — they can go look if it's still on the desk, but they never stop it being re-shelved. The magic is RAII: "the destructor does the cleanup", so freeing memory becomes the compiler's job, not yours.

Golden rule: prefer unique_ptr ; promote to shared_ptr only when ownership is truly shared; use weak_ptr to observe without owning.

1. The Problem with Raw new / delete

When you write new , you take on a promise to write a matching delete on every path out of the function — including early returns and thrown exceptions. Miss one and you leak memory; run it twice and you get a crash. This bookkeeping is where countless C++ bugs come from. Run the example and note how the delete is easy to forget.

2. unique_ptr — One Owner, Automatic Cleanup

A std::unique_ptr owns its object exclusively. Create it with std::make_unique<T>(args...) — no new , no delete . When the unique_ptr goes out of scope, its destructor runs and frees the object for you, guaranteed, even if an exception unwinds the stack. You access members with -> exactly like a raw pointer.

Because there can only be one owner, a unique_ptr can't be copied — but you can move ownership with std::move . After the move, the source is empty and the destination owns the object:

Your turn. Fill in the blank to create a Player on the heap with a smart pointer — no manual cleanup required:

These lines should create a shared object, copy it, and print the reference count. Put them in the right order:

Open main (D), make the shared object (B), copy it so two owners exist (A), then print the count — now 2 (C), then return 0; (E) and close the brace (F).

3. shared_ptr — Shared Ownership

When several parts of a program must keep an object alive and none clearly outlives the others, use std::shared_ptr . It keeps a reference count : every copy increments it, every destruction decrements it, and the object is freed only when the count reaches zero. You can inspect the count with .use_count() . Create them with std::make_shared , which allocates the object and its control block in one step.

4. weak_ptr — Observe Without Owning

Two shared_ptr s that point at each other form a reference cycle : their counts never reach zero, so the memory leaks. The fix is std::weak_ptr , which points at a shared object without bumping the count. To use what it points at, you call .lock() , which gives you a temporary shared_ptr if the object is still alive (or nullptr if it's gone). .expired() tells you whether the object has already been destroyed.

Smart pointers are an application of RAII — "Resource Acquisition Is Initialization". The resource (heap memory) is acquired when the smart pointer is constructed and released when it is destroyed. Because C++ guarantees destructors run when an object leaves scope — including during exception unwinding — cleanup becomes automatic and exception-safe.

Modern guidance: express ownership with unique_ptr / shared_ptr , and use plain references or raw pointers only for non-owning, borrowed access. A raw pointer in modern code says "I'm looking, not owning".

Predict the output before revealing the answer.

3 — three shared_ptrs (a, b, c) all own the same int, so the count is 3.

No — a unique_ptr can't be copied. You'd need unique_ptr<int> b = std::move(a); .

3. After b.reset() below, what does w.expired() return?

1 (true) — b was the only owner; resetting it frees the int, so the weak_ptr has expired.

Build a heap-allocated Account with a unique_ptr , run a few transactions, and watch it clean itself up — no delete in sight.

Practice quiz

Which smart pointer should be your default choice?

  • shared_ptr
  • weak_ptr
  • unique_ptr
  • auto_ptr

Answer: unique_ptr. std::unique_ptr expresses single, exclusive ownership with zero overhead beyond a raw pointer — the default.

Can a unique_ptr be copied?

  • No — it can only be moved with std::move to transfer ownership
  • Yes, each copy shares ownership
  • Yes, but only with make_unique
  • Only inside the same scope

Answer: No — it can only be moved with std::move to transfer ownership. Copying a unique_ptr would create two owners of one object, which is forbidden; you move it instead.

Does this compile? unique_ptr<int> a = make_unique<int>(1); unique_ptr<int> b = a;

  • Yes
  • Yes, but b becomes null
  • Only with -std=c++20
  • No — a unique_ptr can't be copied; you'd need std::move(a)

Answer: No — a unique_ptr can't be copied; you'd need std::move(a). unique_ptr is move-only. You must write unique_ptr<int> b = std::move(a);.

Why prefer make_unique / make_shared over writing new directly?

  • They are the only way to allocate memory
  • They are safer, clearer, and make_shared does a single allocation for object and control block
  • They never call the destructor
  • They make the pointer global

Answer: They are safer, clearer, and make_shared does a single allocation for object and control block. The make functions are exception-safe and clearer; make_shared also combines the object and control block in one allocation.

What does this print? auto a = make_shared<int>(5); auto b = a; auto c = a; cout << a.use_count();

  • 3
  • 1
  • 2
  • 0

Answer: 3. Three shared_ptrs (a, b, c) all own the same int, so the reference count is 3.

When is a shared_ptr's managed object actually freed?

  • When the first shared_ptr is destroyed
  • Immediately after make_shared returns
  • When the reference count reaches zero (the last owner goes away)
  • Never — you must call delete

Answer: When the reference count reaches zero (the last owner goes away). shared_ptr reference-counts; the object is destroyed only when use_count drops to 0.

What is a weak_ptr used for?

  • Owning an object faster than unique_ptr
  • Observing a shared object without keeping it alive, e.g. to break reference cycles
  • Storing raw pointers safely
  • Replacing all references

Answer: Observing a shared object without keeping it alive, e.g. to break reference cycles. weak_ptr points at a shared object without bumping the count; it's the standard fix for reference cycles.

How do you safely access the object a weak_ptr points to?

  • Dereference it directly with *
  • Call .get() and delete it
  • Convert it with static_cast
  • Call .lock(), which returns a shared_ptr if the object still exists (or nullptr)

Answer: Call .lock(), which returns a shared_ptr if the object still exists (or nullptr). .lock() yields a temporary shared_ptr when the object is still alive, or nullptr if it has expired.

After auto b = make_shared<int>(9); weak_ptr<int> w = b; b.reset(); what does w.expired() return?

  • 0 (false)
  • 1 (true)
  • It crashes
  • 9

Answer: 1 (true). b was the only owner; resetting it frees the int, so the weak_ptr has expired and expired() returns 1 (true).

Which header must you include for unique_ptr, shared_ptr, weak_ptr, and the make functions?

  • <smartptr>
  • <pointers>
  • <memory>
  • <utility>

Answer: <memory>. All three smart pointers and make_unique/make_shared live in the <memory> header.