C++ Smart Pointers
A smart pointer is an object that wraps a raw pointer and automatically manages the lifetime of the memory it points to. Instead of calling new and remembering to call delete at exactly the right moment, you let the smart pointer’s destructor do it for you. Smart pointers live in the <memory> header and were standardized in C++11, and they are now the default way experienced C++ programmers manage dynamically allocated objects. They matter because manual memory management is one of the biggest sources of bugs in C++: forgetting to free memory causes leaks, freeing it twice causes crashes, and using it after it’s freed causes undefined behavior. Smart pointers eliminate almost all of these mistakes while adding little to no runtime cost.
Overview: How Smart Pointers Work
Smart pointers are built on a C++ idiom called RAII (Resource Acquisition Is Initialization). The idea is simple: tie a resource’s lifetime to an object’s lifetime. When the object is constructed, it acquires the resource (in this case, memory from new). When the object goes out of scope, its destructor runs automatically and releases the resource. Because destructors run deterministically in C++ — the moment a stack variable’s scope ends, or a member’s owning object is destroyed — the memory is freed at a precise, predictable point, even if an exception is thrown along the way.
There are three smart pointer types in the standard library, each with a different ownership model:
std::unique_ptr<T>— represents exclusive ownership. Only oneunique_ptrcan own a given object at a time. It cannot be copied, only moved. Internally it is usually just a single raw pointer with no extra bookkeeping, so it has essentially zero overhead compared to a raw pointer.std::shared_ptr<T>— represents shared ownership. Multipleshared_ptrinstances can point to the same object, and the object is destroyed only when the last one is destroyed. Internally, everyshared_ptrkeeps a pointer to a heap-allocated control block that stores a strong reference count (how manyshared_ptrs own the object) and a weak reference count (how manyweak_ptrs observe it). Incrementing and decrementing these counts is done atomically, which makes copying ashared_ptrthread-safe, at the cost of a small amount of overhead versus a raw pointer.std::weak_ptr<T>— a non-owning observer of an object managed by ashared_ptr. It does not affect the strong reference count, so it does not keep the object alive. To actually use the object, you call.lock(), which returns a temporaryshared_ptrif the object is still alive, or an empty one if it has already been destroyed.
When you write std::make_shared<T>(args...), the library allocates the control block and the object together in a single heap allocation, which is faster and more cache-friendly than writing std::shared_ptr<T>(new T(args...)), which performs two separate allocations (one for the object, one for the control block created afterward by the shared_ptr constructor).
Syntax
std::unique_ptr<Type> ptr1 = std::make_unique<Type>(constructorArgs...);
std::shared_ptr<Type> ptr2 = std::make_shared<Type>(constructorArgs...);
std::weak_ptr<Type> ptr3 = ptr2; // observes ptr2's object without owning it
| Member / function | Applies to | Meaning |
|---|---|---|
make_unique<T>(args) |
unique_ptr | Constructs T and wraps it in a unique_ptr in one step |
make_shared<T>(args) |
shared_ptr | Constructs T and its control block in a single allocation |
.get() |
all | Returns the raw pointer without transferring ownership |
.reset() |
unique_ptr, shared_ptr | Releases the current object, optionally replacing it |
.release() |
unique_ptr only | Gives up ownership and returns the raw pointer without deleting it |
.use_count() |
shared_ptr | Number of shared_ptrs that currently own the object |
.lock() |
weak_ptr | Returns a shared_ptr to the object, or an empty one if it’s gone |
.expired() |
weak_ptr | True if the observed object has already been destroyed |
Examples
Example 1: unique_ptr and exclusive ownership
#include <iostream>
#include <memory>
#include <string>
class Resource {
public:
Resource(const std::string& name) : name_(name) {
std::cout << "Acquiring resource: " << name_ << std::endl;
}
~Resource() {
std::cout << "Releasing resource: " << name_ << std::endl;
}
void use() const {
std::cout << "Using resource: " << name_ << std::endl;
}
private:
std::string name_;
};
int main() {
std::unique_ptr<Resource> res1 = std::make_unique<Resource>("File Handle");
res1->use();
std::unique_ptr<Resource> res2 = std::move(res1);
if (!res1) {
std::cout << "res1 is now empty after move" << std::endl;
}
res2->use();
return 0;
}
Output:
Acquiring resource: File Handle
Using resource: File Handle
res1 is now empty after move
Using resource: File Handle
Releasing resource: File Handle
The unique_ptr named res1 owns the Resource. Because unique_ptr cannot be copied, transferring ownership to res2 requires std::move, which leaves res1 empty (equivalent to holding nullptr). When res2 goes out of scope at the end of main, its destructor automatically deletes the Resource — no manual delete was ever written.
Example 2: shared_ptr and reference counting
#include <iostream>
#include <memory>
#include <vector>
class Sensor {
public:
Sensor(int id) : id_(id) {
std::cout << "Sensor " << id_ << " created" << std::endl;
}
~Sensor() {
std::cout << "Sensor " << id_ << " destroyed" << std::endl;
}
int id() const { return id_; }
private:
int id_;
};
int main() {
std::vector<std::shared_ptr<Sensor>> registry;
std::shared_ptr<Sensor> s1 = std::make_shared<Sensor>(101);
std::cout << "Use count after creation: " << s1.use_count() << std::endl;
registry.push_back(s1);
std::cout << "Use count after adding to registry: " << s1.use_count() << std::endl;
{
std::shared_ptr<Sensor> s2 = s1;
std::cout << "Use count with local copy: " << s1.use_count() << std::endl;
}
std::cout << "Use count after local copy goes out of scope: " << s1.use_count() << std::endl;
return 0;
}
Output:
Sensor 101 created
Use count after creation: 1
Use count after adding to registry: 2
Use count with local copy: 3
Use count after local copy goes out of scope: 2
Sensor 101 destroyed
Every time a shared_ptr is copied — into the vector, or into the local variable s2 — the control block’s strong reference count increases. When s2 goes out of scope at the end of the inner block, the count drops back down. The Sensor object itself is only destroyed once the very last owner (here, the registry vector and s1) is gone at the end of main.
Example 3: weak_ptr to avoid reference cycles
#include <iostream>
#include <memory>
class Node {
public:
int value;
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // weak_ptr avoids a reference cycle
Node(int v) : value(v) {
std::cout << "Node " << value << " created" << std::endl;
}
~Node() {
std::cout << "Node " << value << " destroyed" << std::endl;
}
};
int main() {
std::shared_ptr<Node> first = std::make_shared<Node>(1);
std::shared_ptr<Node> second = std::make_shared<Node>(2);
first->next = second;
second->prev = first; // weak_ptr, does not add to use_count
std::cout << "first use_count: " << first.use_count() << std::endl;
std::cout << "second use_count: " << second.use_count() << std::endl;
if (auto locked = second->prev.lock()) {
std::cout << "second's prev is node " << locked->value << std::endl;
}
return 0;
}
Output:
Node 1 created
Node 2 created
first use_count: 1
second use_count: 2
second's prev is node 1
Node 1 destroyed
Node 2 destroyed
If both next and prev were shared_ptr, the two nodes would keep each other’s reference count above zero forever, and neither would ever be destroyed — a memory leak called a reference cycle. By making prev a weak_ptr, it can still be used (via .lock()) to reach the other node, but it does not keep it alive. Notice that first‘s use_count is only 1, even though second->prev points at it — a weak reference simply doesn’t count.
Under the Hood
When a unique_ptr goes out of scope, its destructor runs a single check (‘am I null?’) and, if not, calls delete on the raw pointer it holds — there is no shared state to update, which is why it is essentially free compared to a raw pointer.
When a shared_ptr is copied, it atomically increments the strong count in the shared control block; when a copy is destroyed, it atomically decrements that same count. Only when the count reaches zero does the control block call the object’s destructor and free its memory. The control block itself (which also tracks the weak count) is only freed once both the strong count and the weak count reach zero — which is why a lingering weak_ptr keeps a small control-block allocation alive even after the object it observes has been destroyed.
Calling .lock() on a weak_ptr atomically checks whether the strong count is still greater than zero. If it is, it increments the count and returns a valid shared_ptr; if the object has already been destroyed, it returns an empty shared_ptr instead of a dangling pointer. This is what makes weak_ptr safe for caches and observer patterns: you never accidentally dereference freed memory.
Common Mistakes
Mistake 1: Trying to copy a unique_ptr. Because ownership must stay exclusive, the compiler deletes unique_ptr‘s copy constructor. This code fails to compile:
std::unique_ptr<int> a = std::make_unique<int>(42);
std::unique_ptr<int> b = a; // error: call to deleted copy constructor
Use std::move to transfer ownership instead:
std::unique_ptr<int> a = std::make_unique<int>(42);
std::unique_ptr<int> b = std::move(a);
std::cout << *b << std::endl;
Mistake 2: Constructing two independent shared_ptrs from the same raw pointer. Each shared_ptr constructed directly from a raw pointer creates its own control block that knows nothing about the other one. When both go out of scope, the object gets deleted twice — undefined behavior that often crashes:
int* raw = new int(42);
std::shared_ptr<int> p1(raw);
std::shared_ptr<int> p2(raw); // WRONG: two unrelated control blocks, double free
The fix is to only ever create one owning shared_ptr (preferably via make_shared) and share it by copying, so all owners use the same control block:
std::shared_ptr<int> p1 = std::make_shared<int>(42);
std::shared_ptr<int> p2 = p1; // correct: shares the same control block
std::cout << p1.use_count() << std::endl;
Best Practices
- Default to
unique_ptrfor ownership; reach forshared_ptronly when an object genuinely needs multiple owners. - Always create smart pointers with
make_uniqueormake_sharedinstead of pairing a rawnewwith a smart pointer constructor. - Never mix manual
deletewith memory already owned by a smart pointer. - Use
weak_ptrto break reference cycles (parent/child, observer/subject) and for caches where an entry may legitimately disappear. - Pass a raw pointer or reference to functions that only need to use an object, and reserve passing a
shared_ptr/unique_ptrby value for functions that actually take or share ownership. - Never call
.get()to construct a second, independent smart pointer over the same memory. - Use
unique_ptrto return dynamically allocated objects from factory functions — ownership transfers cleanly with no leak risk even if an exception is thrown.
Practice Exercises
- Write a
Loggerclass that prints a message in its constructor and destructor. Create it withstd::make_unique, call a method on it, and confirm it is destroyed automatically at the end ofmain. - Create two
std::shared_ptr<int>instances pointing at the same value usingmake_sharedand copy assignment. Print.use_count()before and after one of them is reset with.reset(), and explain the change. - Build a small two-node linked structure where each node holds a
shared_ptrto the next node and aweak_ptrback to the previous node. Verify with.use_count()that no reference cycle exists.
Summary
- Smart pointers use RAII to tie memory lifetime to object lifetime, so memory is freed automatically and deterministically.
unique_ptrgives exclusive, move-only ownership with essentially zero overhead over a raw pointer.shared_ptrgives shared ownership backed by an atomically reference-counted control block; the object is destroyed when the last owner disappears.weak_ptrobserves ashared_ptr-managed object without owning it, and.lock()safely checks whether it is still alive.- Prefer
make_unique/make_sharedover rawnew, and never construct two independent smart pointers from the same raw pointer.
