C++ unique_ptr and shared_ptr
A smart pointer is a class that wraps a raw pointer and automatically manages the lifetime of the object it points to, freeing memory the moment it is no longer needed. C++’s two essential smart pointers — std::unique_ptr and std::shared_ptr — live in the <memory> header and let you write code that almost never calls delete directly, eliminating the most common sources of memory leaks, double frees, and dangling pointers. unique_ptr models exclusive ownership: exactly one owner is responsible for the resource. shared_ptr models shared ownership: any number of owners can hold the resource, and it is destroyed only when the last one lets go. Choosing correctly between them, and understanding what happens under the hood, is one of the most important skills in modern C++.
Overview: How Smart Pointers Work
Both smart pointers are built on RAII (Resource Acquisition Is Initialization): a resource is acquired in a constructor and released in a destructor, so its lifetime is tied to an object’s scope rather than to manual delete calls scattered through your code. When a smart pointer goes out of scope — because a function returns, an exception is thrown, or a container holding it is destroyed — its destructor runs automatically and frees the managed object. This is what makes smart pointers exception-safe: even if an exception unwinds the stack, the destructor still runs.
std::unique_ptr is essentially a thin, zero-overhead wrapper around a raw pointer. With the default deleter it stores nothing more than that one pointer, and its destructor simply calls delete on it. Because ownership must stay exclusive, unique_ptr‘s copy constructor and copy assignment operator are deleted — you cannot copy one. You can, however, move one: moving transfers the raw pointer from the source to the destination and sets the source to nullptr, so only one unique_ptr ever points at the object at a time. This makes unique_ptr a practically free replacement for a raw owning pointer.
std::shared_ptr is more involved. In addition to a pointer to the managed object, every shared_ptr stores a pointer to a shared control block. The control block holds a strong reference count (how many shared_ptrs own the object), a weak reference count (how many weak_ptrs are observing it), and the deleter. Copying a shared_ptr increments the strong count; destroying one decrements it — both using atomic operations, so this bookkeeping is safe even when different threads hold copies of the same shared_ptr. When the strong count reaches zero, the managed object is destroyed immediately. When both counts reach zero, the control block itself is freed. This bookkeeping means shared_ptr is heavier than unique_ptr — copying one is not free — so it should be reserved for cases where ownership is genuinely shared.
Syntax
// unique_ptr: exclusive ownership
std::unique_ptr<T> ptr = std::make_unique<T>(args...);
// shared_ptr: shared ownership
std::shared_ptr<T> ptr = std::make_shared<T>(args...);
// weak_ptr: non-owning observer of a shared_ptr
std::weak_ptr<T> weak = sharedPtr;
std::unique_ptr<T>/std::shared_ptr<T>— the smart pointer type, templated on the pointee typeT.std::make_unique<T>(args...)/std::make_shared<T>(args...)— factory functions that allocate and constructTin one exception-safe step. Always prefer these over a barenew.*ptr,ptr->member— dereference the smart pointer to access the pointee, exactly like a raw pointer.ptr.get()— returns the underlying raw pointer without transferring or affecting ownership.ptr.reset()— destroys the currently owned object (if any) and optionally takes ownership of a new one.ptr.use_count()(shared_ptr only) — the number of shared_ptr instances currently sharing ownership.weak.lock()(weak_ptr only) — returns ashared_ptrto the object if it still exists, or an empty one if it has already been destroyed.
Examples
Example 1: Basic unique_ptr ownership
#include <iostream>
#include <memory>
class Resource {
public:
Resource(int id) : id_(id) {
std::cout << "Resource " << id_ << " created\n";
}
~Resource() {
std::cout << "Resource " << id_ << " destroyed\n";
}
void greet() const {
std::cout << "Hello from resource " << id_ << "\n";
}
private:
int id_;
};
int main() {
std::unique_ptr<Resource> res = std::make_unique<Resource>(1);
res->greet();
return 0;
}
Output:
Resource 1 created
Hello from resource 1
Resource 1 destroyed
make_unique allocates the Resource and hands ownership straight to res. Calling res->greet() works exactly like a raw pointer, and when main returns, res‘s destructor runs automatically, deleting the Resource — no explicit delete was ever written.
Example 2: Moving a unique_ptr
#include <iostream>
#include <memory>
#include <string>
std::unique_ptr<std::string> makeGreeting(const std::string& name) {
return std::make_unique<std::string>("Hello, " + name + "!");
}
int main() {
std::unique_ptr<std::string> a = makeGreeting("Ada");
std::cout << *a << "\n";
std::unique_ptr<std::string> b = std::move(a);
std::cout << "b: " << *b << "\n";
std::cout << "a is now " << (a ? "not null" : "null") << "\n";
return 0;
}
Output:
Hello, Ada!
b: Hello, Ada!
a is now null
makeGreeting returns a unique_ptr by value; the compiler moves it out rather than copying, which is why returning a unique_ptr from a function is perfectly fine and cheap. std::move(a) then explicitly transfers ownership from a to b: after the move, b owns the string and a holds nullptr, which is why a converts to false in a boolean context.
Example 3: shared_ptr and reference counting
#include <iostream>
#include <memory>
#include <string>
class Logger {
public:
Logger() { std::cout << "Logger created\n"; }
~Logger() { std::cout << "Logger destroyed\n"; }
void log(const std::string& msg) const {
std::cout << "[LOG] " << msg << "\n";
}
};
void useLogger(std::shared_ptr<Logger> logger) {
logger->log("used inside function");
std::cout << "use_count inside function: " << logger.use_count() << "\n";
}
int main() {
std::shared_ptr<Logger> logger1 = std::make_shared<Logger>();
std::cout << "use_count after creation: " << logger1.use_count() << "\n";
{
std::shared_ptr<Logger> logger2 = logger1;
std::cout << "use_count after copy: " << logger1.use_count() << "\n";
useLogger(logger2);
}
std::cout << "use_count after inner scope ends: " << logger1.use_count() << "\n";
return 0;
}
Output:
Logger created
use_count after creation: 1
use_count after copy: 2
[LOG] used inside function
use_count inside function: 3
use_count after inner scope ends: 1
Logger destroyed
Every copy of logger1 bumps the strong reference count: copying into logger2 takes it to 2, and passing logger2 by value into useLogger takes it to 3. As each copy goes out of scope — first the function parameter, then logger2 at the end of the inner block — the count drops back down. The Logger object itself is only destroyed when the very last owner, logger1, disappears at the end of main.
Under the Hood
When you write std::make_shared<T>(args...), the library performs a single heap allocation that holds both the control block and the T object side by side. This is more efficient and more cache-friendly than std::shared_ptr<T>(new T(args...)), which allocates the object and the control block separately in two allocations. The one downside of make_shared is that the memory for T is not reclaimed until every weak_ptr referencing it is also gone, since object and control block share one allocation — a rare edge case worth knowing but usually not a practical problem.
A weak_ptr points at the same control block as a shared_ptr but only increments the weak count, never the strong one, so it never keeps the object alive by itself. To actually use the object, you call .lock(), which atomically checks whether the strong count is still greater than zero and, if so, returns a new shared_ptr (temporarily bumping the strong count while you hold it); otherwise it returns an empty shared_ptr. This is what makes weak_ptr safe for breaking reference cycles and for caches that should not prevent objects from being freed.
Common Mistakes
Mistake 1: Two unique_ptrs owning the same raw pointer
int* raw = new int(42);
std::unique_ptr<int> p1(raw);
std::unique_ptr<int> p2(raw); // BUG: p1 and p2 both think they own "raw"
// when p1 and p2 are destroyed, delete is called on the same address twice
Constructing two independent smart pointers from the same raw pointer means both will eventually call delete on it, which is undefined behavior (a double free, often a crash). The fix is to never let a raw pointer be adopted by more than one owning smart pointer, and to prefer make_unique so a raw pointer never exists in your code at all:
#include <iostream>
#include <memory>
int main() {
std::unique_ptr<int> p1 = std::make_unique<int>(42);
std::unique_ptr<int> p2 = std::move(p1); // ownership transferred, not duplicated
std::cout << *p2 << "\n";
return 0;
}
Output:
42
Mistake 2: Reference cycles with shared_ptr
class A {
public:
std::shared_ptr<B> bPtr;
};
class B {
public:
std::shared_ptr<A> aPtr; // BUG: A and B keep each other's count above zero forever
};
If A holds a shared_ptr to B and B holds a shared_ptr back to A, neither object’s strong count can ever reach zero, even after every external shared_ptr to them is gone — a genuine memory leak. The fix is to make one direction of the relationship a weak_ptr, typically the “back-pointer”:
#include <iostream>
#include <memory>
class B;
class A {
public:
std::shared_ptr<B> bPtr;
~A() { std::cout << "A destroyed\n"; }
};
class B {
public:
std::weak_ptr<A> aPtr;
~B() { std::cout << "B destroyed\n"; }
};
int main() {
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a->bPtr = b;
b->aPtr = a;
std::cout << "a use_count: " << a.use_count() << "\n";
std::cout << "b use_count: " << b.use_count() << "\n";
return 0;
}
Output:
a use_count: 1
b use_count: 2
A destroyed
B destroyed
Because b->aPtr is a weak_ptr, it does not add to A‘s strong count, so a‘s count stays at 1 and both objects are correctly destroyed once main ends.
Mistake 3: Storing the result of .get() past the smart pointer’s lifetime
std::string* dangling;
{
std::unique_ptr<std::string> temp = std::make_unique<std::string>("temporary");
dangling = temp.get(); // raw pointer copied out
} // temp is destroyed here, the string is freed
std::cout << *dangling; // BUG: dangling now points at freed memory
get() does not extend the object’s lifetime; it just hands you a peek at the address. Once the owning unique_ptr is destroyed, any raw pointer obtained from get() is dangling. Only use get() for short-lived interop (for example, passing a pointer into a C API call) and never store or return it for later use.
Best Practices
- Always create smart pointers with
std::make_uniqueorstd::make_sharedinstead of a barenew— it is exception-safe and, forshared_ptr, faster. - Default to
unique_ptr. Reach forshared_ptronly when you have a genuine, provable need for multiple simultaneous owners. - Pass smart pointers by reference (
const std::unique_ptr<T>&or just a rawT&/T*) when a function merely needs to use the object, and by value only when it should actually take or share ownership. - Use
weak_ptrto break ownership cycles and for observer-style references that should never keep an object alive. - Never manage the same raw pointer with two independently-constructed smart pointers.
- Treat
.get()as a short-lived escape hatch for legacy APIs only — never store its result beyond the smart pointer’s own scope. - Prefer passing raw pointers or references to functions that only observe an object; reserve smart-pointer parameters for functions that actually affect ownership.
Practice Exercises
- Write a function
createEmployeethat returns astd::unique_ptr<Employee>(a struct with astd::string nameand anintsalary) built withmake_unique, then call it frommain, print the employee’s name, and let theunique_ptrclean up automatically. - Define a simple singly linked list where each
Nodeowns the next node through astd::unique_ptr<Node> nextmember. Build a list of 3 nodes and write a loop that prints each value, relying on the destructor chain to free the whole list when the head goes out of scope. - Create two classes,
ParentandChild, whereParentholds astd::shared_ptr<Child>andChildholds astd::weak_ptr<Parent>. Print a message in each destructor and confirm both messages are printed when yourmainfunction ends, proving there is no leak.
Summary
std::unique_ptrgives exclusive, move-only ownership with essentially zero overhead over a raw pointer — use it as your default choice.std::shared_ptrgives shared ownership through an atomically reference-counted control block; the object is destroyed only when the last owner disappears.std::weak_ptrobserves ashared_ptr-managed object without affecting its reference count, and is the standard tool for breaking ownership cycles.- Always prefer
std::make_uniqueandstd::make_sharedover rawnewfor exception safety and, withshared_ptr, fewer allocations. - Never let two independent smart pointers manage the same raw pointer, and never keep a
.get()result alive past its owner’s lifetime.
