C++ · lesson 13 of 24
Beginner
6Intermediate
11- 5 Operators and Operator Overloading in C++
- 8 Classes and Object-Oriented Programming in C++
- 9 Inheritance and Polymorphism in C++
- 11 STL Containers in C++
- 12 STL Algorithms in C++
- 13 Smart Pointers in C++
- 15 Lambdas and Functional Programming in C++
- 18 Error Handling in C++
- 19 File I/O in C++
- 23 C++ Interview Preparation: Top 35 Questions
- 24 Static Members in C++
Advanced
7Smart Pointers in C++
unique_ptr, shared_ptr and weak_ptr run for real: a leak LeakSanitizer catches, reference counts, a shared_ptr cycle that never frees, and custom deleters.
By SkillByExample · Updated
Raw owning pointers put every cleanup path on you: each early return and each exception needs its own delete. Smart pointers tie the delete to a destructor, so it runs however the scope is left. The modern rule: don’t write new or delete in application code.
Why Raw Pointers Leak
An exception skips the delete that the happy path would have reached. Both functions below throw; watch what each one leaves behind.
#include <iostream>
#include <memory>
#include <stdexcept>
void risky_raw(bool fail) {
int* p = new int(42);
if (fail) throw std::runtime_error("raw failed"); // skips the delete below
delete p;
}
void safe_smart(bool fail) {
auto p = std::make_unique<int>(42); // freed on every exit path
if (fail) throw std::runtime_error("smart failed");
}
int main() {
for (auto f : {risky_raw, safe_smart}) {
try { f(true); } catch (const std::exception& e) { std::cout << "caught: " << e.what() << "\n"; }
}
}
$ g++ -std=c++20 -Wall -Wextra -o leak leak.cpp
$ ./leak
caught: raw failed
caught: smart failed
The output looks identical — nothing on screen says anything went wrong. LeakSanitizer does:
$ g++ -std=c++20 -g -fsanitize=address -o leak leak.cpp
$ ./leak 2>&1 | grep -E 'ERROR|Direct|#1|SUMMARY'
==6059==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 4 byte(s) in 1 object(s) allocated from:
#1 0x55f649a68426 in risky_raw(bool) /tmp/demo/leak.cpp:6
SUMMARY: AddressSanitizer: 4 byte(s) leaked in 1 allocation(s).
The four-byte int from risky_raw (line 6) was never freed, because throw left the function before delete p. safe_smart doesn’t appear: the unique_ptr’s destructor ran during stack unwinding.
std::unique_ptr — Exclusive Ownership
unique_ptr owns one object and deletes it in its destructor. It can’t be copied, only moved, so there is always exactly one owner.
#include <iostream>
#include <memory>
#include <stdexcept>
#include <string>
struct Connection {
std::string host;
explicit Connection(std::string h) : host(std::move(h)) { std::cout << "connect " << host << "\n"; }
~Connection() { std::cout << "disconnect " << host << "\n"; }
};
std::unique_ptr<Connection> open(std::string host) { // factory: caller owns result
return std::make_unique<Connection>(std::move(host));
}
int main() {
try {
auto conn = open("db-a");
throw std::runtime_error("query failed"); // conn still cleaned up
} catch (const std::exception& e) {
std::cout << "caught: " << e.what() << "\n";
}
auto conn = open("db-b");
auto conn2 = std::move(conn); // ownership moves, no copy
std::cout << std::boolalpha << "conn null: " << (conn == nullptr)
<< ", conn2 -> " << conn2->host << "\n";
std::cout << "sizeof(unique_ptr) = " << sizeof(conn2)
<< ", sizeof(raw pointer) = " << sizeof(Connection*) << "\n";
}
$ g++ -std=c++20 -Wall -Wextra -o unique unique.cpp
$ ./unique
connect db-a
disconnect db-a
caught: query failed
connect db-b
conn null: true, conn2 -> db-b
sizeof(unique_ptr) = 8, sizeof(raw pointer) = 8
disconnect db-b
db-a was disconnected before the catch block ran: unwinding destroyed conn on the way out of the try. Returning a unique_ptr from open hands ownership to the caller with no copy. After std::move, conn is null and conn2 owns the connection, which is released at the end of main. And a unique_ptr is the same 8 bytes as a raw pointer — the safety is free.
std::shared_ptr — Shared Ownership
shared_ptr counts its owners; the object is destroyed when the last one lets go.
#include <iostream>
#include <memory>
#include <string>
#include <vector>
struct Image {
std::string path;
explicit Image(std::string p) : path(std::move(p)) {}
~Image() { std::cout << "free " << path << "\n"; }
};
int main() {
std::vector<std::shared_ptr<Image>> gallery;
{
auto img = std::make_shared<Image>("background.png");
std::cout << "count " << img.use_count() << "\n";
{
auto thumb = img; // copy: count 2
gallery.push_back(img); // copy: count 3
std::cout << "count " << img.use_count() << "\n";
}
std::cout << "count " << img.use_count() << "\n";
} // img gone, gallery still owns it
std::cout << "scope left, gallery holds it: count " << gallery[0].use_count() << "\n";
gallery.clear(); // last owner: object freed now
std::cout << "sizeof(shared_ptr) = " << sizeof(std::shared_ptr<Image>) << "\n";
}
$ g++ -std=c++20 -Wall -Wextra -o shared shared.cpp
$ ./shared
count 1
count 3
count 2
scope left, gallery holds it: count 1
free background.png
sizeof(shared_ptr) = 16
Copying a shared_ptr bumps the count (1 → 3 with thumb and the vector entry), and destroying a copy lowers it. When img itself went out of scope the image survived, because the vector still owned it; gallery.clear() dropped the last owner and free background.png printed right then. A shared_ptr is two pointers (16 bytes) — one to the object, one to the shared control block holding the counts — and each copy updates the count atomically, so pass it around only when you mean to share ownership.
std::weak_ptr — Breaking Cycles
If two objects own each other through shared_ptr, neither count can reach zero:
#include <iostream>
#include <memory>
struct StrongNode {
int id;
std::shared_ptr<StrongNode> other; // owns its neighbour
~StrongNode() { std::cout << " ~StrongNode " << id << "\n"; }
};
struct WeakNode {
int id;
std::shared_ptr<WeakNode> next; // owns forward
std::weak_ptr<WeakNode> prev; // observes backward
~WeakNode() { std::cout << " ~WeakNode " << id << "\n"; }
};
int main() {
std::cout << "strong cycle:\n";
{
auto a = std::make_shared<StrongNode>(1);
auto b = std::make_shared<StrongNode>(2);
a->other = b;
b->other = a;
std::cout << " a count " << a.use_count() << "\n";
}
std::cout << "weak back-pointer:\n";
{
auto a = std::make_shared<WeakNode>(1);
auto b = std::make_shared<WeakNode>(2);
a->next = b;
b->prev = a;
std::cout << " a count " << a.use_count() << "\n";
}
std::cout << "end of main\n";
}
$ g++ -std=c++20 -Wall -Wextra -o cycle cycle.cpp
$ ./cycle
strong cycle:
a count 2
weak back-pointer:
a count 1
~WeakNode 1
~WeakNode 2
end of main
The strong pair never printed a destructor, even at the end of main — both nodes leaked, each kept alive by the other (a count 2: the local plus b->other). In the second pair prev is a weak_ptr, which doesn’t count, so a has one owner, and both nodes are destroyed when the locals go. Rule of thumb: parent-to-child links own, child-to-parent links observe.
A weak_ptr can’t be dereferenced directly; lock() gives you a temporary shared_ptr if the object still exists:
#include <iostream>
#include <memory>
int main() {
auto owner = std::make_shared<int>(42);
std::weak_ptr<int> observer = owner;
if (auto p = observer.lock()) // temporary shared ownership
std::cout << "alive: " << *p << ", count while locked " << owner.use_count() << "\n";
std::cout << "count after lock scope " << owner.use_count() << "\n";
owner.reset(); // last owner lets go
std::cout << std::boolalpha << "expired: " << observer.expired() << "\n";
if (auto p = observer.lock()) std::cout << "still " << *p << "\n";
else std::cout << "lock() returned null\n";
}
$ g++ -std=c++20 -Wall -Wextra -o weak weak.cpp
$ ./weak
alive: 42, count while locked 2
count after lock scope 1
expired: true
lock() returned null
While locked, the count rose to 2, so the object couldn’t disappear mid-use. After the last owner’s reset(), expired() is true and lock() returns null — the check is built into the type.
Custom Deleters
Smart pointers can release things other than newed memory — here a C FILE* that must be fclosed.
#include <cstdio>
#include <iostream>
#include <memory>
int main() {
auto closer = [](std::FILE* f) { std::cout << "fclose called\n"; std::fclose(f); };
{
std::unique_ptr<std::FILE, decltype(closer)> log(std::fopen("app.log", "w"), closer);
std::fputs("hello\n", log.get()); // .get() for C APIs
std::cout << "writing...\n";
} // closer runs here
using FnPtrDeleter = std::unique_ptr<std::FILE, int (*)(std::FILE*)>;
std::cout << "sizeof with lambda deleter: " << sizeof(std::unique_ptr<std::FILE, decltype(closer)>)
<< ", with function-pointer deleter: " << sizeof(FnPtrDeleter) << "\n";
std::shared_ptr<std::FILE> shared(std::fopen("app.log", "r"),
[](std::FILE* f) { std::cout << "shared fclose\n"; std::fclose(f); });
char line[16];
std::fgets(line, sizeof line, shared.get());
std::cout << "read back: " << line;
}
$ g++ -std=c++20 -Wall -Wextra -o deleters deleters.cpp
$ ./deleters
writing...
fclose called
sizeof with lambda deleter: 8, with function-pointer deleter: 16
read back: hello
shared fclose
The unique_ptr’s deleter ran at the closing brace, so the file was flushed and closed before being reopened for reading. The deleter is part of a unique_ptr’s type: a captureless lambda adds nothing (8 bytes), while a function-pointer deleter must be stored (16 bytes). shared_ptr keeps its deleter in the control block, so the type stays plain std::shared_ptr<std::FILE>. .get() hands the raw pointer to C APIs without giving up ownership.
Passing Smart Pointers to Functions
The parameter type says what a function does with ownership.
#include <iostream>
#include <memory>
struct Widget { int id = 7; ~Widget() { std::cout << " ~Widget\n"; } };
void use(const Widget& w) { std::cout << " use: borrows widget " << w.id << "\n"; }
void maybe_use(const Widget* w) { std::cout << " maybe_use: " << (w ? "got one" : "null") << "\n"; }
void consume(std::unique_ptr<Widget> w) { std::cout << " consume: now owns " << w->id << "\n"; }
void share(std::shared_ptr<Widget> w) { std::cout << " share: count " << w.use_count() << "\n"; }
void peek(const std::shared_ptr<Widget>& w) { std::cout << " peek: count " << w.use_count() << "\n"; }
int main() {
auto u = std::make_unique<Widget>();
use(*u);
maybe_use(u.get());
maybe_use(nullptr);
consume(std::move(u)); // ~Widget runs when consume returns
std::cout << "after consume, u is " << (u ? "set" : "null") << "\n";
auto s = std::make_shared<Widget>();
share(s); // by value: +1 while the call runs
peek(s); // by const&: no count change
std::cout << "end of main\n";
}
$ g++ -std=c++20 -Wall -Wextra -o passing passing.cpp
$ ./passing
use: borrows widget 7
maybe_use: got one
maybe_use: null
consume: now owns 7
~Widget
after consume, u is null
share: count 2
peek: count 1
end of main
~Widget
use and maybe_use only borrow, so they take a reference or raw pointer and don’t care how the widget is owned. consume takes a unique_ptr by value, which forces the caller’s std::move and makes the transfer visible — the widget died when consume returned. share takes a shared_ptr by value and holds a second reference during the call (count 2); peek takes it by const& and doesn’t touch the count. If a function only uses the object, don’t make it take a smart pointer at all.
Common Pitfalls
Two shared_ptrs built from the same raw pointer each start their own count, so both delete the object:
#include <iostream>
#include <memory>
struct Widget { ~Widget() { std::cout << "~Widget" << std::endl; } };
int main() {
Widget* raw = new Widget();
std::shared_ptr<Widget> p1(raw);
std::shared_ptr<Widget> p2(raw); // a second, independent control block
std::cout << "p1 count " << p1.use_count() << ", p2 count " << p2.use_count() << std::endl;
}
$ g++ -std=c++20 -Wall -Wextra -o double_owner double_owner.cpp
$ ./double_owner; echo "exit code: $?"
p1 count 1, p2 count 1
~Widget
~Widget
free(): double free detected in tcache 2
Aborted
exit code: 134
Each count was 1, each owner ran ~Widget, and the second delete aborted the program. Create the object with make_shared and copy the shared_ptr, never the raw pointer.
Getting a shared_ptr to this requires std::enable_shared_from_this, and the object must already be owned by one:
#include <iostream>
#include <memory>
struct Session : std::enable_shared_from_this<Session> {
std::shared_ptr<Session> keep_alive() { return shared_from_this(); }
};
int main() {
auto s = std::make_shared<Session>();
auto self = s->keep_alive(); // shares s's control block
std::cout << "count " << s.use_count() << "\n";
Session on_stack; // not owned by any shared_ptr
try {
on_stack.keep_alive();
} catch (const std::bad_weak_ptr& e) {
std::cout << "bad_weak_ptr: " << e.what() << "\n";
}
}
$ g++ -std=c++20 -Wall -Wextra -o from_this from_this.cpp
$ ./from_this
count 2
bad_weak_ptr: bad_weak_ptr
For s, shared_from_this() joined the existing control block (count 2) — the pattern async code uses to keep an object alive until its callback runs. On a stack object there is no control block to join, and C++17 guarantees std::bad_weak_ptr.
Choosing the Right Pointer
| Situation | Use |
|---|---|
| Single owner | unique_ptr (the default) |
| Genuinely shared ownership | shared_ptr via make_shared |
| Observer that must not extend lifetime, or a back-pointer | weak_ptr |
| Function that only uses the object | reference, or raw pointer if null is allowed |
| Function that takes ownership | unique_ptr by value |
Practice
-
Rewrite
risky_rawinleak.cppwithout a smart pointer or a leak, and withouttry/catchinside it.Solution
void risky_raw(bool fail) { int value = 42; // no heap allocation needed if (fail) throw std::runtime_error("raw failed"); }The simplest fix for a small object is not to heap-allocate it. When the heap is required, use
std::make_unique; a rawnewwith manualtry/catchcleanup is what smart pointers exist to replace. -
Fix the strong cycle in
cycle.cppwithout changingStrongNode’s type, by breaking the link before the scope ends.Solution
Add
a->other.reset();before the closing brace.b’s count drops to 1 (just the local), and when the locals are destroyed both counts reach zero:~StrongNode 2and then~StrongNode 1print. It works, but it relies on remembering to break the cycle by hand —weak_ptrmakes it automatic. -
Make a
std::unique_ptr<int[]>holding five ints, fill it with squares, and print them.Solution
auto arr = std::make_unique<int[]>(5); // value-initialized: all 0 for (int i = 0; i < 5; ++i) arr[i] = i * i; for (int i = 0; i < 5; ++i) std::cout << arr[i] << " "; // 0 1 4 9 16The array form calls
delete[]and providesoperator[]. In most code astd::vector<int>is still the better choice. -
In
passing.cpp, changeshareto takeconst std::shared_ptr<Widget>&and store it into a globalstd::vector<std::shared_ptr<Widget>>. What count doespeekthen print?Solution
- Storing the reference into the vector makes a copy, so the vector is a second owner that outlives the call. Taking
shared_ptrby value and thenstd::move-ing it into the vector would do the same with one fewer atomic increment.
- Storing the reference into the vector makes a copy, so the vector is a second owner that outlives the call. Taking
-
Fix
double_owner.cppso it prints~Widgetonce and exits with code 0.Solution
auto p1 = std::make_shared<Widget>(); std::shared_ptr<Widget> p2 = p1; // copy the shared_ptr, not the raw pointerBoth now share one control block and print
count 2; the single~Widgetruns when the last of them is destroyed.