Memory Management

Smart Pointers

Master C++ smart pointers — unique_ptr, shared_ptr, weak_ptr — automatic memory management, make_unique/make_shared, ownership semantics, and the Rule of Zero.

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.

cpp
#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 destroyed

make_unique vs new

Always use make_unique:

cpp
// 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

cpp
#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:

cpp
#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 destroyed

make_shared — The Right Way to Create shared_ptr

cpp
// 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:

cpp
#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 destroyed

When to Use Which

Smart PointerUse When
unique_ptrSole ownership — default choice for heap objects
shared_ptrShared ownership — multiple owners, lifetime unclear
weak_ptrObserver/cache — access but not own; break cycles
Raw pointerNon-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

cpp
#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:

cpp
#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

cpp
#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 objects
  • shared_ptr — reference-counted shared ownership; use when multiple owners need to outlive each other
  • weak_ptr — non-owning observer; use to break cycles in shared_ptr graphs
  • Always use make_unique and make_shared instead of new — safer and often faster
  • Pass unique_ptr by 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.