C++ by Example
C++ by Example

Smart Pointers

2 min read

Smart Pointers

Smart pointers are objects that behave like pointers but manage the lifetime of the heap-allocated object they point to. They eliminate memory leaks and make ownership explicit.

std::unique_ptr

unique_ptr owns an object exclusively. When the unique_ptr is destroyed, it deletes the object. Ownership can be transferred but not shared:

#include <iostream>
#include <memory>

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

int main() {
    auto w = std::make_unique<Widget>(1);
    std::cout << w->id << "\n";
}
// Widget 1 destroyed — automatically when w goes out of scope

Always use std::make_unique<T>(args...) rather than new. It is exception-safe and cleaner.

Transferring ownership with std::move

unique_ptr cannot be copied — only moved. Moving transfers ownership:

auto a = std::make_unique<Widget>(2);
auto b = std::move(a);   // b now owns Widget 2; a is null

std::cout << (a == nullptr) << "\n";   // 1
std::cout << b->id          << "\n";   // 2

std::shared_ptr

shared_ptr allows multiple owners. It keeps a reference count; the object is deleted when the last shared_ptr to it is destroyed:

#include <memory>
#include <iostream>

int main() {
    auto a = std::make_shared<Widget>(3);
    {
        auto b = a;   // both own Widget 3
        std::cout << a.use_count() << "\n";   // 2
    }
    // b destroyed — count drops to 1, Widget survives
    std::cout << a.use_count() << "\n";       // 1
}
// Widget 3 destroyed when a goes out of scope

use_count() returns the current number of owners.

std::weak_ptr

weak_ptr observes a shared_ptr without contributing to the reference count. It is used to break ownership cycles that would otherwise prevent deletion:

#include <memory>
#include <iostream>

int main() {
    auto shared = std::make_shared<int>(42);
    std::weak_ptr<int> weak = shared;

    std::cout << weak.expired() << "\n";   // 0 — still alive

    if (auto locked = weak.lock()) {       // temporarily elevate to shared_ptr
        std::cout << *locked << "\n";      // 42
    }

    shared.reset();
    std::cout << weak.expired() << "\n";   // 1 — object gone
}

weak.lock() returns a shared_ptr if the object is still alive, or an empty shared_ptr if it has been deleted.

Custom deleters

Both unique_ptr and shared_ptr accept a custom deleter for resources that are not freed with delete:

#include <cstdio>
#include <memory>

int main() {
    // Manage a C FILE* with a unique_ptr
    auto file = std::unique_ptr<FILE, decltype(&fclose)>(
        fopen("data.txt", "r"),
        fclose
    );

    if (file) {
        // use file.get() to get the raw FILE*
    }
}
// fclose is called automatically

Choosing the right smart pointer

Need Use
Single owner std::unique_ptr
Shared ownership std::shared_ptr
Non-owning observer of a shared_ptr std::weak_ptr
Non-owning observer (always valid) raw T* or T&

unique_ptr has zero overhead over a raw pointer. shared_ptr has a small overhead from the reference count and control block. Use unique_ptr by default and reach for shared_ptr only when sharing is genuinely required.

C++ by Example
C++ by Example

Learn modern C++ through working code. Each chapter introduces one concept — variables, functions, classes, templates, smart pointers, concurrency — with a clear example, a line-by-line explanation, and notes on how it applies in real programs.

View book →