Smart Pointers (unique_ptr, shared_ptr, weak_ptr)
Reviewed & published by Brayan K
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.
Part of the free C++ course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn
- Why raw new/delete leads to leaks and dangling pointers
- Own heap objects exclusively with std::unique_ptr
- Create them with make_unique and make_shared (no raw new)
- Transfer ownership with std::move
- Share ownership with std::shared_ptr and a reference count
- Break reference cycles with std::weak_ptr and .lock()
💡 Real-World Analogy
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.
📊 The Three Smart Pointers
| Type | Ownership | Copyable? | Use when |
|---|---|---|---|
| unique_ptr | exactly one owner | no (move only) | the default choice |
| shared_ptr | shared, ref-counted | yes | many owners share an object |
| weak_ptr | none (observes) | yes | break cycles / observe safely |
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.
#include <iostream>
using namespace std;
struct Widget {
Widget() { cout << "Widget built" << endl; }
~Widget() { cout << "Widget destroyed" << endl; }
void greet() { cout << "Hello from Widget" << endl; }
};
int main() {
// The OLD way, with a raw pointer + new/delete:
Widget* w = new Widget(); // Widget built
w->greet(); // Hello from Widget
// Easy to forget this -> a MEMORY LEAK.
// And if an exception is thrown above, we never reach it at all.
delete w; // Widget destroyed
return 0;
}
// ✅ Expected output:
// Widget built
// Hello from Widget
// Widget destroyed2. 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.
#include <iostream>
#include <memory>
using namespace std;
struct Widget {
Widget() { cout << "Widget built" << endl; }
~Widget() { cout << "Widget destroyed" << endl; }
void greet() { cout << "Hello from Widget" << endl; }
};
int main() {
// unique_ptr OWNS the object exclusively. No new, no delete.
unique_ptr<Widget> w = make_unique<Widget>(); // Widget built
w->greet(); // Hello from Widget
// When w goes out of scope at the end of main, the Widget is
// destroyed AUTOMATICALLY -- even if an exception is thrown.
return 0;
// Output also prints: Widget destroyed
}
// ✅ Expected output:
// Widget built
// Hello from Widget
// Widget destroyedBecause 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:
#include <iostream>
#include <memory>
#include <utility> // std::move
using namespace std;
struct Resource {
int id;
Resource(int i) : id(i) { cout << "open " << id << endl; }
~Resource() { cout << "close " << id << endl; }
};
int main() {
unique_ptr<Resource> a = make_unique<Resource>(7); // open 7
// unique_ptr CANNOT be copied -- only ONE owner allowed:
// unique_ptr<Resource> bad = a; // ❌ compile error
// ...but ownership can be MOVED. After this, 'a' is empty.
unique_ptr<Resource> b = std::move(a);
cout << "a is " << (a ? "set" : "empty") << endl; // a is empty
cout << "b owns id " << b->id << endl; // b owns id 7
return 0;
// 'b' destroys the Resource here -> close 7
}
// ✅ Expected output:
// open 7
// a is empty
// b owns id 7
// close 7Your turn. Fill in the blank to create a Player on the heap with a smart pointer — no manual cleanup required:
#include <iostream>
#include <memory>
using namespace std;
struct Player { int hp = 100; };
int main() {
// 🎯 YOUR TURN — create a Player on the heap with a smart pointer.
// 1) Make a unique_ptr<Player> using make_unique:
auto p = ___; // 👉 make_unique<Player>();
// 2) Damage the player:
p->hp -= 30;
cout << "HP: " << p->hp << endl;
// ✅ Expected output: HP: 70
// (No delete needed -- the Player is freed automatically.)
return 0;
}🧩 Reorder Challenge
These lines should create a shared object, copy it, and print the reference count. Put them in the right order:
A. shared_ptr<int> b = a;
B. auto a = make_shared<int>(42);
C. cout << a.use_count() << endl;
D. int main() {
E. return 0;
F. }Correct order: D → B → A → C → E → F
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.
#include <iostream>
#include <memory>
using namespace std;
struct Texture {
Texture() { cout << "load texture" << endl; }
~Texture() { cout << "free texture" << endl; }
};
int main() {
// shared_ptr lets MANY owners share one object. A reference count
// tracks how many shared_ptrs point at it.
shared_ptr<Texture> a = make_shared<Texture>(); // load texture
cout << "count: " << a.use_count() << endl; // count: 1
{
shared_ptr<Texture> b = a; // copy -> both own it
cout << "count: " << a.use_count() << endl; // count: 2
} // b goes away -> count drops back to 1, NOT destroyed yet
cout << "count: " << a.use_count() << endl; // count: 1
return 0;
// last owner 'a' leaves -> count hits 0 -> free texture
}
// ✅ Expected output:
// load texture
// count: 1
// count: 2
// count: 1
// free texture4. weak_ptr — Observe Without Owning
Two shared_ptrs 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.
#include <iostream>
#include <memory>
using namespace std;
struct Node {
int value;
weak_ptr<Node> next; // weak: does NOT keep 'next' alive
Node(int v) : value(v) {}
};
int main() {
auto a = make_shared<Node>(1);
auto b = make_shared<Node>(2);
a->next = b; // a observes b, but does not own it
// .lock() upgrades a weak_ptr to a shared_ptr IF the object lives:
if (auto locked = a->next.lock()) {
cout << "next value: " << locked->value << endl; // next value: 2
}
b.reset(); // release the only owner of node 2
cout << "expired? " << a->next.expired() << endl; // expired? 1
if (a->next.lock() == nullptr) {
cout << "node 2 is gone" << endl; // node 2 is gone
}
return 0;
}
// ✅ Expected output:
// next value: 2
// expired? 1
// node 2 is gone🔎 Deep Dive: RAII, and why you rarely write new
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".
auto u = make_unique<Widget>(); // owns
Widget* borrowed = u.get(); // borrows -- does NOT own, never delete it
process(*borrowed); // fine while 'u' is alive
// when 'u' leaves scope, the Widget is destroyed -- automaticallyPro Tips
- 💡 Prefer make_unique/make_shared: they're exception-safe and clearer than raw new.
- 💡 Default to unique_ptr: it's zero-overhead and expresses single ownership; reach for shared_ptr only when sharing is real.
- 💡 Pass unique_ptr by std::move: to hand ownership to a function, move it in.
- 💡 Use weak_ptr to break cycles: parent owns child with shared_ptr, child observes parent with weak_ptr.
🧠 Quick Recall
Predict the output before revealing the answer.
1. What does this print?
auto a = make_shared<int>(5);
auto b = a;
auto c = a;
cout << a.use_count();3 — three shared_ptrs (a, b, c) all own the same int, so the count is 3.
2. Does this compile?
unique_ptr<int> a = make_unique<int>(1);
unique_ptr<int> b = a; // ?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?
auto b = make_shared<int>(9);
weak_ptr<int> w = b;
b.reset();
cout << w.expired();1 (true) — b was the only owner; resetting it frees the int, so the weak_ptr has expired.
Common Errors (and the fix)
- Copying a unique_ptr: auto b = a; fails to compile. Use auto b = std::move(a); to transfer ownership.
- Two shared_ptrs from one raw pointer: Widget* raw = new Widget; shared_ptr<Widget> a(raw); shared_ptr<Widget> b(raw); gives two control blocks and a double free. Always use make_shared.
- Reference cycle leak: two objects holding shared_ptrs to each other never reach count 0. Make one side a weak_ptr.
- Dangling raw from .get(): storing u.get() and using it after u is destroyed is undefined behaviour. Only borrow while the owner is alive.
- Forgetting #include <memory>: all three smart pointers and the make-functions live there.
📋 Quick Reference
| Task | Code |
|---|---|
| Own one object | auto u = make_unique<T>(args); |
| Share an object | auto s = make_shared<T>(args); |
| Transfer ownership | auto v = std::move(u); |
| Reference count | s.use_count() |
| Observe | weak_ptr<T> w = s; |
| Use a weak_ptr | if (auto sp = w.lock()) |
| Borrow raw | T* raw = u.get(); |
Mini-Challenge: Self-Closing Account
Build a heap-allocated Account with a unique_ptr, run a few transactions, and watch it clean itself up — no delete in sight.
#include <iostream>
#include <memory>
#include <string>
using namespace std;
struct Account {
string owner;
double balance;
Account(string o, double b) : owner(o), balance(b) {
cout << "opened " << owner << "'s account" << endl;
}
~Account() { cout << "closed " << owner << "'s account" << endl; }
};
int main() {
// 🎯 MINI-CHALLENGE: Bank account with a smart pointer
// 1. Create a unique_ptr<Account> for "Mia" starting at 100.0
// using make_unique.
// 2. Deposit 50 (add to ->balance), then withdraw 30.
// 3. Print the final balance.
// 4. Notice the account closes AUTOMATICALLY at the end -- no delete.
//
// ✅ Example output:
// opened Mia's account
// Balance: 120
// closed Mia's account
// your code here
return 0;
}🎉 Lesson Complete
- ✅ Raw new/delete invites leaks and double-frees; smart pointers automate cleanup
- ✅ unique_ptr owns one object exclusively — move it, never copy it
- ✅ make_unique / make_shared replace raw new
- ✅ shared_ptr reference-counts; the object frees at count zero
- ✅ weak_ptr observes without owning and breaks reference cycles via .lock()
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.
Continue this course
- Previous: Pointers & References
- Next: Structs (struct vs class) — Group related data — and how structs differ from classes
- Quick reference: C++ cheat sheet › Memory
- From the blog: Memory Management in C++
Frequently asked questions
When should I use unique_ptr vs shared_ptr?
Default to std::unique_ptr — it expresses single, exclusive ownership and has zero overhead beyond a raw pointer. Use std::shared_ptr only when ownership is genuinely shared, i.e. several parts of the program must keep the object alive and you cannot say which one outlives the others.
Why should I use std::make_unique and std::make_shared?
They are safer and clearer than calling new directly. make_shared also performs a single allocation for both the object and its control block, which is more efficient. As a rule, you should almost never write the keyword new in modern C++.
What is a weak_ptr for?
std::weak_ptr observes an object owned by a shared_ptr without keeping it alive. It is the standard fix for reference cycles (A owns B, B owns A) which would otherwise leak. You call .lock() to obtain a temporary shared_ptr if the object still exists.
Can I copy a unique_ptr?
No — a unique_ptr cannot be copied, because that would create two owners of one object. You can move it with std::move to transfer ownership. shared_ptr can be copied freely; each copy bumps the reference count.
Do smart pointers replace all manual memory management?
For ownership, yes — they automatically call delete when the last owner goes away, eliminating most leaks and double-frees. You still use plain references or raw pointers for non-owning access, where you are just borrowing an object you do not own.