What Are Smart Pointers in C++? Types, Examples, and How to Use Them

What Are Smart Pointers in C++ Types, Examples, and How to Use Them

If you’ve spent any real time writing C++, you’ve hit this bug at least once: a new somewhere in a function, an early return on some edge case, and no matching delete on that path. Nothing crashes immediately. The program just quietly leaks memory until it doesn’t, and by the time you notice, you’re chasing the leak through a codebase that’s grown ten times larger since you wrote that function.

Smart pointers exist to make that specific class of bug much harder to write by accident. They’re not a niche feature or a “nice to have” from a style guide, they’ve been the default way to manage dynamically allocated memory in modern C++ since C++11, and most production codebases today treat a raw new/delete pair as a code smell unless there’s a specific reason for it.

This guide walks through what smart pointers in C++ actually are, how they work under the hood, the three types you’ll use in real code, and how to bring them into your own projects, including a few things worth knowing if you’re writing for embedded or resource-constrained targets, where “just use the heap” isn’t always the right answer.

A smart pointer in C++ is an object that wraps a raw pointer and automatically manages the lifetime of the memory it points to, it calls delete for you when the object is no longer needed. Defined in the header since C++11, the three main types are std::unique_ptr (exclusive ownership), std::shared_ptr (shared, reference-counted ownership), and std::weak_ptr (a non-owning reference used alongside shared_ptr).

What Is a Smart Pointer in C++?

A smart pointer is a class template that behaves like a pointer – you can dereference it, follow it to a member, check it against nullptr – but it also owns the memory it points to and cleans that memory up automatically when it’s no longer needed.

Compare that to a raw pointer. A raw pointer (Sensor* s = new Sensor();) is just an address. It doesn’t know whether it owns the memory, whether something else is also pointing at it, or when it’s safe to free. That responsibility sits entirely with you, spread across however many functions touch that pointer. Miss a delete and you leak memory. Call delete twice and you get undefined behavior. Use the pointer after deleting it and you get a dangling pointer bug that might not even show up until production.

Smart pointers fix this with a pattern called RAII – Resource Acquisition Is Initialization. The idea is simple: tie a resource’s lifetime to an object’s lifetime. The constructor acquires the resource, the destructor releases it. Since C++ guarantees a local object’s destructor runs when it goes out of scope – a normal return, an early return, or the stack unwinding because of a thrown exception – cleanup happens automatically, every time, without you writing a single delete.

Because smart pointers overload operator* and operator->, swapping a raw pointer for a smart one in existing code usually needs very little change:

// Raw pointer - you own this delete call

Sensor* s1 = new Sensor();

s1->read();

delete s1;

// Smart pointer - cleanup happens automatically

auto s2 = std::make_unique();

s2->read();

// no delete needed - s2 is freed when it goes out of scope

Smart pointers are a C++ Standard Library feature, not a language keyword – they’re ordinary class templates defined in . That also means they’re C++-only. Plain C has no direct equivalent, though GCC and Clang offer a 

_attribute_((cleanup)) extension that some embedded C codebases use for similar scope-based cleanup.

How Do Smart Pointers Work in C++?

Under the hood, a smart pointer’s constructor takes ownership of a raw pointer, and its destructor calls delete (or a custom cleanup function) on that pointer. Everything else – the pointer-like syntax, the ownership rules, the reference counting in shared_ptr – is built on top of that basic RAII mechanism.

The three standard smart pointer types differ mainly in who’s allowed to own the object:

  • std::unique_ptr – exactly one owner at a time. Ownership can move from one unique_ptr to another, but it can never be duplicated.
  • std::shared_ptr – multiple simultaneous owners, tracked with a reference count. The object is destroyed when the last owner goes away.
  • std::weak_ptr – no ownership at all. It refers to an object managed by a shared_ptr without keeping it alive, and it can check whether that object still exists.

That ownership model is the whole point. Once you know who owns an object, you know exactly when it gets destroyed – no guessing, no relying on comments or naming conventions to communicate intent.

One thing worth knowing: shared_ptr and weak_ptr didn’t start in the standard library – they originated in Boost and were folded into C++11 largely unchanged. unique_ptr also arrived in C++11, but std::make_unique wasn’t added until C++14, an acknowledged gap in the original release. If you’re stuck on an older C++11-only toolchain, you’ll need unique_ptr without make_unique.

registor_now_P

Types of Smart Pointers in C++

C++ gives you three smart pointer types in active use today, plus one you’ll still see in older code but shouldn’t write yourself.

std::unique _ptr – Exclusive Ownership

unique_ptr is the default choice for most owned resources. It can’t be copied – only moved – which means at any point in the program, there’s exactly one unique_ptr responsible for a given object.

#include 

std::unique_ptr createSensor() {

    return std::make_unique();

}

void useSensor() {

    auto sensor = createSensor();   // ownership moves in from the function

    sensor->read();

}   // sensor destroyed here automatically

Use it for factory functions, PIMPL-style implementation pointers, and basically any case where one part of your code is clearly responsible for an object’s lifetime.

std::shared_ptr – Shared Ownership

shared_ptr uses reference counting: every time a shared_ptr is copied, the count goes up; every time one is destroyed, the count goes down. When the count hits zero, the object is deleted.

#include 

#include

class SensorDriver {

public:

    explicit SensorDriver(int id) : id_(id) {

        std::cout << "Driver " << id_ << " initialized\n";

    }

    ~SensorDriver() {

        std::cout << "Driver " << id_ << " released\n";

    }

private:

    int id_;

};

void useDriver() {

    auto driver = std::make_shared(1);

    {

        auto driverCopy = driver;                   // reference count is now 2

        std::cout << driver.use_count() << "\n";    // prints 2

    }                                                 // driverCopy destroyed, count drops to 1

    std::cout << driver.use_count() << "\n";         // prints 1

}   // driver destroyed here, count drops to 0, SensorDriver is released

Reach for shared_ptr when an object genuinely needs multiple independent owners – a cache entry referenced from several places, or nodes in a graph structure where no single node is the “real” owner. It shouldn’t be your default; reference counting adds overhead a unique_ptr doesn’t have, and it’s easy to end up with unclear ownership if it’s used everywhere out of habit.

One caveat worth knowing: std::make_shared allocates the control block (the reference counts) and the object itself in a single block of memory, which is more efficient than shared_ptr(new T()). The trade-off is that if a weak_ptr outlives every shared_ptr, that entire block – including the space for the object – stays allocated until the last weak_ptr is gone too. Rarely an issue in practice, but worth knowing if you’re tracking memory closely.

std::weak_ptr – A Non-Owning Reference

weak_ptr points to an object managed by a shared_ptr without contributing to its reference count. Because it doesn’t own the object, it can’t access it directly – you call .lock(), which returns a working shared_ptr if the object still exists, or an empty one if it’s already been destroyed.

std::weak_ptr weakRef = driver;

if (auto locked = weakRef.lock()) {

    // object still exists - safe to use 'locked' here

} else {

    // the object weakRef referred to has already been destroyed

}

weak_ptr is mainly used for two things: observing an object without keeping it alive (a cache that shouldn’t prevent cleanup, a child referencing its parent), and breaking reference cycles — a real limitation of shared_ptr covered below.

std::auto_ptr – Deprecated, Don’t Use It

If you’re reading older C++ tutorials or maintaining legacy code, you may run into std::auto_ptr. It was C++98’s early attempt at an owning pointer, and it had a serious flaw: copying an auto_ptr silently transferred ownership instead of either duplicating the object or refusing to compile, which caused subtle bugs whenever one was passed by value. It was deprecated in C++11 and removed entirely from the standard in C++17. If you see it anywhere, treat it as a sign to modernize that code to unique_ptr.

Smart Pointers in C++ with Example

Here’s a slightly more complete example that shows why weak_ptr matters in practice – a doubly-linked structure where each node points to the next and back to the previous:

#include 

struct Node {

    std::shared_ptr next;

    std::weak_ptr prev;   // weak_ptr avoids a reference cycle

};

void buildList() {

    auto first  = std::make_shared();

    auto second = std::make_shared();

    first->next = second;

    second->prev = first;   // if this were shared_ptr, neither node

                             // would ever reach a reference count of zero

}

If prev were a shared_ptr instead of a weak_ptr, first and second would reference each other forever. Neither reference count would ever hit zero, and both nodes would leak – even though nothing outside the function still holds a pointer to either one.

Smart pointers also generalize well beyond plain memory. Any resource that needs a matching “release” call – a file handle, a mutex, a hardware peripheral – can be wrapped the same way using a custom deleter:

#include 

#include

struct FileCloser {

    void operator()(FILE* fp) const {

        if (fp) fclose(fp);

    }

};

void readConfig() {

    std::unique_ptr<FILE, FileCloser> file(fopen("config.txt", "r"));

    if (file) {

        // read from file

    }

}   // file is closed automatically - even if an exception is thrown here

This is the same RAII idea applied to a resource that isn’t heap memory at all, and it’s a genuinely useful pattern once you start looking for places to use it.

How to Use Smart Pointers in C++

Getting smart pointers into your own code comes down to a handful of habits:

  1. Include . All three types live there.
  2. Prefer make_unique and make_shared over calling new directly. They’re exception-safe, and they remove one more place where you could mistype something and leak memory.
  3. Decide on ownership before you decide on a type. If exactly one part of your code owns the object, that’s unique_ptr. If ownership is genuinely shared, that’s shared_ptr. If you just need to observe an object without owning it, that’s weak_ptr.
  4. Pass ownership explicitly. Take a unique_ptr by value (and std::move() it in) if a function should take ownership. Pass a reference to the underlying object, or a raw pointer, if the function only needs to use it.
  5. Use weak_ptr to break cycles or observe safely. Any time two objects might reference each other through shared_ptr, one of those references should be a weak_ptr.
  6. Never call .get() and then manually delete or free() the result. That defeats the entire purpose and will cause a double free.
  7. Don’t mix a raw pointer and a smart pointer for the same object. Wrap it once, in one place, and let the smart pointer manage it exclusively from there on.

Explore Courses - Learn More

unique_ptr vs shared_ptr vs weak_ptr – Quick Comparison

 

unique_ptr

shared_ptr

weak_ptr

Ownership

Exclusive – one owner

Shared – reference-counted

None – non-owning

Copyable

No (move-only)

Yes

Yes

Runtime overhead

Minimal – same size as a raw pointer in the default case

Higher – control block plus atomic reference counting

Similar to shared_ptr‘s control block cost

Access

Direct (*, ->)

Direct (*, ->)

Indirect – must call .lock() first

Typical use case

Default choice; factory return values, sole-owner resources

Objects with genuinely multiple owners

Breaking reference cycles, safe observation

Introduced

C++11 (make_unique in C++14)

C++11

C++11

Smart Pointers in Embedded and Resource-Constrained C++

Most C++ tutorials assume you’re running on a system with virtual memory and a heap that can absorb allocation overhead without much thought. That assumption doesn’t always hold on a microcontroller with a few tens of kilobytes of SRAM and no MMU.

A few things are worth knowing if you’re writing C++ for an embedded target:

  • unique_ptr itself adds almost no overhead. It’s typically the same size as the raw pointer it wraps, and its destructor call compiles down to essentially the same code you’d write by hand. It’s the underlying dynamic allocation – the new it wraps – that carries risk on constrained targets, not unique_ptr as a language feature.
  • shared_ptr has real, measurable cost. Beyond the control block’s extra bookkeeping, every copy and destruction increments or decrements an atomic reference count, so the operation is thread-safe by default. On cores without native atomic instructions, that atomicity sometimes has to be emulated – for instance, by briefly disabling interrupts – which adds latency you generally don’t want inside an ISR or a tight control loop.
  • Many safety-critical and automotive coding guidelines, MISRA C++ among them, restrict or forbid dynamic memory allocation after initialization. This is about guaranteeing bounded, predictable execution time and avoiding heap fragmentation in a device that might run for months without a restart, not about smart pointers specifically. Teams working under those constraints typically avoid heap-based allocation of any kind in the runtime path, smart pointer or otherwise.
  • A common, genuinely useful pattern is unique_ptr with a custom deleter over statically or pool-allocated resources. You get the RAII guarantee, automatic, exception-safe cleanup — without the object ever coming from the heap at runtime. This works well for wrapping a peripheral handle, a slot from a fixed DMA buffer pool, or a critical-section guard.

If you’re targeting an embedded platform, check your specific toolchain before committing to any of this — some embedded compiler configurations disable exceptions or ship a reduced C++ standard library, and support can vary between vendor SDKs and the standard GCC ARM toolchain.

Advantages and Limitations

What smart pointers get you:

  • Automatic, scope-tied cleanup, no delete to remember or forget
  • Exception safety, the object is still cleaned up correctly if an exception unwinds the stack between allocation and where you intended to free it
  • Ownership stated directly in the type, so anyone reading the code knows who’s responsible for an object without hunting through the rest of the file
  • Protection against an entire category of bugs: double frees, leaks from early returns, and (in well-behaved code) use-after-free

What they don’t solve:

  • shared_ptr‘s reference counting is thread-safe, but that only protects the count itself. Reading or writing the object it points to from multiple threads still needs its own synchronization, a shared_ptr doesn’t make the object it manages thread-safe.
  • Two shared_ptr instances that reference each other create a cycle. Since neither reference count ever reaches zero, both objects leak, exactly what weak_ptr exists to prevent.
  • Smart pointers manage memory correctly; they don’t manage program logic. Holding a shared_ptr longer than you actually need to still keeps that object alive longer than intended, even though nothing is “leaking” in the traditional sense.

Final Thoughts

If you take one habit away from this: default to unique_ptr. Reach for shared_ptr only when an object genuinely needs more than one owner, and use weak_ptr the moment you notice two objects might reference each other. On embedded targets, the same RAII discipline still applies, you just apply it to statically or pool-allocated resources with a custom deleter instead of letting the heap decide.

None of this is about following a rule for its own sake. It’s about writing code where anyone reading it, including you, six months from now, can tell exactly who owns what, and exactly when it gets cleaned up.

Talk to Academic Advisor

FAQs

unique_ptr has effectively no overhead beyond the allocation it wraps. shared_ptr is measurably slower due to its control block and atomic reference counting, though this rarely matters outside of tight loops or embedded targets with limited resources.

Yes, in one specific case: two shared_ptr instances that reference each other will never reach a reference count of zero, so neither object is ever freed. Use weak_ptr for one side of that relationship to break the cycle.

It depends on your target and coding standard. unique_ptr with statically or pool-allocated resources is generally low-overhead and safe. Teams following MISRA C++ or similar guidelines that restrict dynamic allocation typically avoid heap-based smart pointers in the runtime path, using them mainly at initialization time, if at all.

Yes. std::unique_ptr<T[]> supports arrays directly and calls delete[] correctly. std::shared_ptr<T[]> gained the same support in C++17 — before that, it needed a custom deleter for arrays. That said, prefer std::vector in most cases, since it carries its size along with it.

Author

Embedded Systems and IOT Trainer– IIES

Updated On: 18-08-26


10+ years of hands-on experience delivering practical training in Embedded Systems and it's design