C++: Smart Pointers Advanced
Last updated: 2026-08-26
In lesson 45, we learned about move semantics.
Now, we'll dive deep into smart pointers — the core tool for modern C++ memory management.
With smart pointers, you never have to worry about memory leaks again.
1. Smart Pointer Overview
(1) 1.1 What Are Smart Pointers?
Smart pointers are template classes that manage dynamic memory, automatically releasing memory (RAII).
Three types of smart pointers:
| Smart Pointer | Function | Use Case |
|---|---|---|
unique_ptr |
Exclusive ownership | Sole ownership of an object |
shared_ptr |
Shared ownership | Multiple places need access to the same object |
weak_ptr |
Weak reference | Breaking circular references |
2. unique_ptr
(1) 2.1 Basic Usage
unique_ptr is a smart pointer with exclusive ownership — it cannot be copied, only moved.
Example: unique_ptr basic usage (Difficulty ⭐)
▶ Example 2: Code example (Difficulty ⭐)
#include <iostream>
#include <memory>
int main() {
std::unique_ptr<int> p1(new int(42));
std::cout << *p1 << std::endl; // Output: 42
// std::unique_ptr<int> p2 = p1; // ❌ Error! Cannot copy
std::unique_ptr<int> p2 = std::move(p1); // ✅ Can move
std::cout << *p2 << std::endl; // Output: 42
return 0;
} // p2 automatically releases memory
Output:
(Program output)
(2) 2.2 Custom Deleter
Example: Using unique_ptr to manage a file (Difficulty ⭐⭐)
#include <iostream>
#include <memory>
#include <cstdio>
// Custom deleter: close file
struct FileDeleter {
void operator()(FILE* fp) const {
if (fp) {
fclose(fp);
std::cout << "File closed" << std::endl;
}
}
};
int main() {
std::unique_ptr<FILE, FileDeleter> file(fopen("test.txt", "w"));
// No need to manually fclose, unique_ptr automatically calls FileDeleter
return 0;
}
3. shared_ptr
(1) 3.1 Basic Usage
shared_ptr is a smart pointer with shared ownership, managing memory with reference counting.
Example: shared_ptr basic usage (Difficulty ⭐)
#include <iostream>
#include <memory>
int main() {
std::shared_ptr<int> p1 = std::make_shared<int>(42);
std::cout << "Reference count: " << p1.use_count() << std::endl; // 1
{
std::shared_ptr<int> p2 = p1; // Copy, reference count +1
std::cout << "Reference count: " << p1.use_count() << std::endl; // 2
} // p2 destroyed, reference count -1
std::cout << "Reference count: " << p1.use_count() << std::endl; // 1
return 0;
}
(2) 3.2 make_shared vs new
Recommended: Use std::make_shared instead of new.
| Comparison | new |
make_shared |
|---|---|---|
| Exception safety | May leak | Safe |
| Performance | Two allocations | One allocation |
| Code clarity | Verbose | Concise |
// Recommended
auto p1 = std::make_shared<int>(42);
// Not recommended
std::shared_ptr<int> p2(new int(42));
4. weak_ptr
(1) 4.1 Why Do We Need weak_ptr?
Problem: shared_ptr can cause circular references, leading to memory leaks.
Example: Circular reference (Difficulty ⭐⭐⭐)
#include <iostream>
#include <memory>
struct Node {
std::shared_ptr<Node> next; // Circular reference!
~Node() { std::cout << "Node destroyed" << std::endl; }
};
int main() {
auto n1 = std::make_shared<Node>();
auto n2 = std::make_shared<Node>();
n1->next = n2; // Circular reference
n2->next = n1;
return 0;
} // ❌ n1 and n2 will not be released (memory leak)
(2) 4.2 Breaking Circular References with weak_ptr
Solution: Replace one of the shared_ptr with weak_ptr.
struct Node {
std::weak_ptr<Node> next; // Use weak_ptr, does not increase reference count
~Node() { std::cout << "Node destroyed" << std::endl; }
};
int main() {
auto n1 = std::make_shared<Node>();
auto n2 = std::make_shared<Node>();
n1->next = n2; // Does not increase reference count
n2->next = n1;
return 0;
} // ✅ Released correctly
5. Smart Pointer Selection Guide
(1) 5.1 How to Choose?
| Scenario | Recommendation |
|---|---|
| Exclusive ownership | unique_ptr |
| Shared ownership | shared_ptr |
| Observer | weak_ptr or raw pointer |
| Arrays | unique_ptr<T> |
(2) 5.2 What Not to Do
| Mistake | Explanation |
|---|---|
| Don't initialize multiple shared_ptrs from the same raw pointer | Will cause double free |
| Don't mix raw pointers and smart pointers | Breaks RAII |
| Don't manually call delete | Let smart pointers manage it |
6. Practice: Managing Resources with Smart Pointers
▶ Example 1: Using smart pointers to manage a database connection (Difficulty ⭐⭐⭐)
#include <iostream>
#include <memory>
// Simulated database connection
class DatabaseConnection {
public:
DatabaseConnection() {
std::cout << "Connecting to database" << std::endl;
}
~DatabaseConnection() {
std::cout << "Disconnected" << std::endl;
}
void query(const std::string& sql) {
std::cout << "Executing SQL: " << sql << std::endl;
}
};
int main() {
// Use unique_ptr to manage exclusive resource
std::unique_ptrDatabaseConnection conn(new DatabaseConnection());
conn->query("SELECT * FROM users");
return 0;
} // Automatically disconnects
Output:
Connecting to database
Disconnected
Executing SQL:
❓ FAQ
unique_ptr is faster (no reference counting overhead). Prefer unique_ptr.Q: Is shared_ptr thread-safe? A: - Reference count modifications are thread-safe - But the pointed-to object is not thread-safe (needs mutex protection)
▶ Example 3: shared_ptr shared ownership (Difficulty ⭐)
#include <iostream>
#include <memory>
class Resource {
public:
Resource() { std::cout << "Construct" << std::endl; }
~Resource() { std::cout << "Destruct" << std::endl; }
};
int main() {
{
std::shared_ptr<Resource> p1 = std::make_shared<Resource>();
std::shared_ptr<Resource> p2 = p1; // Shared ownership
std::cout << "Reference count: " << p1.use_count() << std::endl;
}
// Leaves scope, automatically destructs
std::cout << "Program ended" << std::endl;
return 0;
}
Output:
Construct
Destruct
Reference count:
Program ended
shared_ptr shares ownership and automatically releases when the reference count reaches zero. Using make_shared is safer for creation.
| Key Point | Summary |
|---|---|
| unique_ptr | Exclusive ownership, cannot be copied |
| shared_ptr | Shared ownership, reference counting |
| weak_ptr | Weak reference, breaks circular references |
| make_shared | Recommended approach, exception-safe |
| Selection principle | Prefer unique_ptr, use shared_ptr when needed |
📖 Summary
- shared_ptr: Shared ownership, managed by reference counting
- unique_ptr: Exclusive ownership, non-copyable
- weak_ptr: Weak reference, does not increase reference count
- make_shared/make_unique: Recommended creation methods
📝 Exercises
-
Basic (Difficulty ⭐): Use
unique_ptrto manage a dynamically allocatedint, created withmake_unique. Try copying theunique_ptr(it should fail to compile), then usemoveinstead. -
Intermediate (Difficulty ⭐⭐): Use
shared_ptrto implement multiple objects sharing the same resource. Create twoshared_ptrs pointing to the same object, and outputuse_count()to observe reference count changes. -
Challenge (Difficulty ⭐⭐⭐): Use
weak_ptrto solve ashared_ptrcircular reference problem. Create classes A and B that holdshared_ptrs to each other, and observe the memory leak. Then switch toweak_ptrand verify correct release.
- unique_ptr has exclusive ownership, non-copyable but movable
- shared_ptr has shared ownership, managed by reference counting
- weak_ptrcombined with shared_ptr avoids circular references
- make_unique/make_shared are exception-safe ways to create
- Custom deleters handle special resource release
Next lesson: Template Metaprogramming Introduction (#47)