Smart Pointers in C++: unique_ptr, shared_ptr and weak_ptr with Worked Examples

Learn who owns a C++ object, when it is destroyed, and how to choose among unique_ptr, shared_ptr, weak_ptr and a borrowed raw pointer.

KnowledgeGate Team

Exam prep & CS education

Updated 1 Oct 20265 min read

A raw pointer can point to an object without saying who must delete it. Early returns, exceptions and ownership hand-offs then make simple code fragile in practice. std::unique_ptr provides exclusive ownership, std::shared_ptr provides shared ownership, and std::weak_ptr provides non-owning observation. Smart pointers manage the lifetime of dynamically allocated objects. They are not replacements for every pointer in a program.

Smart pointers in C++: ownership, lifetime and RAII

A pointer stores an address. Ownership is the responsibility to release a resource. Lifetime is the interval during which the object exists and may be used.

RAII binds a resource's lifetime to an object's lifetime. When an owning smart-pointer object is destroyed, it releases its owned object.

cpp
Widget *raw = new Widget(42);
if (!valid) return -1; // leak: delete is skipped
delete raw;

Compare that with:

cpp
auto owned = std::make_unique<Widget>(42);
if (!valid) return -1; // owned is destroyed during return

The second version deletes Widget(42) exactly once even on the early return. A stack address or borrowed API parameter usually needs a reference or raw pointer. The Coding & DSA Courses for Placements page provides the wider C++ learning path. C++ ownership is also distinct from operating-system memory topics such as Memory Management in OS: Paging and Segmentation. Smart pointers do not control pages or virtual addresses.

unique_ptr in C++: exclusive ownership

std::unique_ptr<T> has one owner. It cannot be copied, but ownership can be transferred by a move operation. Prefer std::make_unique<T>(arguments) for ordinary construction.

cpp
#include <iostream>
#include <memory>
#include <utility>

struct Book {
    int id;
    explicit Book(int value) : id(value) {}
    ~Book() { std::cout << "destroy " << id << '\n'; }
};

int main() {
    auto first = std::make_unique<Book>(42);
    auto second = std::move(first);

    std::cout << std::boolalpha << static_cast<bool>(first) << '\n';
    std::cout << second->id << '\n';
}

The exact output is:

Code
false
42
destroy 42

Before the move, first owns Book(42). std::move(first) enables its transfer. Afterwards, first == nullptr and second owns the object. At the end of main, second deletes the book once. auto copy = first; would not compile because exclusive ownership cannot be copied.

For a dynamic array, auto values = std::make_unique<int[]>(4); allocates four integers. Assigning values[0] through values[3] the values 3, 5, 8, 13 makes values[3] equal to 13. Array deletion is automatic.

Three panels showing unique_ptr ownership of Book{id=42} moving from first to second, then the object destroyed once at the end of main.

shared_ptr in C++: reference counting

Copied std::shared_ptr<T> objects share ownership through one control block. The object is destroyed when the last strong owner releases it. Prefer std::make_shared<T>(arguments).

cpp
auto owner = std::make_shared<int>(7); // count 1
auto copy = owner;                     // count 2
{
    auto temporary = copy;             // count 3
}                                      // count 2
copy.reset();                           // count 1
owner.reset();                          // count 0, destroy int(7)

State

Strong count

after make_shared

1

after copy

2

inside inner scope

3

after inner scope

2

after copy.reset()

1

after owner.reset()

0

The integer remains 7 throughout its lifetime and is destroyed only at the transition from 1 to 0. use_count() can illustrate this trace, but should not drive concurrent logic because the count can change. A shared_ptr carries control-block bookkeeping. Safe count updates do not make unsynchronised writes to the object safe.

weak_ptr in C++: observing and breaking cycles

Suppose external shared_ptr owners hold Person("Alice") and Person("Bob"). If each person's partner is a shared_ptr to the other, both strong counts become 2. Resetting the external owners leaves each count stuck at 1 through the cycle, so neither destructor runs.

Declare std::weak_ptr<Person> partner; instead. Assigning Bob as Alice's partner and Alice as Bob's partner leaves both strong counts at 1 because weak references do not own.

cpp
{
    auto locked = alice->partner.lock();
    if (locked) std::cout << locked->name; // Bob
}

Inside the scope, locked raises Bob's strong count from 1 to 2. It returns to 1 when locked is destroyed. If Bob has expired, lock() returns an empty shared_ptr, so check it before dereferencing. Use weak_ptr for observers, caches and back-references when the target is already managed by shared_ptr, not for every raw observer.

A shared_ptr cycle leaves Alice and Bob stuck at strong count 1, while weak_ptr partners hold count 1 and lock() lifts Bob to 2.

Choosing a smart pointer or raw observer

Need

Type

Ownership rule

Copy or move

Concrete example

One dynamic owner

unique_ptr

Exclusive

Move only

Factory returns Book(42)

Genuine co-owners

shared_ptr

Shared strong ownership

Copy allowed

Components share Session(7)

Observe shared state

weak_ptr

Does not own

Copy allowed

Cache watches that session

Borrow during a call

const Book& or Book*

Does not own

No ownership transfer

Inspector, with pointer if null is meaningful

Begin with unique_ptr. Choose shared_ptr only when several independent owners are required, then use weak_ptr for non-owning edges in that shared graph. Function signatures make the intent visible: std::unique_ptr<Book> make_book() returns ownership, void consume(std::unique_ptr<Book> book) takes it, and void inspect(const Book& book) borrows without extending lifetime.

Smart-pointer mistakes and repairs

Mistake

What goes wrong

Repair

Why

shared_ptr<int> a(raw); shared_ptr<int> b(raw); for one new int(9)

Two control blocks try to delete one address

auto a = make_shared<int>(9); auto b = a;

One control block, count 2

Book* view = owner.get(); owner.reset(); view->id;

view dangles

Use it only while owner lives

get() does not extend lifetime

Discard first.release()

The object leaks

Let unique_ptr delete it or pass it directly to an ownership-taking API

release() gives up deletion duty

Strong back-references

A cycle keeps counts above 0

Make the back-reference weak

The cycle no longer owns itself

Using shared_ptr by habit can also hide the intended owner. Draw the ownership graph and default to unique_ptr. If an object already under shared ownership must safely produce a shared_ptr to itself, use enable_shared_from_this; never construct a fresh shared_ptr(this).

Smart pointers in interviews and code traces

  • After auto p = std::make_unique<int>(12); auto q = std::move(p);, p is empty and *q is 12.

  • After auto a = std::make_shared<int>(30); auto b = a; std::weak_ptr<int> w = a;, the strong count is 2, not 3.

  • After b.reset(), the strong count is 1 and w.expired() is false while a still owns the integer.

Practise classifying ownership, predicting compilation failures, tracing strong counts, finding cycles and deciding whether a destructor runs. Keep the core distinction exact: unique_ptr means exclusive ownership, shared_ptr means shared ownership, and weak_ptr means non-owning observation of shared state. Once object lifetime is clear, Time Complexity and Asymptotic Notation is a useful next programming-analysis lesson.

Smart pointers in C++: the short version

  • Identify the owner.

  • Prefer automatic objects where possible.

  • Default dynamic ownership to unique_ptr.

  • Use shared_ptr only for real co-ownership.

  • Use weak_ptr to observe shared state and break cycles.

  • Never create two smart owners independently from one raw address.

Moving Book(42) leaves first empty. The shared int(7) is destroyed at the 1-to-0 strong-count transition. Continue with the C++ Programming Course for a sequenced route through C++ concepts and coding practice. Then compile the book example, add one temporary owner to the integer trace, predict every state first, and confirm the output.