Smart Pointers — unique_ptr, shared_ptr, weak_ptr, make_unique, make_shared
In this tutorial, you will learn about Smart Pointers. We cover key concepts, practical examples, and best practices to help you master this topic.
C++ smart pointers automate dynamic memory management with unique_ptr for exclusive ownership, shared_ptr for reference-counted sharing, and weak_ptr for non-owning observation that avoids circular references.
What You'll Learn
You will manage heap-allocated objects using std::unique_ptr (exclusive ownership, zero overhead), std::shared_ptr (reference-counted shared ownership with control block), and std::weak_ptr (non-owning Observer that breaks cycles). You will use std::make_unique and std::make_shared for exception-safe creation, and understand when each smart pointer type is appropriate.
Why It Matters
Smart pointers are the cornerstone of modern C++ resource management. They eliminate manual new/delete and implement RAII for heap memory. unique_ptr has zero overhead over a raw pointer, making it suitable for all ownership contexts. shared_ptr adds reference counting for shared ownership scenarios. Learning these tools virtually eliminates memory leaks and double-free bugs from your code.
Learning Path
graph LR
A["23: Dynamic Memory"] --> B["24: Smart Pointers"]
B --> C["25: Custom Deleters"]
C --> D["26: Allocators"]
style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
style D fill:#4a90d9,stroke:#2c5f8a,color:#fff
std::unique_ptr
Exclusive ownership: only one unique_ptr can own a resource at a time. Cannot be copied, but can be moved.
#include <iostream>
#include <memory>
#include <vector>
class Resource {
public:
Resource() { std::cout << "Resource acquired\n"; }
~Resource() { std::cout << "Resource released\n"; }
void work() const { std::cout << "Working\n"; }
};
int main() {
// Create with make_unique (C++14)
std::unique_ptr<Resource> ptr = std::make_unique<Resource>();
ptr->work();
// Cannot copy unique_ptr
// auto ptr2 = ptr; // Error: copy deleted
// Must move to transfer ownership
std::unique_ptr<Resource> ptr2 = std::move(ptr);
if (!ptr) {
std::cout << "ptr is now null\n";
}
ptr2->work();
// Vector of unique_ptrs
std::vector<std::unique_ptr<int>> numbers;
numbers.push_back(std::make_unique<int>(10));
numbers.push_back(std::make_unique<int>(20));
for (const auto& num : numbers) {
std::cout << *num << "\n";
}
// Raw pointer access (non-owning)
Resource* raw = ptr2.get();
raw->work();
// Release ownership without destroying
Resource* released = ptr2.release();
delete released; // must delete manually
}
unique_ptr is the default smart pointer. It has no overhead compared to a raw pointer and integrates perfectly with standard containers.
std::shared_ptr
Shared ownership via reference counting. The resource is destroyed when the last shared_ptr is destroyed.
#include <iostream>
#include <memory>
struct SharedResource {
int id;
SharedResource(int i) : id(i) { std::cout << "SharedResource " << id << " created\n"; }
~SharedResource() { std::cout << "SharedResource " << id << " destroyed\n"; }
};
int main() {
std::shared_ptr<SharedResource> sp1 = std::make_shared<SharedResource>(1);
{
std::shared_ptr<SharedResource> sp2 = sp1;
std::cout << "Use count: " << sp1.use_count() << "\n"; // 2
std::shared_ptr<SharedResource> sp3 = sp1;
std::cout << "Use count: " << sp1.use_count() << "\n"; // 3
}
// sp2 and sp3 destroyed, count back to 1
std::cout << "Use count: " << sp1.use_count() << "\n"; // 1
// Resource destroyed when sp1 goes out of scope
}
The control block holds the reference count and the deleter. make_shared allocates the object and control block in a single memory allocation, which is more efficient than separate new and control block allocation.
std::weak_ptr
Non-owning observer that does not affect the reference count. Used to break circular references.
#include <iostream>
#include <memory>
struct Node {
int value;
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // weak to avoid cycle
~Node() { std::cout << "Node " << value << " destroyed\n"; }
};
int main() {
auto n1 = std::make_shared<Node>();
auto n2 = std::make_shared<Node>();
n1->value = 1;
n2->value = 2;
// Circular reference using weak_ptr
n1->next = n2;
n2->prev = n1;
// Use weak_ptr: must lock() to get shared_ptr
if (auto shared = n2->prev.lock()) {
std::cout << "Previous: " << shared->value << "\n"; // 1
}
std::cout << "n1 use count: " << n1.use_count() << "\n"; // 1 (only n1 itself)
// n1 and n2 are destroyed properly when they go out of scope
}
Without weak_ptr (using shared_ptr for both directions), the nodes would never be destroyed because each holds the other alive.
std::make_unique and std::make_shared
#include <memory>
#include <vector>
struct Widget {
int x;
double y;
Widget(int a, double b) : x(a), y(b) {}
};
int main() {
// make_unique (C++14)
auto uptr = std::make_unique<int>(42);
auto w1 = std::make_unique<Widget>(10, 3.14);
// make_shared
auto sptr = std::make_shared<int>(99);
auto w2 = std::make_shared<Widget>(20, 2.71);
// make_unique for arrays (C++17)
auto arr = std::make_unique<int[]>(10);
arr[0] = 5;
// make_shared for arrays (C++20)
auto sarr = std::make_shared<int[]>(10);
sarr[0] = 5;
}
Always prefer make_unique and make_shared over using new:
- Exception safety: no gap between allocation and smart pointer construction
- Fewer memory allocations:
make_sharedcombines object and control block - Cleaner syntax: type is only written once
- No naked
newin user code
Converting Between Smart Pointer Types
#include <memory>
int main() {
// unique_ptr -> shared_ptr (implicit conversion)
auto up = std::make_unique<int>(42);
std::shared_ptr<int> sp = std::move(up);
// shared_ptr -> unique_ptr (NOT possible directly)
// auto up2 = std::unique_ptr<int>(sp.get()); // dangerous!
// weak_ptr -> shared_ptr via lock()
std::weak_ptr<int> wp = sp;
if (auto locked = wp.lock()) {
// use locked
}
}
Custom Deleter with Smart Pointers
#include <iostream>
#include <memory>
#include <cstdio>
struct FileCloser {
void operator()(std::FILE* fp) const {
if (fp) {
std::fclose(fp);
std::cout << "File closed\n";
}
}
};
int main() {
std::unique_ptr<std::FILE, FileCloser> file(std::fopen("test.txt", "w"));
// With lambda
auto deleter = [](std::FILE* f) {
if (f) std::fclose(f);
};
std::unique_ptr<std::FILE, decltype(deleter)> file2(std::fopen("test2.txt", "w"), deleter);
}
Smart Pointer Comparison
| Feature | unique_ptr | shared_ptr | weak_ptr |
|---|---|---|---|
| Ownership | Exclusive | Shared | None |
| Reference count | No | Yes (control block) | No |
| Overhead | None | Control block + atomic ops | Control block access |
| Copyable | No (move only) | Yes | Yes |
| Thread-safe ref count | N/A | Yes | Yes |
| Use case | Local ownership, containers | Shared lifetime, caches | Breaking cycles, Caching |
Common Mistakes
Mistake 1: Using shared_ptr When unique_ptr Suffices
std::shared_ptr<Widget> ptr = std::make_shared<Widget>(); // unnecessary overhead
Use unique_ptr by default. Only use shared_ptr when ownership is truly shared.
Mistake 2: Circular shared_ptr References
struct A { std::shared_ptr<B> b; };
struct B { std::shared_ptr<A> a; };
Use weak_ptr in one direction to break the cycle.
Mistake 3: Getting a Raw Pointer and Deleting It
auto sp = std::make_shared<int>(42);
int* raw = sp.get();
delete raw; // undefined behavior: double free
Mistake 4: Creating shared_ptr from Raw Pointer Multiple Times
int* raw = new int(42);
std::shared_ptr<int> sp1(raw);
std::shared_ptr<int> sp2(raw); // two independent control blocks, double delete!
Mistake 5: Using .get() to Store in a Container
std::vector<int*> vec;
auto sp = std::make_shared<int>(42);
vec.push_back(sp.get()); // dangerous: sp may be destroyed before vector
Mistake 6: Assuming weak_ptr::lock() Always Succeeds
Always check the result of lock(): it returns an empty shared_ptr if the object has been deleted.
Practice Questions
- When should you use
unique_ptrversusshared_ptr? - Why does
make_shareduse a single allocation? - What problem does
weak_ptrsolve? - Can you store
unique_ptrin astd::vector? How? - Write a function that creates a
unique_ptr<int>and transfers ownership to the caller.
Challenge
Implement a simplified shared_ptr that maintains a reference count in a separate control block. Include construction, copy, move, destruction, and operator->. Test that the object is destroyed when the last copy goes out of scope.
FAQ
Mini Project
Build a simple observer registry using shared_ptr and weak_ptr:
#include <iostream>
#include <memory>
#include <vector>
#include <algorithm>
class Observer {
public:
virtual ~Observer() = default;
virtual void notify(int event) = 0;
};
class Subject {
private:
std::vector<std::weak_ptr<Observer>> observers_;
public:
void addObserver(std::shared_ptr<Observer> obs) {
observers_.push_back(obs);
}
void notifyAll(int event) {
// Remove expired observers and notify alive ones
observers_.erase(
std::remove_if(observers_.begin(), observers_.end(),
[event](const std::weak_ptr<Observer>& wp) {
if (auto sp = wp.lock()) {
sp->notify(event);
return false;
}
return true;
}),
observers_.end()
);
}
};
class ConcreteObserver : public Observer {
private:
int id_;
public:
ConcreteObserver(int id) : id_(id) {}
void notify(int event) override {
std::cout << "Observer " << id_ << " received event " << event << "\n";
}
};
int main() {
Subject subject;
auto obs1 = std::make_shared<ConcreteObserver>(1);
auto obs2 = std::make_shared<ConcreteObserver>(2);
subject.addObserver(obs1);
subject.addObserver(obs2);
subject.notifyAll(100);
obs2.reset(); // observer 2 is destroyed
subject.notifyAll(200); // only observer 1 is notified
}
What's Next
Smart pointers handle ownership. The next lesson covers custom deleters for smart pointers: function objects, lambda deleters, and resource handles for non-memory resources like files and sockets.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro