Looking for a clear, beginner‑friendly guide to Smart Pointer in C++? This article explains what smart pointers are, why they matter, and exactly how to use them safely—with commented code, outputs, and “Try it yourself” challenges after each section.
🔹 What is a Smart Pointer in C++?
A Smart Pointer in C++ is a class template that behaves like a pointer but also owns and automatically releases the resource it points to (RAII: Resource Acquisition Is Initialization). This means memory and other resources are freed deterministically when the smart pointer goes out of scope, even if exceptions happen.
#include <iostream>
#include <memory>
using namespace std;
struct Greeter {
Greeter() { cout << "Greeter constructed\n"; }
~Greeter() { cout << "Greeter destroyed\n"; }
void hello() const { cout << "Hello, RAII!\n"; }
};
int main() {
// unique_ptr owns the Greeter; auto-cleans in all paths
unique_ptr<Greeter> g = make_unique<Greeter>();
g->hello();
// no delete needed; ~Greeter runs when g leaves scope
}
Output
Greeter constructed
Hello, RAII!
Greeter destroyed
Explanation: unique_ptr
takes ownership of the object and deletes it automatically at scope exit; no manual delete
is required. This is the core idea behind Smart Pointer in C++.
Try it yourself
- Add a second inner scope and create another
unique_ptr<Greeter>
; observe the destruction order. - Comment out
make_unique
and tryunique_ptr<Greeter> g(new Greeter())
; then switch back tomake_unique
to prefer exception safety.
🔹 Why use Smart Pointer in C++?
- Automatic cleanup: Prevents leaks and double frees by tying ownership to scope (RAII).
- Exception safety: Destructors run during unwinding, so resources are released even on errors.
- Clear ownership:
unique_ptr
= single owner,shared_ptr
= shared ownership,weak_ptr
= non‑owning observer. - Interoperability: Works with STL containers, algorithms, and custom deleters for non‑memory resources.
🔹 unique_ptr: The Default Smart Pointer in C++
unique_ptr
expresses exclusive ownership: copy is disallowed, move is allowed. Use make_unique
in modern code for safety and clarity.
#include <iostream>
#include <memory>
using namespace std;
struct Toy {
string name;
explicit Toy(string n) : name(move(n))
{ cout << "Make " << name << "\n"; }
~Toy() { cout << "Drop " << name << "\n"; }
void play() const
{ cout << "Playing with " << name << "\n"; }
};
unique_ptr<Toy> make() {
// Prefer make_unique for exception-safe construction
return make_unique<Toy>("Car");
}
int main() {
auto p = make(); // p owns the Toy
p->play();
// Transfer (move) ownership to q
auto q = move(p);
if (!p)
{ cout << "p is empty after move\n"; }
q->play(); // still valid via q
// q goes out of scope → Toy destroyed
}
Output
Make Car
Playing with Car
p is empty after move
Playing with Car
Drop Car
Explanation: unique_ptr
enforces one owner. Moving transfers ownership cleanly. This is the most common and safest default for Smart Pointer in C++ when sharing isn’t needed.
Try it yourself
- Uncomment a copy attempt
auto r = q;
and read the compiler error to understand why copying is disabled. - Store
unique_ptr<Toy>
in avector
; push a few withemplace_back(make_unique<Toy>(...))
and observe construction/destruction order.
🔹 shared_ptr: Shared Ownership Smart Pointer in C++
shared_ptr
keeps a reference count so multiple owners can share the same object. When the last shared_ptr
is destroyed/reset, the object is deleted. Use make_shared
to allocate object and control block together for efficiency.
#include <iostream>
#include <memory>
#include <vector>
using namespace std;
struct Book {
string title;
explicit Book(string t) : title(move(t))
{ cout << "Open " << title << "\n"; }
~Book()
{ cout << "Close " << title << "\n"; }
};
int main() {
auto b = make_shared<Book>("CPP Primer");
cout << "use_count: "
<< b.use_count() << "\n";
vector<shared_ptr<Book>> shelf;
shelf.push_back(b);
shelf.push_back(b);
cout << "use_count after copies: "
<< b.use_count() << "\n";
shelf.clear(); // drop two owners
cout << "use_count after clear: "
<< b.use_count() << "\n";
b.reset(); // last owner gone → destructor runs
}
Output
Open CPP Primer
use_count: 1
use_count after copies: 3
use_count after clear: 1
Close CPP Primer
Explanation: shared_ptr
increments/decrements its control block count as copies come and go. When the count reaches zero, the managed object is deleted automatically.
Try it yourself
- Create two
shared_ptr
fromnew
of the same raw pointer (don’t actually do this in real code) and observe double delete—then fix by using only oneshared_ptr
created viamake_shared
. - Put
shared_ptr
into amap<string, shared_ptr<Book>>
, erase entries, and watchuse_count
changes.
🔹 weak_ptr: Observing Without Owning
weak_ptr
points to a shared_ptr
-managed object without increasing its reference count, helping break cycles and check liveness safely with lock()
.
#include <iostream>
#include <memory>
using namespace std;
struct Node {
string name;
shared_ptr<Node> next; // owning
weak_ptr<Node> prev; // non-owning to avoid cycle
explicit Node(string n) : name(move(n))
{ cout << "New " << name << "\n"; }
~Node() { cout << "Free " << name << "\n"; }
};
int main() {
auto a = make_shared<Node>("A");
auto b = make_shared<Node>("B");
a->next = b; // A owns B
b->prev = a; // B observes A (weak)
if (auto pa = b->prev.lock()) {
cout << "Prev of B is "
<< pa->name << "\n";
}
// Both freed at scope end; weak_ptr prevents cycle leak
}
Output
New A
New B
Prev of B is A
Free B
Free A
Explanation: Use weak_ptr
to avoid reference cycles (e.g., bidirectional graphs). Use lock()
to get a temporary shared_ptr
if the object is still alive.
Try it yourself
- Change
prev
toshared_ptr<Node>
and see that destructors don’t run (leak via cycle); revert toweak_ptr
to fix. - Reset
a
explicitly before the end and verifyb->prev.lock()
returns empty.
🔹 make_unique vs new, make_shared vs new
Prefer factory helpers (make_unique
, make_shared
) over raw new
to avoid leaks on exceptions, reduce typing, and improve efficiency (especially for make_shared
).
#include <memory>
#include <vector>
#include <string>
using namespace std;
struct Item { string id; explicit Item(string s) : id(move(s)) {} };
int main() {
vector<shared_ptr<Item>> v;
// Good: strong exception safety and fewer allocations overall
v.push_back(make_shared<Item>("A"));
v.push_back(make_shared<Item>("B"));
// Also good for exclusive ownership
auto u = make_unique<Item>("Temp");
(void)u;
}
Explanation: make_shared
allocates the control block and object in one go, often improving performance and locality. make_unique
avoids accidental naked new
.
Try it yourself
- Create 10,000 items with
make_shared
and then with separateshared_ptr<Item>(new Item(...))
; time both approaches to compare. - Return
unique_ptr
from a factory function to practice move semantics without extra copies.
🔹 Custom Deleters: Managing Non‑Memory Resources
A Smart Pointer in C++ can free any kind of handle via a custom deleter—great for files, sockets, and mutexes.
#include <cstdio>
#include <memory>
#include <iostream>
using namespace std;
struct FileCloser {
void operator()(FILE* fp) const {
if (fp)
{ fclose(fp); cout << "File closed\n"; }
}
};
int main() {
unique_ptr<FILE, FileCloser> fp(fopen("out.txt", "w"));
if (!fp) { cout << "Open failed\n"; return 0; }
// automatic fclose at scope exit
fputs("Hello\n", fp.get());
}
Output
File closed
Explanation: The custom deleter runs in unique_ptr
’s destructor, ensuring fclose
is always called, not just memory delete
.
Try it yourself
- Replace
FileCloser
with a lambda deleter likeunique_ptr<FILE, decltype(&fclose)> fp(fopen(...), &fclose)
. - Adapt the pattern to a mock network socket handle and log “Socket closed” in the deleter.
🔹 Breaking Cycles with enable_shared_from_this
For callbacks that need a shared_ptr
to the current object, prefer inheriting
enable_shared_from_this<T>
and calling shared_from_this()
safely
(only when the object itself is owned by shared_ptr
).
#include <iostream>
#include <memory>
#include <functional>
using namespace std;
struct Task : enable_shared_from_this<Task> {
void runLater(function<void(shared_ptr<Task>)> cb) {
// obtain a shared_ptr to this if already owned by shared_ptr
cb(shared_from_this());
}
void work() const {
cout << "Working...\n";
}
};
int main() {
auto t = make_shared<Task>();
t->runLater([](shared_ptr<Task> self) {
self->work();
});
}
Output
Working...
Explanation: shared_from_this()
is safe only if the object is already managed by a
shared_ptr
. Avoid creating a fresh shared_ptr(this)
from within a raw‑constructed object.
Try it yourself
- Create a timer that stores the callback and executes it later using
shared_from_this()
to keep the task alive. - Experiment calling
shared_from_this()
on a stack‑allocated object and observe the exception; then fix by usingmake_shared
.
🔹 Performance Notes and Gotchas
unique_ptr
is as small and fast as a raw pointer (plus destructor semantics); prefer it by default.shared_ptr
adds a control block and atomic ref count; avoid unnecessary sharing on hot paths.make_shared
improves locality and reduces allocations; but cannot use custom deleter with it (use direct constructor if needed).- Never create two independent
shared_ptr
from the same raw pointer; always have a single owning creator. - Prefer passing
const T&
or raw non‑owningT*
/T&
to functions that don’t need ownership, to avoid accidental ref count bumps.
🔹 Best Practices for Smart Pointer in C++
- Default to
unique_ptr
; switch toshared_ptr
only when multiple owners are required. - Use
weak_ptr
to break ownership cycles and to observe optional lifetime safely. - Prefer
make_unique
/make_shared
over rawnew
for safety and clarity. - Attach custom deleters for non‑memory resources (files, sockets, mutexes).
- Do not store pointers or references to elements across vector reallocation unless capacity is reserved.
🔹 Mini Project: Image Cache with Smart Pointer in C++
This cache keeps images alive while referenced by clients and allows automatic cleanup once all shared_ptr
owners are gone;
lookups store non‑owning weak_ptr
inside the cache map.
#include <iostream>
#include <memory>
#include <string>
#include <unordered_map>
using namespace std;
struct Image {
string path;
explicit Image(string p) : path(move(p)) {
cout << "Load " << path << "\n";
}
~Image() {
cout << "Free " << path << "\n";
}
void draw() const {
cout << "Draw " << path << "\n";
}
};
class ImageCache {
unordered_map<string, weak_ptr<Image>> table;
public:
shared_ptr<Image> get(const string& key) {
if (auto it = table.find(key); it != table.end()) {
if (auto sp = it->second.lock()) {
return sp; // reuse
}
}
auto sp = make_shared<Image>(key); // load fresh
table[key] = sp; // remember weakly
return sp;
}
void sweep() {
// Optional: remove expired entries
for (auto it = table.begin(); it != table.end(); ) {
if (it->second.expired()) {
it = table.erase(it);
} else {
++it;
}
}
}
};
int main() {
ImageCache cache;
{
auto a = cache.get("logo.png");
a->draw();
auto b = cache.get("logo.png"); // reused
b->draw();
}
cache.sweep(); // drop expired records
// Fetch again will reload
auto c = cache.get("logo.png");
c->draw();
}
Output
Load logo.png
Draw logo.png
Draw logo.png
Free logo.png
Load logo.png
Draw logo.png
Explanation: The cache holds only weak_ptr
entries. When clients drop all shared_ptr
references,
the image is freed, and the next request reloads it automatically.
Try it yourself
- Add a capacity limit and evict oldest entries first.
- Record hits/misses and print cache statistics after each
get
.
🔹 FAQ: Smart Pointer in C++
Q1. Which smart pointer should be used by default?
unique_ptr
. It is lightweight, safe, and expresses exclusive ownership; switch to shared_ptr
only when multiple owners are required.
Q2. When is weak_ptr
necessary?
Use weak_ptr
to observe an object without extending its lifetime, and to break reference cycles between shared_ptr
owners.
Q3. Is make_shared
always better?
It often is, due to fewer allocations and better locality, but if a custom deleter or special allocator is needed, construct shared_ptr
directly.
Q4. Can a raw pointer be stored inside a smart pointer later?
Yes, but let exactly one smart pointer become the sole owner of a given raw pointer and avoid creating multiple owners from the same raw pointer.
Q5. How to pass smart pointers to functions?
For non‑owning APIs, pass T&
or const T&
/raw T*
. Pass shared_ptr<T>
only if shared ownership transfer or extension is intended.
Q6. Do smart pointers add runtime cost?
unique_ptr
is near zero‑overhead; shared_ptr
incurs ref‑count updates (often atomic). Measure before optimizing.
Q7. Is it safe to use shared_from_this()
in the constructor?
No. It is only valid after the object is owned by a shared_ptr
. Use a factory like make_shared<T>()
to create instances.
Q8. How to manage non‑memory resources?
Attach a custom deleter (function object or lambda) to unique_ptr
or shared_ptr
so the correct release call (e.g., fclose
) runs at scope exit.
🔹 Wrapping Up
A well‑applied Smart Pointer in C++ makes code safer, simpler, and exception‑proof. Default to unique_ptr
, share deliberately with shared_ptr
, observe with weak_ptr
, and leverage make_unique
/make_shared
for clean construction. Practice the “Try it yourself” tasks above to build strong, real‑world instincts.