Lesson 129 min read

Smart Pointers

Automatic janitors for your memory β€” RAII means never saying 'I forgot to delete'

The Janitor Who Never Forgets

Imagine a conference room. People walk in, use the whiteboard, drink coffee. When the last person leaves, a janitor automatically locks the door and cleans up. Nobody has to remember to do it β€” it just happens.

That's RAII β€” Resource Acquisition Is Initialization. In C++, smart pointers are that janitor. They own a heap-allocated object, and when the smart pointer goes out of scope, it automatically deletes what it owns. No manual delete. No memory leaks. No double-frees.

Why Raw new/delete Is Dangerous

With raw pointers, you are the janitor. And humans forget.

  • Forget to delete? Memory leak.
  • Delete twice? Undefined behavior.
  • Exception thrown between new and delete? Leak.
  • Return early from a function? Leak.

Smart pointers solve all of these. Let's meet the three kinds.

unique_ptr β€” Exclusive Ownership

A std::unique_ptr is like a key to a private locker. Only one unique_ptr can own an object at a time. When it's destroyed, the object is deleted. You can't copy it β€” but you can move it, transferring ownership like handing the key to someone else.

unique_ptr β€” Creation and Move

#include <iostream>
#include <memory>
using namespace std;
int main() {
// Create with make_unique (preferred)
auto p1 = make_unique<int>(42);
cout << "p1 = " << *p1 << endl;
// Can't copy:
// auto p2 = p1; // ERROR: deleted copy constructor
// But can move:
auto p2 = move(p1);
cout << "p2 = " << *p2 << endl;
// p1 is now nullptr
if (!p1) cout << "p1 is null after move" << endl;
// Automatically deleted when p2 goes out of scope
return 0;
}
Output
p1 = 42
p2 = 42
p1 is null after move

shared_ptr β€” Shared Ownership

A std::shared_ptr is like a shared Netflix account. Multiple shared_ptrs can point to the same object. Internally, it keeps a reference count. When the last shared_ptr is destroyed, the object is deleted β€” just like the janitor cleaning up when the last person leaves.

shared_ptr with Reference Counting

#include <iostream>
#include <memory>
using namespace std;
int main() {
auto sp1 = make_shared<string>("Hello, RAII!");
cout << "sp1: " << *sp1 << endl;
cout << "count: " << sp1.use_count() << endl;
{
auto sp2 = sp1; // copy β€” both own the string
cout << "count after sp2: " << sp1.use_count() << endl;
auto sp3 = sp1; // another copy
cout << "count after sp3: " << sp1.use_count() << endl;
} // sp2 and sp3 destroyed here
cout << "count after block: " << sp1.use_count() << endl;
// String deleted when sp1 goes out of scope
return 0;
}
Output
sp1: Hello, RAII!
count: 1
count after sp2: 2
count after sp3: 3
count after block: 1

weak_ptr β€” The Observer

A std::weak_ptr is like a spectator with binoculars β€” it can see the object a shared_ptr owns, but it doesn't keep it alive. This breaks circular references (where two shared_ptrs point to each other, and neither ever reaches a count of zero).

To use the object, you must call .lock() which gives you a temporary shared_ptr β€” if the object still exists.

Converting unique_ptr to shared_ptr

#include <iostream>
#include <memory>
using namespace std;
int main() {
// Start with exclusive ownership
auto uniq = make_unique<int>(100);
// Transfer to shared ownership (one-way trip!)
shared_ptr<int> shared = move(uniq);
cout << "shared: " << *shared << endl;
cout << "uniq is null: " << (uniq == nullptr) << endl;
// You CANNOT go from shared_ptr back to unique_ptr
// That would violate exclusive ownership!
return 0;
}
Output
shared: 100
uniq is null: 1

Custom Deleter

#include <iostream>
#include <memory>
using namespace std;
struct Connection {
string name;
Connection(string n) : name(n) {
cout << "Opening " << name << endl;
}
};
void closeConnection(Connection* c) {
cout << "Closing " << c->name << endl;
delete c;
}
int main() {
// Custom deleter: runs closeConnection instead of plain delete
unique_ptr<Connection, decltype(&closeConnection)>
conn(new Connection("DB-1"), closeConnection);
cout << "Using " << conn->name << endl;
// closeConnection called automatically at scope exit
return 0;
}
Output
Opening DB-1
Using DB-1
Closing DB-1

Factory Function Returning unique_ptr

#include <iostream>
#include <memory>
#include <string>
using namespace std;
class Shape {
public:
virtual string name() const = 0;
virtual ~Shape() = default;
};
class Circle : public Shape {
public:
string name() const override { return "Circle"; }
};
class Square : public Shape {
public:
string name() const override { return "Square"; }
};
// Factory: caller gets ownership via unique_ptr
unique_ptr<Shape> makeShape(const string& type) {
if (type == "circle") return make_unique<Circle>();
if (type == "square") return make_unique<Square>();
return nullptr;
}
int main() {
auto s1 = makeShape("circle");
auto s2 = makeShape("square");
if (s1) cout << s1->name() << endl;
if (s2) cout << s2->name() << endl;
// Both automatically cleaned up
return 0;
}
Output
Circle
Square

Quick Reference

Smart PointerOwnershipCopyable?Use When
unique_ptrExclusive (1 owner)No (move only)Default choice. Single owner.
shared_ptrShared (N owners)YesMultiple parts of code share one resource
weak_ptrObserver (0 owners)YesBreaking cycles, caches, optional access
Note: NEVER use raw new/delete in modern C++. Use make_unique and make_shared β€” they're exception-safe and cleaner. If you're writing 'new', you're probably doing it wrong. The only exception is when you need a custom deleter with unique_ptr, where you must use 'new' directly.
Challenge

Quick check

What happens when you try to copy a std::unique_ptr?
← Pointers & ReferencesSTL Containers β†’