Module 12: Smart Pointers
Chapter 12 • Advanced
Smart Pointers
Smart pointers are modern C++ (C++11) objects that manage memory automatically, eliminating many common memory management errors. They're essential for writing safe, modern C++ code.
Why Smart Pointers?
Problems with Raw Pointers:
- ❌ Memory leaks (forgetting to delete)
- ❌ Double deletion (deleting twice)
- ❌ Dangling pointers (using deleted memory)
- ❌ Exception safety issues
Smart Pointers Solve:
- ✅ Automatic memory management
- ✅ Exception safety
- ✅ No memory leaks
- ✅ Clear ownership semantics
Types of Smart Pointers
C++ provides three main smart pointer types:
- `unique_ptr`: Exclusive ownership
- `shared_ptr`: Shared ownership
- `weak_ptr`: Non-owning reference
unique_ptr
Exclusive ownership - only one unique_ptr can own an object at a time.
Basic Usage
#include <memory>
using namespace std;
// Creating unique_ptr
unique_ptr<int> ptr1(new int(10));
unique_ptr<int> ptr2 = make_unique<int>(20); // Preferred (C++14)
// Accessing
cout << *ptr1 << endl; // Dereference
cout << ptr1.get() << endl; // Get raw pointer
// Automatic cleanup when out of scope
Key Features
- Exclusive Ownership: Cannot copy, only move
- Automatic Deletion: Deleted when
unique_ptrgoes out of scope - Custom Deleters: Can specify custom cleanup
- Array Support: Can manage arrays
Examples
// Cannot copy
unique_ptr<int> ptr1 = make_unique<int>(10);
// unique_ptr<int> ptr2 = ptr1; // Error! Cannot copy
// Can move
unique_ptr<int> ptr2 = move(ptr1); // ptr1 is now nullptr
// Custom deleter
unique_ptr<FILE, decltype(&fclose)> file(fopen("data.txt", "r"), fclose);
// Array
unique_ptr<int[]> arr = make_unique<int[]>(10);
arr[0] = 100;
shared_ptr
Shared ownership - multiple shared_ptr objects can own the same resource.
Basic Usage
#include <memory>
using namespace std;
// Creating shared_ptr
shared_ptr<int> ptr1 = make_shared<int>(10); // Preferred
shared_ptr<int> ptr2 = ptr1; // Both point to same object
// Reference counting
cout << ptr1.use_count() << endl; // 2 (ptr1 and ptr2)
// Automatic cleanup when last shared_ptr is destroyed
Reference Counting
shared_ptr uses reference counting:
- Count increases when copied
- Count decreases when destroyed or reset
- Object deleted when count reaches 0
shared_ptr<int> ptr1 = make_shared<int>(10);
cout << ptr1.use_count() << endl; // 1
{
shared_ptr<int> ptr2 = ptr1;
cout << ptr1.use_count() << endl; // 2
} // ptr2 destroyed, count back to 1
ptr1.reset(); // Object deleted (count = 0)
Custom Deleters
shared_ptr<int> ptr(new int[10], [](int* p) { delete[] p; });
weak_ptr
Non-owning reference - doesn't affect reference count, used to break circular references.
Problem: Circular References
struct Node {
shared_ptr<Node> next;
// Circular reference problem!
};
shared_ptr<Node> node1 = make_shared<Node>();
shared_ptr<Node> node2 = make_shared<Node>();
node1->next = node2;
node2->next = node1; // Circular! Memory leak!
Solution: weak_ptr
struct Node {
weak_ptr<Node> next; // Use weak_ptr
};
shared_ptr<Node> node1 = make_shared<Node>();
shared_ptr<Node> node2 = make_shared<Node>();
node1->next = node2;
node2->next = node1; // No circular reference!
// Accessing weak_ptr
if (auto locked = node1->next.lock()) {
// Use locked shared_ptr
}
Comparison Table
| Feature | unique_ptr | shared_ptr | weak_ptr |
|---|---|---|---|
| **Ownership** | Exclusive | Shared | None |
| **Copyable** | No | Yes | Yes |
| **Movable** | Yes | Yes | Yes |
| **Reference Count** | N/A | Yes | No |
| **Overhead** | Minimal | Small | Small |
| **Use Case** | Single owner | Multiple owners | Break cycles |
Best Practices
- ✅ Prefer `make_unique` and `make_shared` over
new - ✅ Use `unique_ptr` by default (simpler, faster)
- ✅ Use `shared_ptr` only when shared ownership is needed
- ✅ Use `weak_ptr` to break circular references
- ✅ Don't mix raw and smart pointers for same object
- ✅ Avoid circular references with
shared_ptr - ✅ Don't create `shared_ptr` from raw pointer multiple times
- ✅ Use `weak_ptr` to check if object still exists
Common Patterns
Pattern 1: Factory Function
unique_ptr<MyClass> createObject() {
return make_unique<MyClass>();
}
Pattern 2: Polymorphism
class Base { };
class Derived : public Base { };
unique_ptr<Base> ptr = make_unique<Derived>();
Pattern 3: Container of Smart Pointers
vector<unique_ptr<MyClass>> objects;
objects.push_back(make_unique<MyClass>());
make_unique vs new
Prefer `make_unique`:
// Good
auto ptr = make_unique<int>(10);
// Avoid
unique_ptr<int> ptr(new int(10));
Benefits:
- Exception safe
- More efficient (single allocation for
shared_ptr) - Cleaner syntax
- Type deduction
Custom Deleters
Smart pointers support custom deleters:
// Function deleter
void customDelete(int* p) {
cout << "Deleting..." << endl;
delete p;
}
unique_ptr<int, decltype(&customDelete)> ptr(new int(10), customDelete);
// Lambda deleter
unique_ptr<int, function<void(int*)>> ptr2(
new int(10),
[](int* p) { delete p; }
);
Common Mistakes
- ❌ Creating multiple
shared_ptrfrom same raw pointer - ❌ Circular references with
shared_ptr - ❌ Using
shared_ptrwhenunique_ptris enough - ❌ Not checking
weak_ptrbefore use - ❌ Mixing raw and smart pointers
- ❌ Returning raw pointer from smart pointer function
Migration from Raw Pointers
Old Style:
int* ptr = new int(10);
// ... use ptr ...
delete ptr; // Easy to forget!
Modern Style:
auto ptr = make_unique<int>(10);
// ... use ptr ...
// Automatically deleted!
Next Module
In Module 13, we'll learn about Exception Handling - how to handle errors gracefully in C++!
Hands-on Examples
unique_ptr Basics
#include <iostream>
#include <memory>
using namespace std;
class Resource {
private:
int id;
public:
Resource(int i) : id(i) {
cout << "Resource " << id << " created" << endl;
}
~Resource() {
cout << "Resource " << id << " destroyed" << endl;
}
void use() {
cout << "Using resource " << id << endl;
}
};
int main() {
cout << "=== Creating unique_ptr ===" << endl;
{
unique_ptr<Resource> ptr1 = make_unique<Resource>(1);
ptr1->use();
// Cannot copy
// unique_ptr<Resource> ptr2 = ptr1; // Error!
// Can move
unique_ptr<Resource> ptr2 = move(ptr1);
if (ptr1 == nullptr) {
cout << "ptr1 is now nullptr" << endl;
}
ptr2->use();
// Automatic cleanup when out of scope
}
cout << "Resources destroyed automatically" << endl;
// Array support
cout << "\n=== Array unique_ptr ===" << endl;
{
unique_ptr<int[]> arr = make_unique<int[]>(5);
for (int i = 0; i < 5; i++) {
arr[i] = i * 10;
}
cout << "Array: ";
for (int i = 0; i < 5; i++) {
cout << arr[i] << " ";
}
cout << endl;
}
return 0;
}unique_ptr provides exclusive ownership. Cannot be copied, only moved. Automatically deletes managed object when out of scope. Use make_unique for creation. Supports arrays with unique_ptr<T[]>. Perfect for single-owner scenarios.
Practice with Programs
Reinforce your learning with hands-on practice programs