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.