Site

Smart Pointers — unique_ptr, shared_ptr

Tutorial S8.0  •  C++ / Learn / StdLib

S8.0 What This Teaches

Smart pointers automate heap memory management using RAII. This tutorial covers:

S8.1 unique_ptr

#include <memory>

auto p = std::make_unique<int>(42);   // preferred over new
std::cout << *p << "\n";              // 42

// transfer ownership
auto q = std::move(p);   // p is now null; q owns the int
std::cout << (p == nullptr) << "\n";  // 1
std::cout << *q << "\n";              // 42
// q is destroyed when it goes out of scope - no delete needed
unique_ptr cannot be copied, only moved. This enforces single ownership at compile time. Use it as the default smart pointer for any heap-allocated object.

S8.2 shared_ptr

auto s1 = std::make_shared<std::string>("shared data");
auto s2 = s1;   // both own the same string; ref count = 2
auto s3 = s1;   // ref count = 3

std::cout << s1.use_count() << "\n";   // 3

s2.reset();   // release s2's ownership; ref count = 2
std::cout << s1.use_count() << "\n";   // 2
// string destroyed when last owner (s1 or s3) goes out of scope
shared_ptr carries a reference-counted control block. Use it when ownership genuinely needs to be shared. Its overhead (atomic ref count, two pointer dereferences) is higher than unique_ptr.

S8.3 weak_ptr

std::weak_ptr<int> weak;
{
    auto strong = std::make_shared<int>(99);
    weak = strong;
    std::cout << weak.expired() << "\n";   // 0

    if (auto locked = weak.lock()) {        // promote to shared_ptr
        std::cout << *locked << "\n";       // 99
    }
}   // strong destroyed
std::cout << weak.expired() << "\n";   // 1
weak_ptr breaks cycles that would keep objects alive forever through shared_ptr. Call lock() to get a temporary shared_ptr; check for null before using it.

S8.4 make_unique and make_shared

// prefer these over new:
auto p1 = std::make_unique<std::string>("hello");
auto p2 = std::make_shared<std::vector<int>>(5, 0);

// unique_ptr for arrays
auto arr = std::make_unique<int[]>(10);
arr[0] = 42;
make_unique and make_shared are exception-safe and clearer than new. make_shared also allocates the control block and the object in a single allocation, reducing overhead.

S8.5 Example - All Together

// SmartPointers - unique_ptr, shared_ptr, weak_ptr.

#include <iostream>
#include <memory>
#include <string>

struct Node {
    std::string name;
    explicit Node(std::string n) : name(std::move(n)) {
        std::cout << "created " << name << "\n";
    }
    ~Node() { std::cout << "destroyed " << name << "\n"; }
};

int main() {
    std::cout << "--- unique_ptr ---\n";
    {
        auto p = std::make_unique<Node>("alpha");
        auto q = std::move(p);
        std::cout << "p null: " << (p == nullptr) << "\n";
    }

    std::cout << "\n--- shared_ptr ---\n";
    {
        auto s1 = std::make_shared<Node>("beta");
        { auto s2 = s1; std::cout << "count=" << s1.use_count() << "\n"; }
        std::cout << "count=" << s1.use_count() << "\n";
    }

    std::cout << "\n--- weak_ptr ---\n";
    std::weak_ptr<Node> w;
    {
        auto s = std::make_shared<Node>("gamma");
        w = s;
        std::cout << "expired=" << w.expired() << "\n";
    }
    std::cout << "expired=" << w.expired() << "\n";
    return 0;
}
--- unique_ptr ---
created alpha
p null: 1
destroyed alpha

--- shared_ptr ---
created beta
count=2
count=1
destroyed beta

--- weak_ptr ---
created gamma
expired=0
destroyed gamma
expired=1

S8.6 Exercise

Exercise
  • Build a singly linked list using unique_ptr<Node> where each Node holds an int and a unique_ptr<Node> to the next node. Write a function that sums all values.
  • Create three shared_ptr copies of the same object and confirm the use count after each copy and each reset.
  • Create a circular reference using two shared_ptrs that point at each other. Observe the memory leak. Fix it by changing one pointer to weak_ptr.

S8.7 Common Mistakes

Managing the same raw pointer with two unique_ptrs

int* raw = new int(42);
auto p1 = std::unique_ptr<int>(raw);
auto p2 = std::unique_ptr<int>(raw);   // double-free: both will delete raw
Always use make_unique. Never construct two smart pointers from the same raw pointer.

Accessing a moved-from unique_ptr

auto p = std::make_unique<int>(42);
auto q = std::move(p);
std::cout << *p;   // undefined behavior: p is null after move

Creating shared_ptr from raw pointer obtained from another shared_ptr

auto s1 = std::make_shared<int>(42);
auto s2 = std::shared_ptr<int>(s1.get());   // double-free: s2 doesn't know about s1
Copy s1 directly to get a new owner: auto s2 = s1;.

S8.8 Key Terms

TermMeaning
unique_ptr<T>Sole owner of a heap object; non-copyable, movable; no overhead over raw pointer
shared_ptr<T>Shared ownership via reference counting; object freed when count reaches 0
weak_ptr<T>Non-owning observer; use lock() to get a temporary shared_ptr
make_unique<T>Creates a unique_ptr; exception-safe, preferred over new
make_shared<T>Creates a shared_ptr; single allocation for object and control block
use_count()Current reference count of a shared_ptr
reset()Release ownership; pointer becomes null
lock()Promote weak_ptr to shared_ptr; returns null if object was destroyed