Skip to content
Home
C++ Smart Pointers: unique_ptr, shared_ptr, and weak_ptr

C++ Smart Pointers: unique_ptr, shared_ptr, and weak_ptr

C/C++ C/C++ 8 min read 1547 words Beginner ExcellentWiki Editorial Team

Smart pointers are C++’s solution to manual memory management. They wrap raw pointers and automatically delete the pointed-to object when the smart pointer goes out of scope. Modern C++ code should rarely use new and delete directly — smart pointers express ownership semantics clearly and prevent memory leaks.

unique_ptr — Exclusive Ownership

std::unique_ptr represents exclusive ownership. Only one unique_ptr can point to a given resource at a time. When the unique_ptr is destroyed, the resource is automatically deleted:

#include <memory>

std::unique_ptr<int> ptr = std::make_unique<int>(42);
std::cout << *ptr << "\n";  // 42
// Automatic deletion when ptr goes out of scope

make_unique (C++14) is the preferred way to create a unique_ptr. It is safer than new because it prevents a potential memory leak when an exception is thrown between allocation and smart pointer construction.

Move-Only Semantics

std::unique_ptr<int> p1 = std::make_unique<int>(10);
std::unique_ptr<int> p2 = std::move(p1);  // Ownership transfers to p2
// p1 is now nullptr

// std::unique_ptr<int> p3 = p1;  // Compile error — no copy

unique_ptr is move-only. To transfer ownership, use std::move. After the move, the source pointer is null. This makes ownership transfers explicit and compile-time checked.

Custom Deleters

auto file_deleter = [](FILE* f) {
    if (f) fclose(f);
    std::cout << "File closed\n";
---;

std::unique_ptr<FILE, decltype(file_deleter)> file(
    fopen("data.txt", "r"), file_deleter);

Custom deleters let unique_ptr manage any resource, not just memory. The deleter type is part of the unique_ptr type signature, which means two unique_ptrs with different deleters are different types.

Ownership Transfer to Functions

class Widget { /* ... */ };

std::unique_ptr<Widget> createWidget() {
    return std::make_unique<Widget>();  // Ownership moves to caller
---

void consumeWidget(std::unique_ptr<Widget> w) {
    // Function takes ownership
---

auto w = createWidget();
consumeWidget(std::move(w));  // Transfer ownership

Functions returning a unique_ptr indicate that the caller receives ownership. Functions taking a unique_ptr by value signal that they will take ownership. These conventions make ownership flow explicit in the type system.

shared_ptr — Shared Ownership

std::shared_ptr implements reference-counted shared ownership. The object is deleted when the last shared_ptr pointing to it is destroyed:

std::shared_ptr<int> p1 = std::make_shared<int>(42);
{
    std::shared_ptr<int> p2 = p1;  // Reference count: 2
    std::cout << p1.use_count();   // 2
---  // p2 destroyed, reference count: 1

// p1 destroyed, reference count: 0, object deleted

make_shared (C++11) allocates the control block and the managed object in a single allocation, which is more efficient than constructing a shared_ptr from a raw pointer.

Reference Counting Internals

shared_ptr stores two pointers internally: one to the managed object and one to a control block containing the reference count and the deleter. The control block is thread-safe (atomic reference count), which adds some overhead:

auto sp = std::make_shared<Widget>();
// Memory layout:
//   shared_ptr: [ptr -> Widget][ctrl -> control block]
//   control block: [ref count: 1][weak count: 0][deleter]

The atomic reference count operations make shared_ptr more expensive than unique_ptr — copying a shared_ptr has synchronization overhead. For hot paths, prefer unique_ptr when ownership is exclusive.

Enabling Shared From This

If a class needs to create shared_ptr instances from this, it should inherit from std::enable_shared_from_this:

class Node : public std::enable_shared_from_this<Node> {
    std::shared_ptr<Node> next;

public:
    void setNext(const std::shared_ptr<Node>& n) {
        next = n;
    }

    std::shared_ptr<Node> shared() {
        return shared_from_this();
    }
---;

This guarantees that the control block is accessible. Never create a raw shared_ptr from this directly — it would create a second control block and cause a double-free.

weak_ptr — Non-Owning Observer

std::weak_ptr holds a non-owning reference to an object managed by shared_ptr. It does not affect the reference count. Use it to break circular references:

std::shared_ptr<int> sp = std::make_shared<int>(42);
std::weak_ptr<int> wp = sp;  // Does not increase ref count

// Must lock to access
if (auto locked = wp.lock()) {
    std::cout << *locked << "\n";  // 42
--- else {
    std::cout << "Object was deleted\n";
---

wp.lock() returns a shared_ptr if the object is still alive, or nullptr if it has been deleted. This atomic check-and-access prevents dangling pointer bugs.

Breaking Circular References

The classic problem with raw shared_ptr usage:

struct B;  // Forward declaration

struct A {
    std::shared_ptr<B> b_ptr;
    ~A() { std::cout << "A destroyed\n"; }
---;

struct B {
    std::shared_ptr<A> a_ptr;  // Circular reference!
    ~B() { std::cout << "B destroyed\n"; }
---;

auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->b_ptr = b;
b->a_ptr = a;
// Neither A nor B is ever destroyed — memory leak

Breaking the cycle with weak_ptr:

struct B {
    std::weak_ptr<A> a_ptr;  // Non-owning reference
    ~B() { std::cout << "B destroyed\n"; }
---;
// Now A holds a shared_ptr to B, B holds a weak_ptr to A
// When A is destroyed, B loses its reference automatically

Use weak_ptr in parent-child or observer relationships where one side should not own the other.

Performance Considerations

// Fastest — no heap allocation overhead
auto uptr = std::make_unique<Widget>();

// Single allocation for object + control block
auto sptr = std::make_shared<Widget>();

// Separate allocations — avoid this
auto sptr2 = std::shared_ptr<Widget>(new Widget());

| Feature | unique_ptr | shared_ptr | weak_ptr | |

Choosing the Right Smart Pointer

The C++ standard library provides three smart pointer types, each with a distinct ownership model:

  • std::unique_ptr — Single ownership. Lightweight, no overhead over raw pointer. Use as the default choice for dynamically allocated objects. Pass by value to transfer ownership, pass by reference or raw pointer for non-owning access.

  • std::shared_ptr — Shared ownership via reference counting. Adds overhead for the control block and atomic reference count operations. Use when multiple objects need to share ownership of a resource.

  • std::weak_ptr — Non-owning observer. Breaks circular references in shared ownership graphs. Always check lock() before use — the observed object may have been destroyed.

Performance Considerations

std::shared_ptr has significant overhead compared to raw pointers or unique_ptr:

  1. Memory overhead — Each shared_ptr stores the managed pointer plus a pointer to the control block.
  2. Control block — Contains reference counts (strong and weak), custom deleter, and allocator.
  3. Atomic operations — Every copy and destruction involves an atomic increment or decrement of the reference count.
  4. Double allocation — Without std::make_shared, the object and control block are allocated separately.

For performance-critical code, prefer std::unique_ptr which has zero overhead over a raw pointer.

Enabling Shared from This

The std::enable_shared_from_this CRTP class allows an object to create shared_ptr instances pointing to itself:

class Node : public std::enable_shared_from_this<Node> {
    std::shared_ptr<Node> get_shared() {
        return shared_from_this();
    }
---;

auto node = std::make_shared<Node>();
auto same_node = node->get_shared();  // Valid — shares reference count

Calling shared_from_this() requires that the object is already managed by a shared_ptr. Calling it on a bare new-allocated object or a unique_ptr-managed object is undefined behavior.

Observer Pattern with weak_ptr

Smart pointers enable safe observer patterns without manual lifetime management:

class Subject {
    std::vector<std::weak_ptr<Observer>> observers;
public:
    void attach(std::shared_ptr<Observer> obs) {
        observers.push_back(obs);
    }
    void notify() {
        observers.erase(std::remove_if(observers.begin(), observers.end(),
            [](auto &wp) { return wp.expired(); }), observers.end());
        for (auto &wp : observers) {
            if (auto sp = wp.lock()) sp->update();
        }
    }
---;

FAQ

What is the difference between malloc and new in C++? malloc allocates raw memory without calling constructors; new allocates memory and calls the constructor. In C++, prefer new for objects. free vs delete follows the same pattern — delete calls the destructor.

How do I prevent memory leaks in C/C++? Use RAII (Resource Acquisition Is Initialization) in C++ — smart pointers like std::unique_ptr and std::shared_ptr automatically free memory. In C, always pair every malloc with a free and use tools like Valgrind or AddressSanitizer to detect leaks.

What is undefined behavior in C/C++? Undefined behavior occurs when code performs operations that the language standard does not define — dereferencing a null pointer, buffer overflow, signed integer overflow. The compiler may generate any code, including unexpected results or crashes.

Should I learn C or C++ first? Learn C first if you want to understand low-level memory and system programming. Learn C++ first if you want object-oriented features and the STL. Both are valuable; C++ builds on C concepts.

What is the difference between a header file and a source file? Header files (.h) declare interfaces — function prototypes, class definitions, macros. Source files (.c or .cpp) implement the declarations. Headers are #included; source files are compiled separately and linked.

—|—|—|—| | Size | 8 bytes (pointer) | 16 bytes (ptr + ctrl) | 8 bytes (ctrl) | | Overhead | None | Atomic ref count | Lock + weak count | | Copyable | No | Yes (O(1) atomic inc) | Yes (atomic inc) | | Custom deleter | Part of type | Type-erased | — |

Best Practices

Prefer make_unique and make_shared — They are more efficient and exception-safe than using new directly. The single-allocation optimization of make_shared is significant.

Use unique_ptr by default — Exclusive ownership is simpler and more efficient than shared ownership. Only use shared_ptr when ownership genuinely needs to be shared.

Avoid shared_ptr cycles — If you have a cycle, identify the non-owning side and use weak_ptr. Document which side owns what.

Do not mix smart and raw pointers — A raw pointer should never own a resource that a smart pointer manages. Use raw pointers only for non-owning observers in code that you know will not outlive the resource.

Pass smart pointers by reference — When a function does not need ownership, pass const unique_ptr<T>& or T* (non-owning). Pass by value only when transferring ownership.


Related: Learn about C OOP, C++ STL, and C vs C++.

Section: C/C++ 1547 words 8 min read Beginner 756 articles in section Report inaccuracy Back to top