12

Module 12: Smart Pointers

Chapter 12 • Advanced

55 min

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:

  1. `unique_ptr`: Exclusive ownership
  2. `shared_ptr`: Shared ownership
  3. `weak_ptr`: Non-owning reference

unique_ptr

Exclusive ownership - only one unique_ptr can own an object at a time.

Basic Usage

cpp.js
#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_ptr goes out of scope
  • Custom Deleters: Can specify custom cleanup
  • Array Support: Can manage arrays

Examples

cpp.js
// 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

cpp.js
#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
cpp.js
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

cpp.js
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

cpp.js
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

cpp.js
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

Featureunique_ptrshared_ptrweak_ptr
**Ownership**ExclusiveSharedNone
**Copyable**NoYesYes
**Movable**YesYesYes
**Reference Count**N/AYesNo
**Overhead**MinimalSmallSmall
**Use Case**Single ownerMultiple ownersBreak cycles

Best Practices

  1. Prefer `make_unique` and `make_shared` over new
  2. Use `unique_ptr` by default (simpler, faster)
  3. Use `shared_ptr` only when shared ownership is needed
  4. Use `weak_ptr` to break circular references
  5. Don't mix raw and smart pointers for same object
  6. Avoid circular references with shared_ptr
  7. Don't create `shared_ptr` from raw pointer multiple times
  8. Use `weak_ptr` to check if object still exists

Common Patterns

Pattern 1: Factory Function

cpp.js
unique_ptr<MyClass> createObject() {
    return make_unique<MyClass>();
}

Pattern 2: Polymorphism

cpp.js
class Base { };
class Derived : public Base { };

unique_ptr<Base> ptr = make_unique<Derived>();

Pattern 3: Container of Smart Pointers

cpp.js
vector<unique_ptr<MyClass>> objects;
objects.push_back(make_unique<MyClass>());

make_unique vs new

Prefer `make_unique`:

cpp.js
// 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:

cpp.js
// 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_ptr from same raw pointer
  • ❌ Circular references with shared_ptr
  • ❌ Using shared_ptr when unique_ptr is enough
  • ❌ Not checking weak_ptr before use
  • ❌ Mixing raw and smart pointers
  • ❌ Returning raw pointer from smart pointer function

Migration from Raw Pointers

Old Style:

cpp.js
int* ptr = new int(10);
// ... use ptr ...
delete ptr;  // Easy to forget!

Modern Style:

cpp.js
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.