Smart Pointers (unique_ptr, shared_ptr, weak_ptr)

Reviewed & published by Brayan K

Manual new and delete are the source of C++'s scariest bugs: memory leaks, dangling pointers, and double-frees. Smart pointers fix this by tying an object's lifetime to a variable's scope, so memory is freed automatically. By the end of this lesson you'll own heap objects safely with unique_ptr, share them with shared_ptr, and break cycles with weak_ptr — and you'll almost never write new again.

Part of the free C++ course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

💡 Real-World Analogy

Think of a heap object as a library book. A unique_ptr is a book checked out to exactly one person — when they leave, the book is returned automatically. A shared_ptr is a book on a shared desk with a tally sheet: every reader signs in, and the book is only re-shelved when the last reader signs out (the reference count hits zero). A weak_ptr is someone who knows the call number but hasn't checked the book out — they can go look if it's still on the desk, but they never stop it being re-shelved. The magic is RAII: "the destructor does the cleanup", so freeing memory becomes the compiler's job, not yours.

📊 The Three Smart Pointers

TypeOwnershipCopyable?Use when
unique_ptrexactly one ownerno (move only)the default choice
shared_ptrshared, ref-countedyesmany owners share an object
weak_ptrnone (observes)yesbreak cycles / observe safely

Golden rule: prefer unique_ptr; promote to shared_ptr only when ownership is truly shared; use weak_ptr to observe without owning.

1. The Problem with Raw new / delete

When you write new, you take on a promise to write a matching delete on every path out of the function — including early returns and thrown exceptions. Miss one and you leak memory; run it twice and you get a crash. This bookkeeping is where countless C++ bugs come from. Run the example and note how the delete is easy to forget.

#include <iostream>
using namespace std;

struct Widget {
    Widget()  { cout << "Widget built" << endl; }
    ~Widget() { cout << "Widget destroyed" << endl; }
    void greet() { cout << "Hello from Widget" << endl; }
};

int main() {
    // The OLD way, with a raw pointer + new/delete:
    Widget* w = new Widget();   // Widget built
    w->greet();                 // Hello from Widget

    // Easy to forget this -> a MEMORY LEAK.
    // And if an exception is thrown above, we never reach it at all.
    delete w;                   // Widget destroyed
    return 0;
}

// ✅ Expected output:
//    Widget built
//    Hello from Widget
//    Widget destroyed

2. unique_ptr — One Owner, Automatic Cleanup

A std::unique_ptr owns its object exclusively. Create it with std::make_unique<T>(args...) — no new, no delete. When the unique_ptr goes out of scope, its destructor runs and frees the object for you, guaranteed, even if an exception unwinds the stack. You access members with -> exactly like a raw pointer.

#include <iostream>
#include <memory>
using namespace std;

struct Widget {
    Widget()  { cout << "Widget built" << endl; }
    ~Widget() { cout << "Widget destroyed" << endl; }
    void greet() { cout << "Hello from Widget" << endl; }
};

int main() {
    // unique_ptr OWNS the object exclusively. No new, no delete.
    unique_ptr<Widget> w = make_unique<Widget>(); // Widget built
    w->greet();                                    // Hello from Widget

    // When w goes out of scope at the end of main, the Widget is
    // destroyed AUTOMATICALLY -- even if an exception is thrown.
    return 0;
    // Output also prints: Widget destroyed
}

// ✅ Expected output:
//    Widget built
//    Hello from Widget
//    Widget destroyed

Because there can only be one owner, a unique_ptr can't be copied — but you can move ownership with std::move. After the move, the source is empty and the destination owns the object:

#include <iostream>
#include <memory>
#include <utility>   // std::move
using namespace std;

struct Resource {
    int id;
    Resource(int i) : id(i) { cout << "open " << id << endl; }
    ~Resource()             { cout << "close " << id << endl; }
};

int main() {
    unique_ptr<Resource> a = make_unique<Resource>(7); // open 7

    // unique_ptr CANNOT be copied -- only ONE owner allowed:
    // unique_ptr<Resource> bad = a;   // ❌ compile error

    // ...but ownership can be MOVED. After this, 'a' is empty.
    unique_ptr<Resource> b = std::move(a);

    cout << "a is " << (a ? "set" : "empty") << endl; // a is empty
    cout << "b owns id " << b->id << endl;            // b owns id 7
    return 0;
    // 'b' destroys the Resource here -> close 7
}

// ✅ Expected output:
//    open 7
//    a is empty
//    b owns id 7
//    close 7

Your turn. Fill in the blank to create a Player on the heap with a smart pointer — no manual cleanup required:

#include <iostream>
#include <memory>
using namespace std;

struct Player { int hp = 100; };

int main() {
    // 🎯 YOUR TURN — create a Player on the heap with a smart pointer.

    // 1) Make a unique_ptr<Player> using make_unique:
    auto p = ___;          // 👉 make_unique<Player>();

    // 2) Damage the player:
    p->hp -= 30;

    cout << "HP: " << p->hp << endl;
    // ✅ Expected output:  HP: 70
    // (No delete needed -- the Player is freed automatically.)
    return 0;
}

🧩 Reorder Challenge

These lines should create a shared object, copy it, and print the reference count. Put them in the right order:

A.     shared_ptr<int> b = a;
B.     auto a = make_shared<int>(42);
C.     cout << a.use_count() << endl;
D. int main() {
E.     return 0;
F. }

Correct order: D → B → A → C → E → F

Open main (D), make the shared object (B), copy it so two owners exist (A), then print the count — now 2 (C), then return 0; (E) and close the brace (F).

3. shared_ptr — Shared Ownership

When several parts of a program must keep an object alive and none clearly outlives the others, use std::shared_ptr. It keeps a reference count: every copy increments it, every destruction decrements it, and the object is freed only when the count reaches zero. You can inspect the count with .use_count(). Create them with std::make_shared, which allocates the object and its control block in one step.

#include <iostream>
#include <memory>
using namespace std;

struct Texture {
    Texture()  { cout << "load texture" << endl; }
    ~Texture() { cout << "free texture" << endl; }
};

int main() {
    // shared_ptr lets MANY owners share one object. A reference count
    // tracks how many shared_ptrs point at it.
    shared_ptr<Texture> a = make_shared<Texture>(); // load texture
    cout << "count: " << a.use_count() << endl;     // count: 1

    {
        shared_ptr<Texture> b = a;   // copy -> both own it
        cout << "count: " << a.use_count() << endl; // count: 2
    } // b goes away -> count drops back to 1, NOT destroyed yet

    cout << "count: " << a.use_count() << endl;     // count: 1
    return 0;
    // last owner 'a' leaves -> count hits 0 -> free texture
}

// ✅ Expected output:
//    load texture
//    count: 1
//    count: 2
//    count: 1
//    free texture

4. weak_ptr — Observe Without Owning

Two shared_ptrs that point at each other form a reference cycle: their counts never reach zero, so the memory leaks. The fix is std::weak_ptr, which points at a shared object without bumping the count. To use what it points at, you call .lock(), which gives you a temporary shared_ptr if the object is still alive (or nullptr if it's gone). .expired() tells you whether the object has already been destroyed.

#include <iostream>
#include <memory>
using namespace std;

struct Node {
    int value;
    weak_ptr<Node> next;   // weak: does NOT keep 'next' alive
    Node(int v) : value(v) {}
};

int main() {
    auto a = make_shared<Node>(1);
    auto b = make_shared<Node>(2);
    a->next = b;            // a observes b, but does not own it

    // .lock() upgrades a weak_ptr to a shared_ptr IF the object lives:
    if (auto locked = a->next.lock()) {
        cout << "next value: " << locked->value << endl; // next value: 2
    }

    b.reset();             // release the only owner of node 2
    cout << "expired? " << a->next.expired() << endl;    // expired? 1

    if (a->next.lock() == nullptr) {
        cout << "node 2 is gone" << endl;                // node 2 is gone
    }
    return 0;
}

// ✅ Expected output:
//    next value: 2
//    expired? 1
//    node 2 is gone

🔎 Deep Dive: RAII, and why you rarely write new

Smart pointers are an application of RAII — "Resource Acquisition Is Initialization". The resource (heap memory) is acquired when the smart pointer is constructed and released when it is destroyed. Because C++ guarantees destructors run when an object leaves scope — including during exception unwinding — cleanup becomes automatic and exception-safe.

Modern guidance: express ownership with unique_ptr / shared_ptr, and use plain references or raw pointers only for non-owning, borrowed access. A raw pointer in modern code says "I'm looking, not owning".

auto u = make_unique<Widget>();   // owns
Widget* borrowed = u.get();       // borrows -- does NOT own, never delete it
process(*borrowed);               // fine while 'u' is alive
// when 'u' leaves scope, the Widget is destroyed -- automatically

Pro Tips

🧠 Quick Recall

Predict the output before revealing the answer.

1. What does this print?

auto a = make_shared<int>(5);
auto b = a;
auto c = a;
cout << a.use_count();

3 — three shared_ptrs (a, b, c) all own the same int, so the count is 3.

2. Does this compile?

unique_ptr<int> a = make_unique<int>(1);
unique_ptr<int> b = a;   // ?

No — a unique_ptr can't be copied. You'd need unique_ptr<int> b = std::move(a);.

3. After b.reset() below, what does w.expired() return?

auto b = make_shared<int>(9);
weak_ptr<int> w = b;
b.reset();
cout << w.expired();

1 (true) — b was the only owner; resetting it frees the int, so the weak_ptr has expired.

Common Errors (and the fix)

📋 Quick Reference

TaskCode
Own one objectauto u = make_unique<T>(args);
Share an objectauto s = make_shared<T>(args);
Transfer ownershipauto v = std::move(u);
Reference counts.use_count()
Observeweak_ptr<T> w = s;
Use a weak_ptrif (auto sp = w.lock())
Borrow rawT* raw = u.get();

Mini-Challenge: Self-Closing Account

Build a heap-allocated Account with a unique_ptr, run a few transactions, and watch it clean itself up — no delete in sight.

#include <iostream>
#include <memory>
#include <string>
using namespace std;

struct Account {
    string owner;
    double balance;
    Account(string o, double b) : owner(o), balance(b) {
        cout << "opened " << owner << "'s account" << endl;
    }
    ~Account() { cout << "closed " << owner << "'s account" << endl; }
};

int main() {
    // 🎯 MINI-CHALLENGE: Bank account with a smart pointer
    // 1. Create a unique_ptr<Account> for "Mia" starting at 100.0
    //    using make_unique.
    // 2. Deposit 50 (add to ->balance), then withdraw 30.
    // 3. Print the final balance.
    // 4. Notice the account closes AUTOMATICALLY at the end -- no delete.
    //
    // ✅ Example output:
    //    opened Mia's account
    //    Balance: 120
    //    closed Mia's account

    // your code here
    return 0;
}

🎉 Lesson Complete

Practice quiz

Which smart pointer should be your default choice?

  • shared_ptr
  • weak_ptr
  • unique_ptr
  • auto_ptr

Answer: unique_ptr. std::unique_ptr expresses single, exclusive ownership with zero overhead beyond a raw pointer — the default.

Can a unique_ptr be copied?

  • No — it can only be moved with std::move to transfer ownership
  • Yes, each copy shares ownership
  • Yes, but only with make_unique
  • Only inside the same scope

Answer: No — it can only be moved with std::move to transfer ownership. Copying a unique_ptr would create two owners of one object, which is forbidden; you move it instead.

Does this compile? unique_ptr<int> a = make_unique<int>(1); unique_ptr<int> b = a;

  • Yes
  • Yes, but b becomes null
  • Only with -std=c++20
  • No — a unique_ptr can't be copied; you'd need std::move(a)

Answer: No — a unique_ptr can't be copied; you'd need std::move(a). unique_ptr is move-only. You must write unique_ptr<int> b = std::move(a);.

Why prefer make_unique / make_shared over writing new directly?

  • They are the only way to allocate memory
  • They are safer, clearer, and make_shared does a single allocation for object and control block
  • They never call the destructor
  • They make the pointer global

Answer: They are safer, clearer, and make_shared does a single allocation for object and control block. The make functions are exception-safe and clearer; make_shared also combines the object and control block in one allocation.

What does this print? auto a = make_shared<int>(5); auto b = a; auto c = a; cout << a.use_count();

  • 3
  • 1
  • 2
  • 0

Answer: 3. Three shared_ptrs (a, b, c) all own the same int, so the reference count is 3.

When is a shared_ptr's managed object actually freed?

  • When the first shared_ptr is destroyed
  • Immediately after make_shared returns
  • When the reference count reaches zero (the last owner goes away)
  • Never — you must call delete

Answer: When the reference count reaches zero (the last owner goes away). shared_ptr reference-counts; the object is destroyed only when use_count drops to 0.

What is a weak_ptr used for?

  • Owning an object faster than unique_ptr
  • Observing a shared object without keeping it alive, e.g. to break reference cycles
  • Storing raw pointers safely
  • Replacing all references

Answer: Observing a shared object without keeping it alive, e.g. to break reference cycles. weak_ptr points at a shared object without bumping the count; it's the standard fix for reference cycles.

How do you safely access the object a weak_ptr points to?

  • Dereference it directly with *
  • Call .get() and delete it
  • Convert it with static_cast
  • Call .lock(), which returns a shared_ptr if the object still exists (or nullptr)

Answer: Call .lock(), which returns a shared_ptr if the object still exists (or nullptr). .lock() yields a temporary shared_ptr when the object is still alive, or nullptr if it has expired.

After auto b = make_shared<int>(9); weak_ptr<int> w = b; b.reset(); what does w.expired() return?

  • 0 (false)
  • 1 (true)
  • It crashes
  • 9

Answer: 1 (true). b was the only owner; resetting it frees the int, so the weak_ptr has expired and expired() returns 1 (true).

Which header must you include for unique_ptr, shared_ptr, weak_ptr, and the make functions?

  • <smartptr>
  • <pointers>
  • <memory>
  • <utility>

Answer: <memory>. All three smart pointers and make_unique/make_shared live in the <memory> header.

Continue this course

Frequently asked questions

When should I use unique_ptr vs shared_ptr?

Default to std::unique_ptr — it expresses single, exclusive ownership and has zero overhead beyond a raw pointer. Use std::shared_ptr only when ownership is genuinely shared, i.e. several parts of the program must keep the object alive and you cannot say which one outlives the others.

Why should I use std::make_unique and std::make_shared?

They are safer and clearer than calling new directly. make_shared also performs a single allocation for both the object and its control block, which is more efficient. As a rule, you should almost never write the keyword new in modern C++.

What is a weak_ptr for?

std::weak_ptr observes an object owned by a shared_ptr without keeping it alive. It is the standard fix for reference cycles (A owns B, B owns A) which would otherwise leak. You call .lock() to obtain a temporary shared_ptr if the object still exists.

Can I copy a unique_ptr?

No — a unique_ptr cannot be copied, because that would create two owners of one object. You can move it with std::move to transfer ownership. shared_ptr can be copied freely; each copy bumps the reference count.

Do smart pointers replace all manual memory management?

For ownership, yes — they automatically call delete when the last owner goes away, eliminating most leaks and double-frees. You still use plain references or raw pointers for non-owning access, where you are just borrowing an object you do not own.