Smart Pointers

Hi everyone ✋

In the previous post we learned dynamic memory allocation ─ how new grabs memory from the heap and delete gives it back. We also met two scary dangers ─ the memory leak (forgetting to delete) and the dangling pointer (using memory after delete).

At the very end, we got a little taste of the cure ─ smart pointers. Today we’re going to explore them properly 🥳

If you haven’t read the earlier posts in this series, please go through them first ─ especially the dynamic memory post, because today is all about making that dangerous manual work safe and automatic.

Let’s take a deep dive 🦴

The problem, one more time 🤔

Let’s quickly remember the pain from last post. With raw new and delete, the burden is entirely on us ─

#include <iostream>
using namespace std;

int main()
{
    int *ptr = new int(42);

    // ... lots of code ...
    // ... maybe an early return, maybe an exception ...

    delete ptr;   // did we remember? are we SURE this always runs? 😱

    return 0;
}

The trouble is that delete is easy to forget. And even if we write it, what if the function returns early before reaching it? What if an error is thrown in the middle? The delete never runs, and we leak memory 😱

Wouldn’t it be wonderful if the cleanup just… happened by itself, automatically, no matter what? That’s exactly what smart pointers give us 🥳

The big idea ─ a pointer that cleans up after itself

A smart pointer is a small object that wraps a raw pointer and takes responsibility for it. It behaves like a normal pointer ─ we can still dereference it with * ─ but with one magical difference ─ when the smart pointer goes out of scope, it automatically calls delete for us.

The trick behind this magic has a name ─ RAII (Resource Acquisition Is Initialization). Don’t be scared by the fancy term. It just means “tie a resource to the lifetime of an object.” When the object is born, it grabs the resource. When the object dies, it releases the resource. Since C++ automatically destroys objects when they go out of scope, our cleanup becomes automatic too 🥳

To use smart pointers, we include the <memory> header. There are three kinds we’ll meet today ─ unique_ptr, shared_ptr, and weak_ptr. Let’s take them one at a time.

1. unique_ptr ─ one owner, no sharing 🥇

The unique_ptr is the simplest and most common smart pointer. Its rule is right there in the name ─ it is the one and only owner of the memory. No other pointer is allowed to own the same memory at the same time.

When a unique_ptr goes out of scope, it deletes its memory automatically. Let’s see it.

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

int main()
{
    // create a unique_ptr that owns an int with value 42
    unique_ptr<int> ptr = make_unique<int>(42);

    cout << "value: " << *ptr << endl;

    // NO delete needed ─ ptr cleans itself up automatically! 🥳

    return 0;
}

The output will be ─

value: 42

🥳 Notice what’s missing ─ there’s no delete anywhere! When ptr goes out of scope at the end of main, it automatically frees the memory. The leak we worried about is simply impossible here.

Also notice we used make_unique<int>(42) instead of new. This is the modern, preferred way to create a unique_ptr ─ it’s safer and cleaner. As a rule of thumb, prefer make_unique over writing new yourself 🤔

Why “unique”? Let’s test the rule

The word unique means only one owner. So what happens if we try to copy a unique_ptr? Let’s find out.

unique_ptr<int> a = make_unique<int>(42);

unique_ptr<int> b = a;   // ERROR ─ won't compile! 😱

This won’t compile, and that’s on purpose 🥳 If copying were allowed, then both a and b would think they own the same memory ─ and when both went out of scope, both would try to delete it, causing a crash. By forbidding the copy, C++ protects us from that mistake before the program even runs.

But what if we genuinely want to hand over ownership from one pointer to another? For that, we use std::move ─ which transfers ownership instead of copying it.

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

int main()
{
    unique_ptr<int> a = make_unique<int>(42);

    // transfer ownership from a to b
    unique_ptr<int> b = move(a);

    // now b owns the memory, and a owns nothing
    cout << "b says: " << *b << endl;

    if (a == nullptr)
    {
        cout << "a is now empty!" << endl;
    }

    return 0;
}

The output will be ─

b says: 42
a is now empty!

🥳 After the move, ownership passed from a to b. Now b is the sole owner, and a has become empty (nullptr). There’s still only one owner at any moment ─ the rule is never broken. This is the safest and most efficient smart pointer, so reach for unique_ptr by default.

2. shared_ptr ─ shared ownership with counting 🤝

Sometimes one owner isn’t enough. Imagine several parts of our program all need to use the same object, and we don’t know which one will finish last. Who should delete it? 🤔

This is where shared_ptr comes in. Unlike unique_ptr, a shared_ptr can be copied, and many shared_ptrs can own the same memory together. It keeps a hidden counter ─ called the reference count ─ that tracks how many owners currently exist. The memory is only deleted when the last owner goes away and the count hits zero.

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

int main()
{
    shared_ptr<int> a = make_shared<int>(42);

    cout << "count: " << a.use_count() << endl;   // 1 owner

    {
        shared_ptr<int> b = a;   // copy allowed! now 2 owners

        cout << "count: " << a.use_count() << endl;   // 2 owners
    }   // b goes out of scope here ─ count drops back to 1

    cout << "count: " << a.use_count() << endl;   // 1 owner again

    return 0;
}

The output will be ─

count: 1
count: 2
count: 1

🥳 Watch the count go up and down. When we copied a into b, the count rose to 2 ─ both now own the same integer. When b went out of scope (at the closing }), the count dropped back to 1. The memory itself is only freed when that count finally reaches 0 ─ which happens when a dies at the end of main.

Think of it like a shared apartment 🏠 ─ the lease stays active as long as at least one person still lives there. Only when the last roommate moves out does the landlord reclaim it. shared_ptr counts the roommates for us.

We used make_shared<int>(42) here ─ again, the preferred way to create a shared_ptr, mirroring make_unique from before.

3. weak_ptr ─ looking without owning 👀

Now here’s a subtle problem that shared_ptr alone can create. What if two objects each hold a shared_ptr to each other? 🤔

Then object A keeps B alive, and B keeps A alive. Neither one’s count can ever reach zero, because each is waiting for the other to let go first. They keep each other alive forever ─ a leak, even with smart pointers! This nasty situation is called a circular reference 😱

The cure is weak_ptr. A weak_ptr can look at memory owned by a shared_ptr, but it does not own it and does not increase the reference count. It’s a non-owning observer. Because it doesn’t add to the count, it can’t cause a circular reference.

Since a weak_ptr doesn’t own the memory, we can’t use it directly. We first have to ask it for a temporary shared_ptr using .lock() ─ which also safely checks whether the memory still exists.

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

int main()
{
    shared_ptr<int> owner = make_shared<int>(42);

    // a weak_ptr observes, but does NOT own
    weak_ptr<int> observer = owner;

    cout << "count: " << owner.use_count() << endl;   // still 1! weak_ptr didn't add

    // to use it, lock it into a temporary shared_ptr
    if (shared_ptr<int> temp = observer.lock())
    {
        cout << "observer sees: " << *temp << endl;
    }

    return 0;
}

The output will be ─

count: 1
observer sees: 42

🥳 See how the reference count stayed at 1, even though observer is pointing at the same memory? That’s the whole point ─ a weak_ptr watches without owning. And by using .lock(), we safely got access only because the memory was still alive. If the owner had already been destroyed, .lock() would have returned an empty pointer, and we’d have safely skipped the if block.

weak_ptr is more advanced, so don’t worry if it feels tricky ─ just remember its purpose ─ to observe shared memory without keeping it alive.

Smart pointers with arrays

Just like raw new[], smart pointers can manage arrays too. For a unique_ptr, we simply use the array form unique_ptr<type[]>

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

int main()
{
    // a unique_ptr managing an array of 5 integers
    unique_ptr<int[]> arr = make_unique<int[]>(5);

    for (int i = 0; i < 5; i++)
    {
        arr[i] = (i + 1) * 10;   // use it just like an array!
    }

    for (int i = 0; i < 5; i++)
    {
        cout << arr[i] << " ";
    }
    cout << endl;

    // no delete[] needed ─ handled automatically! 🥳

    return 0;
}

The output will be ─

10 20 30 40 50

🥳 Remember how last post we had to carefully match new[] with delete[] and never forget the brackets? All that worry disappears here. The unique_ptr<int[]> knows it’s managing an array and calls the correct delete[] for us automatically. And thanks to array-pointer decay from the arrays post, we still use arr[i] exactly like normal 🥳

A quick comparison of the three

Let’s put all three smart pointers side by side so their differences are crystal clear.

unique_ptr shared_ptr weak_ptr
Owns the memory? Yes (alone) Yes (shared) No
Can be copied? No (only moved) Yes Yes
Counts owners? No Yes No
Frees memory when? it goes out of scope last owner is gone never (doesn’t own)
Use it when… one clear owner many shared owners observing without owning

A simple rule of thumb 🤔 ─ reach for unique_ptr by default. Only upgrade to shared_ptr when you truly need multiple owners. And only bring in weak_ptr to break a circular reference between shared_ptrs.

Where are smart pointers actually used? 🌎

This all sounds nice in a tutorial, but you might be wondering ─ “do professionals really use these in serious software?” Absolutely 🥳 Smart pointers are everywhere in modern C++, especially in fields where a single memory bug can be catastrophic or expensive. Let’s look at a few real domains.

Game development 🎮

This is one of the biggest users of smart pointers. A game has thousands of objects ─ textures, sounds, 3D models, enemies ─ and many parts of the game may need the same object at once. A shared_ptr is a natural fit for a resource manager, where several systems (rendering, physics, audio) all share ownership of the same texture or model, and it’s freed only when nobody needs it anymore.

In fact, C++ powers most major game engines, and one of the most famous ones ─ Unreal Engine ─ ships its very own smart pointer library. It is a custom implementation of C++11 smart pointers designed to ease the burden of memory allocation and tracking, including the industry-standard shared pointers, weak pointers, and unique pointers. So the exact three types we learned today ─ unique_ptr, shared_ptr, weak_ptr ─ appear right inside a real, professional game engine 🥳

Interestingly, Unreal’s own documentation offers a mature, real-world caution too ─ smart pointers are well-suited for high-level systems, resource management, and tools programming, but some of them are slower than raw pointers, which makes them less useful in low-level engine code such as rendering. This is a great lesson ─ smart pointers are wonderful, but professionals still choose the right tool for the job 🤔

Systems programming 🖥️

Systems programming means building the low-level software that other programs run on ─ operating system components, drivers, compilers, databases, and browsers. Here, programs run for a very long time (sometimes for months without restarting), so even a tiny memory leak that repeats will eventually pile up and crash the machine. Smart pointers guarantee cleanup, which makes long-running system software far more reliable. C++ remains essential for systems programming, embedded systems, and performance-critical applications, and RAII-based smart pointers are a core part of writing safe code in these areas.

Finance and high-frequency trading 💲

In high-frequency trading, programs execute enormous numbers of transactions extremely fast, and they must never crash mid-trade or slowly leak memory over a trading day. Trading platforms and financial modeling systems use C++ to execute millions of transactions per second with minimal latency. Smart pointers help these systems stay both fast and leak-free, which is exactly the combination this field demands 🥳

Embedded systems and medical devices 🏥

Embedded systems are the small computers inside everyday devices ─ cars, appliances, and medical equipment. Such devices often use C++ because it provides both direct control over hardware and object-oriented features. In something like a heart monitor or a car’s braking system, a memory bug isn’t just annoying ─ it could be dangerous. The automatic, guaranteed cleanup of smart pointers is a genuine safety feature here.

The common thread 🧵

Notice the pattern across all of these ─ they’re fields where software runs for a long time, handles many shared objects, or simply cannot afford to crash. That’s precisely where the “never forget to clean up” guarantee of smart pointers becomes priceless. In modern C++, there is little reason not to use a smart pointer or an STL container ─ they prevent leaks, help with exception safety, and make code far easier to reason about 🥳

So why did we learn raw pointers at all? 🤔

You might be wondering ─ “if smart pointers are this safe and easy, why did we spend six posts on raw pointers, new, and delete?”

Great question, and here’s the honest answer ─ smart pointers are built on top of everything we learned. A unique_ptr is just a raw pointer wrapped in an object that remembers to call delete. Under the hood, it’s doing the exact new/delete dance from last post ─ it just does it for us, at exactly the right moment.

Because we understand the raw machinery, we now understand why smart pointers behave the way they do ─ why unique_ptr can’t be copied, why shared_ptr counts, why weak_ptr exists. Without the foundation, these would all feel like arbitrary magic. With it, they make perfect sense 🥳

This is the beautiful payoff of the whole series ─ you don’t just know how to use pointers, you understand what they really are.

So what should we remember? 🤔

Let’s wrap up the key takeaways ─

  • Smart pointers (in the <memory> header) wrap raw pointers and free memory automatically when they go out of scope.
  • This automatic cleanup is powered by RAII ─ tying a resource’s life to an object’s life.
  • unique_ptr ─ one sole owner, can’t be copied (only moved with std::move). Your default choice.
  • shared_ptr ─ multiple owners sharing memory, tracked by a reference count; freed when the count hits zero.
  • weak_ptr ─ a non-owning observer that doesn’t affect the count; use .lock() to access it safely, and use it to break circular references.
  • Prefer make_unique and make_shared over calling new yourself.
  • Smart pointers handle arrays too (e.g. unique_ptr<int[]>), and no manual delete[] is needed.
  • Smart pointers are used heavily in real industries ─ game engines (Unreal Engine ships its own), systems programming, high-frequency trading, and embedded/medical devices ─ anywhere a memory bug would be costly or dangerous.
  • Smart pointers are built directly on the raw new/delete foundation ─ which is exactly why learning the raw version first was worth it.

Once these click, you can enjoy all the power of dynamic memory without the fear of leaks and dangling pointers 👀✨

Congratulations 🥳🥳🥳
We now understand smart pointers ─ the modern, safe way to handle dynamic memory in C++.

In the next post we will talk about pointers to pointers ─ what it means when a pointer points at another pointer, and why that double ** shows up more often than you’d think 😉

Happy Coding 💻 🎵

Leave a Comment

Your email address will not be published. Required fields are marked *