C++ Smart Pointers: unique_ptr, shared_ptr, weak_ptr & RAII Memory Complete Masterclass
Welcome to Phase 17 (Chapter 17): C++ Smart Pointers & Memory Management Masterclass! Modern C++ eliminates manual new/delete through RAII-based smart pointers. std::unique_ptr for exclusive ownership, std::shared_ptr for shared reference-counted ownership, and std::weak_ptr for non-owning observation. Together they make memory-safe, leak-free C++ achievable without a garbage collector.
#include <iostream>
#include <stdexcept>
// โโโ Memory leak example โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
void memoryLeakExample() {
int* p = new int(42);
// If we throw or return early โ leak! delete never called
if (true) return; // <-- LEAK: p is lost!
delete p;
}
// โโโ Dangling pointer example โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
int* danglingPointer() {
int local = 42;
return &local; // DANGER: local is destroyed when function returns!
}
// โโโ Double deletion example โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
void doubleDeletion() {
int* p = new int(99);
delete p;
// delete p; // CRASH: undefined behaviour!
p = nullptr; // Good practice: null after delete
}
// โโโ Exception-unsafe raw new โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
void processData() {
int* data = new int[1000];
// ... some code that might throw ...
// throw std::runtime_error("error!"); // <-- LEAK: data never deleted!
delete[] data; // not reached if exception thrown
}
// โโโ The RAII solution: smart pointers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// (covered in sections below)
unique_ptr Contract:
โข Exactly ONE unique_ptr owns the resource at any time. Cannot be copied โ only moved.
โข Resource is automatically destroyed when the unique_ptr goes out of scope (RAII).
โข Always create with std::make_unique<T>(args) โ exception-safe and avoids raw new.
โข Zero overhead โ same size and cost as a raw pointer at runtime.
#include <iostream>
#include <memory>
#include <string>
#include <vector>
class File {
std::string path_;
bool open_;
public:
explicit File(std::string path) : path_{std::move(path)}, open_{true} {
std::cout << "Opened: " << path_ << "
";
}
~File() {
if (open_) std::cout << "Closed: " << path_ << "
";
}
void write(const std::string& data) {
if (!open_) throw std::runtime_error("File not open!");
std::cout << path_ << " << " << data << "
";
}
void close() { open_ = false; std::cout << "Manually closed: " << path_ << "
"; }
};
// Factory function returning unique_ptr
std::unique_ptr<File> openFile(const std::string& path) {
return std::make_unique<File>(path); // RAII from the start
}
// Function that takes ownership (sink)
void processFile(std::unique_ptr<File> file) {
file->write("Processing data...");
} // file destroyed here automatically
// Function that borrows (non-owning reference)
void readFromFile(const File& file) {
std::cout << "Reading from file
";
}
// Function that uses (non-owning raw pointer)
void updateFile(File* file) {
if (file) file->write("Updated!");
}
int main() {
// Create with make_unique (ALWAYS prefer this!)
auto f1 = std::make_unique<File>("data.txt");
f1->write("Hello World");
// Borrow without transferring ownership
readFromFile(*f1); // pass by reference
updateFile(f1.get()); // get() returns raw pointer โ non-owning!
// Transfer ownership (move semantics)
auto f2 = std::move(f1); // f2 now owns the File
if (!f1) std::cout << "f1 is now null after move
";
f2->write("Written via f2");
// Sink function โ takes ownership, destroys at end of function
processFile(std::move(f2));
if (!f2) std::cout << "f2 is null after move-to-sink
";
// Factory function
auto logFile = openFile("server.log");
logFile->write("Server started");
// unique_ptr to array
auto buffer = std::make_unique<char[]>(1024);
buffer[0] = 'H'; buffer[1] = 'i'; buffer[2] = ' ';
std::cout << "buffer: " << buffer.get() << "
";
// Vector of unique_ptrs (polymorphic collection)
std::vector<std::unique_ptr<File>> filePool;
for (const std::string& name : {"a.txt", "b.txt", "c.txt"}) {
filePool.push_back(std::make_unique<File>(name));
}
for (auto& f : filePool) f->write("batch write");
return 0;
} // All files automatically closed! โ
#include <iostream>
#include <memory>
#include <vector>
#include <string>
class Database {
std::string name_;
int queryCount_{0};
public:
explicit Database(std::string name) : name_{std::move(name)} {
std::cout << "DB '" << name_ << "' connected
";
}
~Database() { std::cout << "DB '" << name_ << "' disconnected
"; }
void query(const std::string& sql) {
++queryCount_;
std::cout << name_ << " query #" << queryCount_ << ": " << sql << "
";
}
int queryCount() const { return queryCount_; }
};
class UserService {
std::shared_ptr<Database> db_; // shared ownership
public:
explicit UserService(std::shared_ptr<Database> db) : db_{std::move(db)} {}
void getUser(int id) { db_->query("SELECT * FROM users WHERE id=" + std::to_string(id)); }
};
class OrderService {
std::shared_ptr<Database> db_;
public:
explicit OrderService(std::shared_ptr<Database> db) : db_{std::move(db)} {}
void getOrders(int userId) { db_->query("SELECT * FROM orders WHERE user_id=" + std::to_string(userId)); }
};
int main() {
// Shared database connection
auto db = std::make_shared<Database>("PostgreSQL");
std::cout << "use_count after creation: " << db.use_count() << "
"; // 1
{
UserService userSvc{db}; // db shared with UserService
OrderService orderSvc{db}; // db shared with OrderService
std::cout << "use_count with 2 services: " << db.use_count() << "
"; // 3
userSvc.getUser(42);
orderSvc.getOrders(42);
// Copy shared_ptr โ increases ref count
auto db2 = db;
auto db3 = db;
std::cout << "use_count with copies: " << db.use_count() << "
"; // 5
db2.reset(); // release one owner
std::cout << "after db2.reset: " << db.use_count() << "
"; // 4
} // userSvc, orderSvc, db3 destroyed โ ref count decreases
std::cout << "use_count after scope: " << db.use_count() << "
"; // 1
// Aliasing constructor โ shared_ptr to a member of an object
struct Config { int timeout = 30; std::string host = "localhost"; };
auto config = std::make_shared<Config>();
// shared_ptr to the host member โ shares ownership of the whole Config!
std::shared_ptr<std::string> hostPtr(config, &config->host);
std::cout << "host: " << *hostPtr << " config use_count: " << config.use_count() << "
";
config.reset(); // Config survives because hostPtr still holds it!
std::cout << "host after config.reset: " << *hostPtr << "
";
return 0;
} // DB disconnected here (use_count hits 0) โ
#include <iostream>
#include <memory>
#include <string>
#include <vector>
// โโโ Cycle with shared_ptr (MEMORY LEAK!) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
struct BadNode {
int val;
std::shared_ptr<BadNode> next; // strong reference
std::shared_ptr<BadNode> prev; // strong reference โ CYCLE!
explicit BadNode(int v) : val{v} { std::cout << "BadNode " << v << " created
"; }
~BadNode() { std::cout << "BadNode " << val << " destroyed
"; }
};
// โโโ Fix with weak_ptr (NO LEAK!) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
struct GoodNode {
int val;
std::shared_ptr<GoodNode> next; // strong โ keeps next alive
std::weak_ptr<GoodNode> prev; // weak โ doesn't prevent destruction!
explicit GoodNode(int v) : val{v} { std::cout << "GoodNode " << v << " created
"; }
~GoodNode() { std::cout << "GoodNode " << val << " destroyed
"; }
};
// โโโ Observer pattern with weak_ptr โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
class EventEmitter;
class EventListener {
std::string name_;
public:
explicit EventListener(std::string name) : name_{std::move(name)} {}
void onEvent(const std::string& event) {
std::cout << name_ << " received: " << event << "
";
}
~EventListener() { std::cout << name_ << " destroyed
"; }
};
class EventEmitter {
std::vector<std::weak_ptr<EventListener>> listeners_;
public:
void subscribe(std::weak_ptr<EventListener> listener) {
listeners_.push_back(std::move(listener));
}
void emit(const std::string& event) {
// Use lock() to safely access the listener
auto it = listeners_.begin();
while (it != listeners_.end()) {
if (auto listener = it->lock()) { // still alive?
listener->onEvent(event);
++it;
} else {
std::cout << "(removing dead listener)
";
it = listeners_.erase(it); // auto-remove dead listeners!
}
}
}
};
int main() {
// CYCLE DEMO โ leak
std::cout << "=== Cycle with shared_ptr (LEAK) ===
";
{
auto n1 = std::make_shared<BadNode>(1);
auto n2 = std::make_shared<BadNode>(2);
n1->next = n2; // n1 holds n2
n2->prev = n1; // n2 holds n1 โ CYCLE
// n1 and n2 use_count = 2 each โ never reaches 0!
} // LEAK โ destructors never called!
std::cout << "(should have seen 'destroyed' โ but didn't!)
";
// FIX โ no leak
std::cout << "=== Fix with weak_ptr (NO LEAK) ===
";
{
auto n1 = std::make_shared<GoodNode>(1);
auto n2 = std::make_shared<GoodNode>(2);
n1->next = n2; // strong: n1โn2
n2->prev = n1; // weak: n2 observes n1 (doesn't prevent destruction)
} // n1 destroyed (use_count 1โ0), then n2 โ
// Observer pattern
std::cout << "
=== Observer Pattern ===
";
EventEmitter emitter;
auto l1 = std::make_shared<EventListener>("Logger");
auto l2 = std::make_shared<EventListener>("Analytics");
emitter.subscribe(l1);
emitter.subscribe(l2);
emitter.emit("user_login");
l2.reset(); // unsubscribe by destroying listener
std::cout << "After l2 reset:
";
emitter.emit("user_logout"); // Analytics auto-removed!
return 0;
}
#include <iostream>
#include <memory>
#include <cstdio>
#include <functional>
// โโโ RAII file handle with custom deleter โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
struct FileDeleter {
void operator()(FILE* f) const {
if (f) { std::fclose(f); std::cout << "FILE closed by custom deleter
"; }
}
};
using FileHandle = std::unique_ptr<FILE, FileDeleter>;
FileHandle openRawFile(const char* path, const char* mode) {
FILE* f = std::fopen(path, mode);
if (!f) throw std::runtime_error(std::string("Cannot open: ") + path);
return FileHandle{f};
}
// โโโ Lambda deleter โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
auto makeBuffer(std::size_t size) {
return std::unique_ptr<char[], std::function<void(char*)>>(
new char[size],
[size](char* p) {
std::cout << "Freeing buffer of " << size << " bytes
";
delete[] p;
}
);
}
// โโโ Passing smart pointers โ guideline table โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
void borrowObject(const std::string& s) { // just borrows โ raw ref
std::cout << "borrowing: " << s << "
";
}
void sinkObject(std::unique_ptr<std::string> s) { // takes ownership
std::cout << "sinking: " << *s << "
";
} // destroyed here
void sharedAccess(std::shared_ptr<std::string> s) { // shares ownership
std::cout << "shared: " << *s << " refcount=" << s.use_count() << "
";
}
void weakAccess(std::weak_ptr<std::string> w) { // optional access
if (auto p = w.lock()) std::cout << "weak: " << *p << "
";
else std::cout << "object expired!
";
}
int main() {
// Custom deleter for FILE
try {
auto f = openRawFile("test_output.txt", "w");
std::fputs("Hello from smart FILE!
", f.get());
// f automatically closed when scope ends
} catch (const std::exception& e) {
std::cout << "File error (OK if no permission): " << e.what() << "
";
}
// Lambda deleter
auto buf = makeBuffer(256);
buf[0] = 'A'; buf[1] = ' ';
std::cout << "buf[0] = " << buf[0] << "
";
// Passing patterns
auto up = std::make_unique<std::string>("Hello");
borrowObject(*up); // borrow by reference
sinkObject(std::move(up)); // transfer ownership (up becomes null)
if (!up) std::cout << "up is null after sink
";
auto sp = std::make_shared<std::string>("World");
sharedAccess(sp); // share (ref count increases temporarily)
weakAccess(sp); // weak access while alive
sp.reset();
weakAccess(std::weak_ptr<std::string>{}); // expired!
return 0;
}
#include <iostream>
#include <memory>
#include <vector>
#include <string>
// Rule of Zero: if you use smart pointers and STL containers to manage
// all resources, you don't need to write ANY of the 5 special members!
class SmartEmployee {
std::string name_;
std::vector<std::string> skills_;
std::unique_ptr<std::string> biography_; // unique resource
public:
SmartEmployee(std::string name, std::string bio)
: name_{std::move(name)}, biography_{std::make_unique<std::string>(std::move(bio))} {}
void addSkill(std::string skill) { skills_.push_back(std::move(skill)); }
void print() const {
std::cout << "Employee: " << name_ << "
";
std::cout << "Bio: " << *biography_ << "
";
std::cout << "Skills: ";
for (const auto& s : skills_) std::cout << s << " ";
std::cout << "
";
}
// No need to write: destructor, copy/move constructors, copy/move assignment!
// unique_ptr automatically makes this class move-only (non-copyable)
};
// enable_shared_from_this โ safe shared_ptr from within the object
class Worker : public std::enable_shared_from_this<Worker> {
std::string task_;
public:
explicit Worker(std::string task) : task_{std::move(task)} {}
std::shared_ptr<Worker> getSelf() {
return shared_from_this(); // safe โ returns shared_ptr to this
// return std::shared_ptr<Worker>(this); // WRONG โ creates separate ownership!
}
void run() { std::cout << "Running task: " << task_ << "
"; }
~Worker() { std::cout << "Worker '" << task_ << "' done
"; }
};
int main() {
// Rule of Zero demo
SmartEmployee emp{"Alice", "Senior developer with 10 years experience"};
emp.addSkill("C++20");
emp.addSkill("RAII");
emp.addSkill("Templates");
emp.print();
// enable_shared_from_this
auto w1 = std::make_shared<Worker>("compile");
auto w2 = w1->getSelf(); // both point to same Worker
w1->run();
std::cout << "Same object: " << (w1.get() == w2.get()) << "
"; // true
std::cout << "use_count: " << w1.use_count() << "
"; // 2
return 0;
}
Q1: Why make_unique/make_shared instead of new?
Exception safety: f(unique_ptr<T>(new T), g()) could leak if g() throws between the new and smart pointer construction (pre-C++17). make_unique<T>() is atomic โ no such risk. Also cleaner and avoids repeating the type.
Q2: What is the overhead of shared_ptr vs unique_ptr?
unique_ptr has zero overhead โ same as raw pointer. shared_ptr carries a second pointer (control block with ref counts) and uses atomic increments/decrements for thread safety โ meaningful overhead in tight loops.
Q3: What is a cyclic reference and how does weak_ptr fix it?
When A holds shared_ptr<B> and B holds shared_ptr<A>, both ref-counts never reach 0 โ memory leaked forever. Break one direction with weak_ptr โ it observes without owning, allowing proper destruction.
Q4: Is shared_ptr thread-safe?
The ref-count management (copy/destruction of shared_ptr) is thread-safe. However, the pointed-to object is NOT protected โ concurrent access to the object still requires a mutex.
Q5: What is enable_shared_from_this?
When a member function needs to return a shared_ptr to itself (this), it can't call shared_ptr<T>(this) โ that creates a second independent ownership chain. Inheriting from enable_shared_from_this<T> and calling shared_from_this() safely returns a sharing copy of the existing shared_ptr.