📝 Lesson 10: Smart Pointers
Smart pointers bring RAII to memory management in modern C++, using unique_ptr, shared_ptr, and weak_ptr to make leaks, dangling pointers, and double frees a thing of the past.
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Explain why raw pointers are error-prone and how smart pointers apply the RAII principle to manage memory automatically.
- Use
unique_ptrfor exclusive ownership, including move semantics,make_unique, and custom deleters. - Use
shared_ptrfor shared, reference-counted ownership and understand its performance trade-offs. - Use
weak_ptrto observe ashared_ptr-managed object without extending its lifetime, and to break reference cycles. - Apply smart pointer best practices and recognize common pitfalls: circular references, array/scalar mismatches, and misuse of
thisin constructors.
Estimated Time: 90–120 minutes
Project: Build a resource management system that combines unique_ptr, shared_ptr, and weak_ptr to safely manage file handles, threads, and pooled objects.
In This Lesson
Why Smart Pointers?
Smart pointers are like responsible pet owners - they automatically take care of their resources, feeding them when needed and cleaning up after them. No more memory leaks or dangling pointers!
The Smart Pointer Family
unique_ptr: Exclusive Ownership
unique_ptr is like having a single key to a house - only one person can own it at a time. When they're done, the house is automatically sold (memory freed)!
unique_ptr Examples
#include <memory>
#include <iostream>
using namespace std;
// Basic unique_ptr usage
void uniquePtrBasics() {
// Creating unique_ptr
unique_ptr<int> p1(new int(42)); // Direct initialization
auto p2 = make_unique<int>(42); // Preferred: make_unique
// Accessing the value
cout << *p2 << endl; // Dereference: 42
cout << p2.get() << endl; // Get raw pointer
// unique_ptr with arrays
auto arr = make_unique<int[]>(10); // Array of 10 ints
arr[0] = 100;
// Custom deleter
auto fileDeleter = [](FILE* f) {
if (f) fclose(f);
};
unique_ptr<FILE, decltype(fileDeleter)> file(
fopen("data.txt", "r"), fileDeleter
);
}
// Ownership transfer
unique_ptr<string> createMessage() {
auto msg = make_unique<string>("Hello from function!");
return msg; // Automatic move
}
void transferOwnership() {
auto p1 = make_unique<int>(42);
// unique_ptr<int> p2 = p1; // ERROR: Can't copy
unique_ptr<int> p2 = move(p1); // OK: Move ownership
if (!p1) {
cout << "p1 is now null" << endl;
}
// Reset and release
p2.reset(); // Delete object, set to null
p2.reset(new int(100)); // Delete old, own new
int* raw = p2.release(); // Release ownership
delete raw; // Now we must delete manually
}
// Using unique_ptr in classes
class Resource {
private:
unique_ptr<int[]> data;
size_t size;
public:
Resource(size_t n) : data(make_unique<int[]>(n)), size(n) {
// Initialize array
for (size_t i = 0; i < size; ++i) {
data[i] = i;
}
}
// No need for destructor - unique_ptr handles it!
// No need for copy constructor/assignment - deleted by default
// Move constructor (compiler-generated is fine)
Resource(Resource&&) = default;
Resource& operator=(Resource&&) = default;
int& operator[](size_t i) { return data[i]; }
};
weak_ptr: Breaking Cycles
weak_ptr is like having a business card - you can call the number to check if the business still exists, but the card itself doesn't keep the business running!
weak_ptr Examples
// Basic weak_ptr usage
void weakPtrBasics() {
shared_ptr<int> sp = make_shared<int>(42);
weak_ptr<int> wp = sp; // Create weak_ptr
// Check if still valid
if (!wp.expired()) {
// Convert to shared_ptr to use
if (auto locked = wp.lock()) {
cout << "Value: " << *locked << endl;
// locked keeps object alive in this scope
}
}
sp.reset(); // Destroy object
if (wp.expired()) {
cout << "Object has been destroyed" << endl;
}
}
// Observer pattern with weak_ptr
class Subject;
class Observer {
private:
weak_ptr<Subject> subject;
public:
void observe(shared_ptr<Subject> s) {
subject = s;
}
void update() {
if (auto s = subject.lock()) {
// Subject still exists, can safely use
cout << "Updating based on subject" << endl;
} else {
cout << "Subject has been destroyed" << endl;
}
}
};
🔭 Beyond the basics (preview)
Two classic weak_ptr patterns build on tools from later lessons: a tree where each child refers back to its parent weakly (uses vector — the STL, Lesson 16 — plus enable_shared_from_this), and a cache of weak_ptrs whose entries expire automatically (uses std::map and templates, Lesson 15). Revisit them once you've met those — the weak_ptr idea is identical; only the containers around it are new.
Smart Pointer Best Practices
Common Pitfalls and Solutions
Performance Considerations
// Performance comparison
class PerformanceDemo {
public:
// unique_ptr: Zero overhead
void uniquePtrPerf() {
unique_ptr<int> up(new int(42));
// Same performance as raw pointer
// No reference counting
// Inline destructor
}
// shared_ptr: Reference counting overhead
void sharedPtrPerf() {
shared_ptr<int> sp(new int(42));
// Atomic reference counting (thread-safe)
// Control block allocation
// Virtual destructor call
}
// make_shared optimization
void makeSharedOptimization() {
// Two allocations: object + control block
shared_ptr<int> sp1(new int(42));
// Single allocation: object + control block together
auto sp2 = make_shared<int>(42); // More cache-friendly
}
// Moving vs copying
void moveVsCopy() {
auto sp1 = make_shared<LargeObject>();
// Copying: Atomic increment/decrement
shared_ptr<LargeObject> sp2 = sp1; // Slower
// Moving: No atomic operations
shared_ptr<LargeObject> sp3 = move(sp1); // Faster
}
};
💡 Smart pointers can also use custom allocators (for example pooled allocation via allocate_shared) in performance-critical code — a generic technique that builds on Templates (Lesson 15) and the STL (Lesson 16).
Practice Exercise: Resource Manager
🏋️ Manage a Resource with Smart Pointers
Practice all three smart pointers on a single simple class — no templates, threads, or containers required.
Instructions:
- Write a small
class Resourcewhose constructor and destructor each print a message, so you can see exactly when it's created and freed. - unique_ptr: create one with
make_uniqueinside a scope; confirm it's destroyed automatically when the scope ends — nodelete. - shared_ptr: create one with
make_shared, make a secondshared_ptrto it, printuse_count(), and confirm it's freed only after both owners are gone. - weak_ptr: make a
weak_ptrto the shared resource, access it withlock(), and show it reportsexpired()once the owners are gone.
Starter Code:
#include <iostream>
#include <memory>
#include <string>
using namespace std;
class Resource {
string name;
public:
Resource(string n) : name(n) { cout << "Created " << name << '\n'; }
~Resource() { cout << "Destroyed " << name << '\n'; }
void use() { cout << "Using " << name << '\n'; }
};
int main() {
// TODO: unique_ptr in a scope
// TODO: shared_ptr + a second owner, print use_count()
// TODO: weak_ptr, lock(), expired()
return 0;
}
💡 Hint
auto u = make_unique<Resource>("A"); frees automatically at scope end. For sharing: auto s1 = make_shared<Resource>("B"); auto s2 = s1; then s1.use_count() is 2. A weak_ptr<Resource> w = s1; does not raise the count; call w.lock() to get a temporary shared_ptr (check it for null), and w.expired() becomes true once all shared owners are gone.
✅ Solution
#include <iostream>
#include <memory>
#include <string>
using namespace std;
class Resource {
string name;
public:
Resource(string n) : name(n) { cout << "Created " << name << '\n'; }
~Resource() { cout << "Destroyed " << name << '\n'; }
void use() { cout << "Using " << name << '\n'; }
};
int main() {
// unique_ptr — exclusive ownership, auto-freed at scope end
{
auto u = make_unique<Resource>("Unique");
u->use();
} // "Destroyed Unique" prints here
// shared_ptr — shared ownership, freed when the last owner goes
weak_ptr<Resource> observer;
{
auto s1 = make_shared<Resource>("Shared");
auto s2 = s1; // second owner
cout << "use_count = " << s1.use_count() << '\n'; // 2
observer = s1; // weak_ptr adds no owner
cout << "use_count = " << s1.use_count() << '\n'; // still 2
if (auto locked = observer.lock()) // temporary owner
locked->use();
} // both s1 and s2 gone -> "Destroyed Shared" prints here
cout << "expired? " << observer.expired() << '\n'; // 1 (true)
return 0;
}
🔭 Optional Stretch (peek ahead)
Optional — these lean on later lessons. Real resource managers combine smart pointers with tools you'll meet soon: a custom deleter to close a FILE* or socket; storing owners in a vector<unique_ptr<T>> (containers arrive in Lesson 16: STL); a generic object pool via Lesson 15: Templates; and a thread pool via Lesson 19: Multithreading. Come back and build a full ResourceManager once you've met those.
Looking Ahead: Smart Pointers in Containers
The three smart pointers you just learned become even more powerful once you can store many objects at once and model relationships between types — a pairing you'll unlock in later lessons. Here's a preview of where they lead:
🔭 A preview — you'll write this soon
A hallmark of modern C++ is a container of owning smart pointers holding a family of related types — for example a vector<unique_ptr<Shape>> that stores circles, rectangles, and triangles together and calls the right draw() on each. That one line combines three things you haven't met yet:
- Inheritance & Polymorphism (Lesson 11) — the
Shapebase type and its derived shapes. - Templates (Lesson 15) — how a single container works for any element type.
- The STL (Lesson 16) —
std::vector,std::map, and friends.
Come back once you've met those and the pattern will read naturally. The key takeaway for now: smart pointers are what make such containers safe — every element cleans itself up, with no manual delete anywhere.
Modern C++ Features
Challenge Exercise: Memory Pool
🏋️ Advanced Challenge: Break a Reference Cycle
Two objects that own each other with shared_ptr will leak — their reference counts never reach zero. Reproduce it, then fix it with weak_ptr.
- Write a
Parentthat holds ashared_ptr<Child>and aChildthat holds ashared_ptr<Parent>. Give both a destructor that prints a message. - Create one of each with
make_shared, link them to each other, then let them go out of scope. Notice the destructors never run — that's the leak. - Fix it by changing one side (e.g.
Child's link back toParent) to aweak_ptr. Run again and confirm both destructors now fire.
💡 Hint
A cycle pins each object's use_count at 1 forever. A weak_ptr refers to an object without raising its count, so breaking one direction of the cycle lets both counts reach zero. Access the weak side with lock() when you need it.
✅ Solution
#include <iostream>
#include <memory>
using namespace std;
struct Child; // forward declaration
struct Parent {
shared_ptr<Child> child;
~Parent() { cout << "Parent destroyed\n"; }
};
struct Child {
weak_ptr<Parent> parent; // weak_ptr breaks the cycle (was shared_ptr)
~Child() { cout << "Child destroyed\n"; }
};
int main() {
{
auto p = make_shared<Parent>();
auto c = make_shared<Child>();
p->child = c; // Parent owns Child
c->parent = p; // Child only observes Parent (weak)
} // both destructors run here
cout << "Done\n";
return 0;
}
With both links as shared_ptr, neither destructor prints — the objects leak. Making Child::parent a weak_ptr restores proper cleanup.
🎯 Quick Quiz
Question 1: Given auto p1 = make_unique<int>(42);, why does unique_ptr<int> p2 = p1; fail to compile?
Question 2: Two Node objects are linked with node1->next = node2; and node2->prev = node1;, where both next and prev are shared_ptr<Node>. What problem does this create, and how do you fix it?
Question 3: Why is make_shared<T>(...) generally preferred over shared_ptr<T> sp(new T(...));?
Summary
🎉 Key Takeaways
- Smart pointers provide automatic memory management
- unique_ptr for exclusive ownership - use by default
- shared_ptr for shared ownership - use when necessary
- weak_ptr to break cycles and observe without owning
- make_unique/make_shared for exception safety and efficiency
- Custom deleters for non-memory resources
- RAII principle - tie resource lifetime to object lifetime
- Move semantics work perfectly with smart pointers
- Zero overhead with unique_ptr compared to raw pointers
📚 Additional Resources
🚀 What's Next?
You've now mastered modern C++ memory management with smart pointers and RAII. Next, in Lesson 11: Inheritance and Polymorphism, you'll relate your classes to one another — letting one type extend another and letting a single interface behave differently depending on the actual object behind it (often held through the very pointers you just learned to manage safely).
🎉 Smart pointer mastery achieved!
You've traded manual new/delete bookkeeping for RAII-powered safety — the exact discipline that separates fragile C++ from production-grade C++.