Skip to content

Smart Pointers — unique_ptr, shared_ptr, weak_ptr, make_unique, make_shared

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about Smart Pointers. We cover key concepts, practical examples, and best practices to help you master this topic.

C++ smart pointers automate dynamic memory management with unique_ptr for exclusive ownership, shared_ptr for reference-counted sharing, and weak_ptr for non-owning observation that avoids circular references.

What You'll Learn

You will manage heap-allocated objects using std::unique_ptr (exclusive ownership, zero overhead), std::shared_ptr (reference-counted shared ownership with control block), and std::weak_ptr (non-owning Observer that breaks cycles). You will use std::make_unique and std::make_shared for exception-safe creation, and understand when each smart pointer type is appropriate.

Why It Matters

Smart pointers are the cornerstone of modern C++ resource management. They eliminate manual new/delete and implement RAII for heap memory. unique_ptr has zero overhead over a raw pointer, making it suitable for all ownership contexts. shared_ptr adds reference counting for shared ownership scenarios. Learning these tools virtually eliminates memory leaks and double-free bugs from your code.

Learning Path

graph LR
    A["23: Dynamic Memory"] --> B["24: Smart Pointers"]
    B --> C["25: Custom Deleters"]
    C --> D["26: Allocators"]
    style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style D fill:#4a90d9,stroke:#2c5f8a,color:#fff

std::unique_ptr

Exclusive ownership: only one unique_ptr can own a resource at a time. Cannot be copied, but can be moved.

#include <iostream>
#include <memory>
#include <vector>

class Resource {
public:
    Resource() { std::cout << "Resource acquired\n"; }
    ~Resource() { std::cout << "Resource released\n"; }
    void work() const { std::cout << "Working\n"; }
};

int main() {
    // Create with make_unique (C++14)
    std::unique_ptr<Resource> ptr = std::make_unique<Resource>();
    
    ptr->work();
    
    // Cannot copy unique_ptr
    // auto ptr2 = ptr;  // Error: copy deleted
    
    // Must move to transfer ownership
    std::unique_ptr<Resource> ptr2 = std::move(ptr);
    if (!ptr) {
        std::cout << "ptr is now null\n";
    }
    ptr2->work();
    
    // Vector of unique_ptrs
    std::vector<std::unique_ptr<int>> numbers;
    numbers.push_back(std::make_unique<int>(10));
    numbers.push_back(std::make_unique<int>(20));
    
    for (const auto& num : numbers) {
        std::cout << *num << "\n";
    }
    
    // Raw pointer access (non-owning)
    Resource* raw = ptr2.get();
    raw->work();
    
    // Release ownership without destroying
    Resource* released = ptr2.release();
    delete released;  // must delete manually
}

unique_ptr is the default smart pointer. It has no overhead compared to a raw pointer and integrates perfectly with standard containers.

std::shared_ptr

Shared ownership via reference counting. The resource is destroyed when the last shared_ptr is destroyed.

#include <iostream>
#include <memory>

struct SharedResource {
    int id;
    SharedResource(int i) : id(i) { std::cout << "SharedResource " << id << " created\n"; }
    ~SharedResource() { std::cout << "SharedResource " << id << " destroyed\n"; }
};

int main() {
    std::shared_ptr<SharedResource> sp1 = std::make_shared<SharedResource>(1);
    {
        std::shared_ptr<SharedResource> sp2 = sp1;
        std::cout << "Use count: " << sp1.use_count() << "\n";  // 2
        
        std::shared_ptr<SharedResource> sp3 = sp1;
        std::cout << "Use count: " << sp1.use_count() << "\n";  // 3
    }
    // sp2 and sp3 destroyed, count back to 1
    
    std::cout << "Use count: " << sp1.use_count() << "\n";  // 1
    // Resource destroyed when sp1 goes out of scope
}

The control block holds the reference count and the deleter. make_shared allocates the object and control block in a single memory allocation, which is more efficient than separate new and control block allocation.

std::weak_ptr

Non-owning observer that does not affect the reference count. Used to break circular references.

#include <iostream>
#include <memory>

struct Node {
    int value;
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev;  // weak to avoid cycle
    
    ~Node() { std::cout << "Node " << value << " destroyed\n"; }
};

int main() {
    auto n1 = std::make_shared<Node>();
    auto n2 = std::make_shared<Node>();
    n1->value = 1;
    n2->value = 2;
    
    // Circular reference using weak_ptr
    n1->next = n2;
    n2->prev = n1;
    
    // Use weak_ptr: must lock() to get shared_ptr
    if (auto shared = n2->prev.lock()) {
        std::cout << "Previous: " << shared->value << "\n";  // 1
    }
    
    std::cout << "n1 use count: " << n1.use_count() << "\n";  // 1 (only n1 itself)
    // n1 and n2 are destroyed properly when they go out of scope
}

Without weak_ptr (using shared_ptr for both directions), the nodes would never be destroyed because each holds the other alive.

std::make_unique and std::make_shared

#include <memory>
#include <vector>

struct Widget {
    int x;
    double y;
    Widget(int a, double b) : x(a), y(b) {}
};

int main() {
    // make_unique (C++14)
    auto uptr = std::make_unique<int>(42);
    auto w1 = std::make_unique<Widget>(10, 3.14);
    
    // make_shared
    auto sptr = std::make_shared<int>(99);
    auto w2 = std::make_shared<Widget>(20, 2.71);
    
    // make_unique for arrays (C++17)
    auto arr = std::make_unique<int[]>(10);
    arr[0] = 5;
    
    // make_shared for arrays (C++20)
    auto sarr = std::make_shared<int[]>(10);
    sarr[0] = 5;
}

Always prefer make_unique and make_shared over using new:

  • Exception safety: no gap between allocation and smart pointer construction
  • Fewer memory allocations: make_shared combines object and control block
  • Cleaner syntax: type is only written once
  • No naked new in user code

Converting Between Smart Pointer Types

#include <memory>

int main() {
    // unique_ptr -> shared_ptr (implicit conversion)
    auto up = std::make_unique<int>(42);
    std::shared_ptr<int> sp = std::move(up);
    
    // shared_ptr -> unique_ptr (NOT possible directly)
    // auto up2 = std::unique_ptr<int>(sp.get());  // dangerous!
    
    // weak_ptr -> shared_ptr via lock()
    std::weak_ptr<int> wp = sp;
    if (auto locked = wp.lock()) {
        // use locked
    }
}

Custom Deleter with Smart Pointers

#include <iostream>
#include <memory>
#include <cstdio>

struct FileCloser {
    void operator()(std::FILE* fp) const {
        if (fp) {
            std::fclose(fp);
            std::cout << "File closed\n";
        }
    }
};

int main() {
    std::unique_ptr<std::FILE, FileCloser> file(std::fopen("test.txt", "w"));
    
    // With lambda
    auto deleter = [](std::FILE* f) {
        if (f) std::fclose(f);
    };
    std::unique_ptr<std::FILE, decltype(deleter)> file2(std::fopen("test2.txt", "w"), deleter);
}

Smart Pointer Comparison

Feature unique_ptr shared_ptr weak_ptr
Ownership Exclusive Shared None
Reference count No Yes (control block) No
Overhead None Control block + atomic ops Control block access
Copyable No (move only) Yes Yes
Thread-safe ref count N/A Yes Yes
Use case Local ownership, containers Shared lifetime, caches Breaking cycles, Caching

Common Mistakes

Mistake 1: Using shared_ptr When unique_ptr Suffices

std::shared_ptr<Widget> ptr = std::make_shared<Widget>();  // unnecessary overhead

Use unique_ptr by default. Only use shared_ptr when ownership is truly shared.

Mistake 2: Circular shared_ptr References

struct A { std::shared_ptr<B> b; };
struct B { std::shared_ptr<A> a; };

Use weak_ptr in one direction to break the cycle.

Mistake 3: Getting a Raw Pointer and Deleting It

auto sp = std::make_shared<int>(42);
int* raw = sp.get();
delete raw;  // undefined behavior: double free

Mistake 4: Creating shared_ptr from Raw Pointer Multiple Times

int* raw = new int(42);
std::shared_ptr<int> sp1(raw);
std::shared_ptr<int> sp2(raw);  // two independent control blocks, double delete!

Mistake 5: Using .get() to Store in a Container

std::vector<int*> vec;
auto sp = std::make_shared<int>(42);
vec.push_back(sp.get());  // dangerous: sp may be destroyed before vector

Mistake 6: Assuming weak_ptr::lock() Always Succeeds

Always check the result of lock(): it returns an empty shared_ptr if the object has been deleted.

Practice Questions

  1. When should you use unique_ptr versus shared_ptr?
  2. Why does make_shared use a single allocation?
  3. What problem does weak_ptr solve?
  4. Can you store unique_ptr in a std::vector? How?
  5. Write a function that creates a unique_ptr<int> and transfers ownership to the caller.

Challenge

Implement a simplified shared_ptr that maintains a reference count in a separate control block. Include construction, copy, move, destruction, and operator->. Test that the object is destroyed when the last copy goes out of scope.

FAQ

Are smart pointers zero-overhead?

unique_ptr has zero overhead over a raw pointer. shared_ptr has the overhead of a control block (two words) and atomic reference count operations.

Can I use smart pointers with arrays?

Yes. unique_ptr<T[]> (C++11) and shared_ptr<T[]> (C++17) support array access with operator[].

Is it safe to return a `unique_ptr` from a function?

Yes. Return a unique_ptr to transfer exclusive ownership to the caller. The caller receives a unique_ptr and is responsible for the object's lifetime.

What is the difference between `shared_ptr` and `weak_ptr`?

A shared_ptr keeps the object alive (prevents deletion). A weak_ptr observes the object without preventing deletion and must be .lock()ed to access it.

Can I use `auto_ptr`?

No. auto_ptr is deprecated and removed in C++17. Use unique_ptr instead.

How does `make_shared` improve exception safety?

Without make_shared, an exception between new and the shared_ptr constructor leaks the object. make_shared eliminates this gap.

Mini Project

Build a simple observer registry using shared_ptr and weak_ptr:

#include <iostream>
#include <memory>
#include <vector>
#include <algorithm>

class Observer {
public:
    virtual ~Observer() = default;
    virtual void notify(int event) = 0;
};

class Subject {
private:
    std::vector<std::weak_ptr<Observer>> observers_;
    
public:
    void addObserver(std::shared_ptr<Observer> obs) {
        observers_.push_back(obs);
    }
    
    void notifyAll(int event) {
        // Remove expired observers and notify alive ones
        observers_.erase(
            std::remove_if(observers_.begin(), observers_.end(),
                [event](const std::weak_ptr<Observer>& wp) {
                    if (auto sp = wp.lock()) {
                        sp->notify(event);
                        return false;
                    }
                    return true;
                }),
            observers_.end()
        );
    }
};

class ConcreteObserver : public Observer {
private:
    int id_;
public:
    ConcreteObserver(int id) : id_(id) {}
    void notify(int event) override {
        std::cout << "Observer " << id_ << " received event " << event << "\n";
    }
};

int main() {
    Subject subject;
    auto obs1 = std::make_shared<ConcreteObserver>(1);
    auto obs2 = std::make_shared<ConcreteObserver>(2);
    
    subject.addObserver(obs1);
    subject.addObserver(obs2);
    
    subject.notifyAll(100);
    
    obs2.reset();  // observer 2 is destroyed
    subject.notifyAll(200);  // only observer 1 is notified
}

What's Next

Smart pointers handle ownership. The next lesson covers custom deleters for smart pointers: function objects, lambda deleters, and resource handles for non-memory resources like files and sockets.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro