Smart Pointers
Smart pointers are RAII wrappers around raw pointers that automatically free memory when it is no longer needed. They were introduced in C++11 and are now the standard way to manage heap-allocated memory. The goal is never write delete in application code — let smart pointers do it.
unique_ptr — Exclusive Ownership
unique_ptr owns the object exclusively — only one unique_ptr can point to it at a time. When the unique_ptr goes out of scope, the object is automatically deleted.
#include <iostream>
#include <memory>
#include <string>
using namespace std;
class Dog {
public:
string name;
Dog(const string& n) : name(n) { cout << "Dog " << name << " created\n"; }
~Dog() { cout << "Dog " << name << " destroyed\n"; }
void bark() const { cout << name << ": Woof!\n"; }
};
int main() {
// Create — prefer make_unique (C++14) over new
unique_ptr<Dog> dog = make_unique<Dog>("Rex");
dog->bark(); // Rex: Woof!
cout << dog->name << "\n"; // Rex
// Transfer ownership — unique_ptr is move-only
unique_ptr<Dog> dog2 = move(dog);
// dog is now nullptr — it no longer owns anything
if (!dog) cout << "dog is null after move\n";
dog2->bark(); // Rex: Woof!
// dog2 goes out of scope here — Rex is destroyed automatically
return 0;
}
// Dog Rex created
// Rex: Woof!
// Rex
// dog is null after move
// Rex: Woof!
// Dog Rex destroyedmake_unique vs new
Always use make_unique:
// Prefer (C++14):
auto p1 = make_unique<int>(42);
auto p2 = make_unique<string>("hello");
auto p3 = make_unique<Dog>("Buddy");
// Avoid — less safe, more verbose:
unique_ptr<Dog> p4(new Dog("Charlie"));
// WHY make_unique is safer:
// In f(unique_ptr<T>(new T()), g()), if g() throws before the unique_ptr
// is constructed, 'new T()' leaks. make_unique eliminates this window.unique_ptr with Arrays
#include <memory>
#include <iostream>
using namespace std;
int main() {
// Array version — uses delete[] automatically
auto arr = make_unique<int[]>(5);
for (int i = 0; i < 5; i++) arr[i] = i * 10;
for (int i = 0; i < 5; i++) cout << arr[i] << " ";
cout << "\n"; // 0 10 20 30 40
// arr is freed here — no delete[] needed
return 0;
}shared_ptr — Shared Ownership
shared_ptr uses reference counting — the object is destroyed when the last shared_ptr pointing to it goes out of scope:
#include <iostream>
#include <memory>
#include <string>
using namespace std;
class Widget {
public:
string name;
Widget(const string& n) : name(n) { cout << "Widget " << name << " created\n"; }
~Widget() { cout << "Widget " << name << " destroyed\n"; }
};
void processWidget(shared_ptr<Widget> w) {
cout << "Processing " << w->name << " (ref count: " << w.use_count() << ")\n";
}
int main() {
shared_ptr<Widget> w1 = make_shared<Widget>("Gadget");
cout << "Count: " << w1.use_count() << "\n"; // 1
{
shared_ptr<Widget> w2 = w1; // Both now own the Widget
cout << "Count: " << w1.use_count() << "\n"; // 2
processWidget(w1); // Count inside: 3 (function param copy)
cout << "Count: " << w1.use_count() << "\n"; // 2 (function returned)
}
// w2 destroyed — count drops to 1
cout << "Count: " << w1.use_count() << "\n"; // 1
// w1 destroyed — count drops to 0 — Widget destroyed
return 0;
}
// Widget Gadget created
// Count: 1
// Count: 2
// Processing Gadget (ref count: 3)
// Count: 2
// Count: 1
// Widget Gadget destroyedmake_shared — The Right Way to Create shared_ptr
// Prefer (single allocation — object + ref count together):
auto sp = make_shared<Dog>("Buddy");
// Avoid (two separate allocations):
shared_ptr<Dog> sp2(new Dog("Buddy"));make_shared is both faster and safer — it allocates the control block and the object in one contiguous block.
weak_ptr — Breaking Cyclic References
weak_ptr holds a non-owning reference to an object managed by shared_ptr. It does not affect the reference count — used to break cycles that would cause memory leaks:
#include <iostream>
#include <memory>
#include <string>
using namespace std;
struct Node {
string name;
shared_ptr<Node> next; // Strong reference
weak_ptr<Node> prev; // Weak reference — avoids cycle
Node(const string& n) : name(n) { cout << "Node " << n << " created\n"; }
~Node() { cout << "Node " << name << " destroyed\n"; }
};
int main() {
auto a = make_shared<Node>("A");
auto b = make_shared<Node>("B");
a->next = b; // A holds strong ref to B
b->prev = a; // B holds WEAK ref to A (no cycle!)
// Use weak_ptr — must lock() to get a shared_ptr
if (auto locked = b->prev.lock()) {
cout << "B's prev: " << locked->name << "\n"; // A
}
// When a and b go out of scope:
// a.use_count() == 1 (no b->prev strong ref), so a is destroyed
// b.use_count() == 1 (only a->next holds it), so b is destroyed too
return 0;
}
// Node A created
// Node B created
// B's prev: A
// Node A destroyed
// Node B destroyedWhen to Use Which
| Smart Pointer | Use When |
|---|---|
unique_ptr | Sole ownership — default choice for heap objects |
shared_ptr | Shared ownership — multiple owners, lifetime unclear |
weak_ptr | Observer/cache — access but not own; break cycles |
| Raw pointer | Non-owning reference to stack or unique_ptr-managed object |
Note: A function that does not own an object should take a raw pointer or reference — not shared_ptr. Passing shared_ptr by value increases the ref count and communicates shared ownership intent.
Custom Deleters
#include <memory>
#include <cstdio>
using namespace std;
int main() {
// unique_ptr with custom deleter for FILE*
auto fileDeleter = [](FILE* f) {
if (f) { fclose(f); printf("File closed\n"); }
};
unique_ptr<FILE, decltype(fileDeleter)> file(
fopen("test.txt", "w"),
fileDeleter
);
if (file) {
fputs("Hello from smart pointer!", file.get());
}
// File closed when unique_ptr goes out of scope
return 0;
}The Rule of Zero
With smart pointers, most classes need no custom destructor, copy constructor, or copy assignment:
#include <memory>
#include <vector>
#include <string>
using namespace std;
// Rule of Zero — let member types manage their own resources
class UserProfile {
string username;
vector<string> tags;
shared_ptr<Avatar> avatar; // shared_ptr manages lifetime
unique_ptr<Session> session; // unique_ptr manages lifetime
public:
UserProfile(const string& name) : username(name) {}
// No destructor, no copy constructor, no assignment needed!
// Default-generated functions do the right thing.
};Practical Example: Resource Manager
#include <iostream>
#include <memory>
#include <vector>
#include <string>
using namespace std;
class Texture {
public:
string name;
size_t width, height;
Texture(const string& n, size_t w, size_t h)
: name(n), width(w), height(h) {
cout << "Loaded texture: " << name << " (" << w << "x" << h << ")\n";
}
~Texture() { cout << "Unloaded texture: " << name << "\n"; }
};
class ResourceManager {
vector<shared_ptr<Texture>> cache;
public:
shared_ptr<Texture> loadTexture(const string& name, size_t w, size_t h) {
// Check cache first
for (auto& t : cache) {
if (t->name == name) {
cout << "Cache hit: " << name << "\n";
return t;
}
}
// Load and cache
auto t = make_shared<Texture>(name, w, h);
cache.push_back(t);
return t;
}
void printStats() const {
cout << "Cached textures: " << cache.size() << "\n";
for (const auto& t : cache) {
cout << " " << t->name << " (refs: " << t.use_count() << ")\n";
}
}
void clearCache() {
cache.clear(); // Releases all shared_ptrs — textures freed if no other owners
}
};
int main() {
ResourceManager rm;
shared_ptr<Texture> bg = rm.loadTexture("background.png", 1920, 1080);
shared_ptr<Texture> logo = rm.loadTexture("logo.png", 256, 256);
shared_ptr<Texture> bg2 = rm.loadTexture("background.png", 0, 0); // Cache hit
cout << (bg.get() == bg2.get() ? "Same object (cached)" : "Different") << "\n";
rm.printStats();
rm.clearCache();
cout << "After clearCache, bg refs: " << bg.use_count() << "\n"; // 2 (bg + bg2)
return 0;
}Summary
unique_ptr— exclusive ownership, zero overhead over raw pointer; prefer for most heap objectsshared_ptr— reference-counted shared ownership; use when multiple owners need to outlive each otherweak_ptr— non-owning observer; use to break cycles inshared_ptrgraphs- Always use
make_uniqueandmake_sharedinstead ofnew— safer and often faster - Pass
unique_ptrby move to transfer ownership; pass by raw pointer or reference to share access without ownership - The Rule of Zero: design classes so that default-generated special functions work correctly — smart pointers and standard containers make this easy
- Custom deleters allow smart pointers to manage any resource — file handles, database connections, GPU textures
Next, we'll explore move semantics — how C++11 eliminates unnecessary copies with rvalue references and std::move.