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.

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:
- Include . All three types live there.
- 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.
- 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.
- 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.
- 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.
- Never call .get() and then manually delete or free() the result. That defeats the entire purpose and will cause a double free.
- 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.

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.
