Smart Pointer in C++: Examples, Uses, and Common Pitfalls

Looking for a clear, beginner‑friendly guide to Smart Pointer in C++? This article explains what smart pointers are, why they matter, and exactly how to use them safely—with commented code, outputs, and “Try it yourself” challenges after each section.

🔹 What is a Smart Pointer in C++?

A Smart Pointer in C++ is a class template that behaves like a pointer but also owns and automatically releases the resource it points to (RAII: Resource Acquisition Is Initialization). This means memory and other resources are freed deterministically when the smart pointer goes out of scope, even if exceptions happen.

#include <iostream>
#include <memory>
using namespace std;
struct Greeter {
    Greeter() { cout << "Greeter constructed\n"; }
    ~Greeter() { cout << "Greeter destroyed\n"; }
    void hello() const { cout << "Hello, RAII!\n"; }
};
int main() {
    // unique_ptr owns the Greeter; auto-cleans in all paths
    unique_ptr<Greeter> g = make_unique<Greeter>();
    g->hello();
    // no delete needed; ~Greeter runs when g leaves scope
}

Output

Greeter constructed
Hello, RAII!
Greeter destroyed

Explanation: unique_ptr takes ownership of the object and deletes it automatically at scope exit; no manual delete is required. This is the core idea behind Smart Pointer in C++.

Try it yourself

  • Add a second inner scope and create another unique_ptr<Greeter>; observe the destruction order.
  • Comment out make_unique and try unique_ptr<Greeter> g(new Greeter()); then switch back to make_unique to prefer exception safety.

🔹 Why use Smart Pointer in C++?

  • Automatic cleanup: Prevents leaks and double frees by tying ownership to scope (RAII).
  • Exception safety: Destructors run during unwinding, so resources are released even on errors.
  • Clear ownership: unique_ptr = single owner, shared_ptr = shared ownership, weak_ptr = non‑owning observer.
  • Interoperability: Works with STL containers, algorithms, and custom deleters for non‑memory resources.

🔹 unique_ptr: The Default Smart Pointer in C++

unique_ptr expresses exclusive ownership: copy is disallowed, move is allowed. Use make_unique in modern code for safety and clarity.

#include <iostream>
#include <memory>
using namespace std;
struct Toy {
    string name;
    explicit Toy(string n) : name(move(n)) 
    { cout << "Make " << name << "\n"; }
    ~Toy() { cout << "Drop " << name << "\n"; }
    void play() const 
    { cout << "Playing with " << name << "\n"; }
};
unique_ptr<Toy> make() {
    // Prefer make_unique for exception-safe construction
    return make_unique<Toy>("Car");
}
int main() {
    auto p = make(); // p owns the Toy
    p->play();
    // Transfer (move) ownership to q
    auto q = move(p);
    if (!p) 
    { cout << "p is empty after move\n"; }
    q->play(); // still valid via q
    // q goes out of scope → Toy destroyed
}

Output

Make Car
Playing with Car
p is empty after move
Playing with Car
Drop Car

Explanation: unique_ptr enforces one owner. Moving transfers ownership cleanly. This is the most common and safest default for Smart Pointer in C++ when sharing isn’t needed.

Try it yourself

  • Uncomment a copy attempt auto r = q; and read the compiler error to understand why copying is disabled.
  • Store unique_ptr<Toy> in a vector; push a few with emplace_back(make_unique<Toy>(...)) and observe construction/destruction order.

🔹 shared_ptr: Shared Ownership Smart Pointer in C++

shared_ptr keeps a reference count so multiple owners can share the same object. When the last shared_ptr is destroyed/reset, the object is deleted. Use make_shared to allocate object and control block together for efficiency.

#include <iostream>
#include <memory>
#include <vector>
using namespace std;
struct Book {
    string title;
    explicit Book(string t) : title(move(t)) 
    { cout << "Open " << title << "\n"; }
    ~Book() 
    { cout << "Close " << title << "\n"; }
};
int main() {
    auto b = make_shared<Book>("CPP Primer");
    cout << "use_count: " 
    << b.use_count() << "\n";
    vector<shared_ptr<Book>> shelf;
    shelf.push_back(b);
    shelf.push_back(b);
    cout << "use_count after copies: " 
    << b.use_count() << "\n";
    shelf.clear(); // drop two owners
    cout << "use_count after clear: " 
    << b.use_count() << "\n";
    b.reset(); // last owner gone → destructor runs
}

Output

Open CPP Primer
use_count: 1
use_count after copies: 3
use_count after clear: 1
Close CPP Primer

Explanation: shared_ptr increments/decrements its control block count as copies come and go. When the count reaches zero, the managed object is deleted automatically.

Try it yourself

  • Create two shared_ptr from new of the same raw pointer (don’t actually do this in real code) and observe double delete—then fix by using only one shared_ptr created via make_shared.
  • Put shared_ptr into a map<string, shared_ptr<Book>>, erase entries, and watch use_count changes.

🔹 weak_ptr: Observing Without Owning

weak_ptr points to a shared_ptr-managed object without increasing its reference count, helping break cycles and check liveness safely with lock().

#include <iostream>
#include <memory>
using namespace std;
struct Node {
    string name;
    shared_ptr<Node> next; // owning
    weak_ptr<Node> prev; // non-owning to avoid cycle
    explicit Node(string n) : name(move(n)) 
    { cout << "New " << name << "\n"; }
    ~Node() { cout << "Free " << name << "\n"; }
};
int main() {
    auto a = make_shared<Node>("A");
    auto b = make_shared<Node>("B");
    a->next = b; // A owns B
    b->prev = a; // B observes A (weak)
    if (auto pa = b->prev.lock()) {
        cout << "Prev of B is " 
        << pa->name << "\n";
    }
    // Both freed at scope end; weak_ptr prevents cycle leak
}

Output

New A
New B
Prev of B is A
Free B
Free A

Explanation: Use weak_ptr to avoid reference cycles (e.g., bidirectional graphs). Use lock() to get a temporary shared_ptr if the object is still alive.

Try it yourself

  • Change prev to shared_ptr<Node> and see that destructors don’t run (leak via cycle); revert to weak_ptr to fix.
  • Reset a explicitly before the end and verify b->prev.lock() returns empty.

🔹 make_unique vs new, make_shared vs new

Prefer factory helpers (make_unique, make_shared) over raw new to avoid leaks on exceptions, reduce typing, and improve efficiency (especially for make_shared).

#include <memory>
#include <vector>
#include <string>
using namespace std;
struct Item { string id; explicit Item(string s) : id(move(s)) {} };
int main() {
    vector<shared_ptr<Item>> v;
    // Good: strong exception safety and fewer allocations overall
    v.push_back(make_shared<Item>("A"));
    v.push_back(make_shared<Item>("B"));
    // Also good for exclusive ownership
    auto u = make_unique<Item>("Temp");
    (void)u;
}

Explanation: make_shared allocates the control block and object in one go, often improving performance and locality. make_unique avoids accidental naked new.

Try it yourself

  • Create 10,000 items with make_shared and then with separate shared_ptr<Item>(new Item(...)); time both approaches to compare.
  • Return unique_ptr from a factory function to practice move semantics without extra copies.

🔹 Custom Deleters: Managing Non‑Memory Resources

A Smart Pointer in C++ can free any kind of handle via a custom deleter—great for files, sockets, and mutexes.

#include <cstdio>
#include <memory>
#include <iostream>
using namespace std;
struct FileCloser {
    void operator()(FILE* fp) const {
        if (fp) 
        { fclose(fp); cout << "File closed\n"; }
    }
};
int main() {
    unique_ptr<FILE, FileCloser> fp(fopen("out.txt", "w"));
    if (!fp) { cout << "Open failed\n"; return 0; }
    // automatic fclose at scope exit
    fputs("Hello\n", fp.get()); 
}

Output

File closed

Explanation: The custom deleter runs in unique_ptr’s destructor, ensuring fclose is always called, not just memory delete.

Try it yourself

  • Replace FileCloser with a lambda deleter like unique_ptr<FILE, decltype(&fclose)> fp(fopen(...), &fclose).
  • Adapt the pattern to a mock network socket handle and log “Socket closed” in the deleter.

🔹 Breaking Cycles with enable_shared_from_this

For callbacks that need a shared_ptr to the current object, prefer inheriting enable_shared_from_this<T> and calling shared_from_this() safely (only when the object itself is owned by shared_ptr).

#include <iostream>
#include <memory>
#include <functional>
using namespace std;
struct Task : enable_shared_from_this<Task> {
    void runLater(function<void(shared_ptr<Task>)> cb) {
        // obtain a shared_ptr to this if already owned by shared_ptr
        cb(shared_from_this());
    }
    void work() const {
        cout << "Working...\n";
    }
};
int main() {
    auto t = make_shared<Task>();
    t->runLater([](shared_ptr<Task> self) {
        self->work();
    });
}

Output

Working...

Explanation: shared_from_this() is safe only if the object is already managed by a shared_ptr. Avoid creating a fresh shared_ptr(this) from within a raw‑constructed object.

Try it yourself

  • Create a timer that stores the callback and executes it later using shared_from_this() to keep the task alive.
  • Experiment calling shared_from_this() on a stack‑allocated object and observe the exception; then fix by using make_shared.

🔹 Performance Notes and Gotchas

  • unique_ptr is as small and fast as a raw pointer (plus destructor semantics); prefer it by default.
  • shared_ptr adds a control block and atomic ref count; avoid unnecessary sharing on hot paths.
  • make_shared improves locality and reduces allocations; but cannot use custom deleter with it (use direct constructor if needed).
  • Never create two independent shared_ptr from the same raw pointer; always have a single owning creator.
  • Prefer passing const T& or raw non‑owning T*/T& to functions that don’t need ownership, to avoid accidental ref count bumps.

🔹 Best Practices for Smart Pointer in C++

  • Default to unique_ptr; switch to shared_ptr only when multiple owners are required.
  • Use weak_ptr to break ownership cycles and to observe optional lifetime safely.
  • Prefer make_unique/make_shared over raw new for safety and clarity.
  • Attach custom deleters for non‑memory resources (files, sockets, mutexes).
  • Do not store pointers or references to elements across vector reallocation unless capacity is reserved.

🔹 Mini Project: Image Cache with Smart Pointer in C++

This cache keeps images alive while referenced by clients and allows automatic cleanup once all shared_ptr owners are gone; lookups store non‑owning weak_ptr inside the cache map.

#include <iostream>
#include <memory>
#include <string>
#include <unordered_map>
using namespace std;
struct Image {
    string path;
    explicit Image(string p) : path(move(p)) {
        cout << "Load " << path << "\n";
    }
    ~Image() {
        cout << "Free " << path << "\n";
    }
    void draw() const {
        cout << "Draw " << path << "\n";
    }
};
class ImageCache {
    unordered_map<string, weak_ptr<Image>> table;
public:
    shared_ptr<Image> get(const string& key) {
        if (auto it = table.find(key); it != table.end()) {
            if (auto sp = it->second.lock()) {
                return sp; // reuse
            }
        }
        auto sp = make_shared<Image>(key); // load fresh
        table[key] = sp; // remember weakly
        return sp;
    }
    void sweep() {
        // Optional: remove expired entries
        for (auto it = table.begin(); it != table.end(); ) {
            if (it->second.expired()) {
                it = table.erase(it);
            } else {
                ++it;
            }
        }
    }
};
int main() {
    ImageCache cache;
    {
        auto a = cache.get("logo.png");
        a->draw();
        auto b = cache.get("logo.png"); // reused
        b->draw();
    }
    cache.sweep(); // drop expired records
    // Fetch again will reload
    auto c = cache.get("logo.png");
    c->draw();
}

Output

Load logo.png
Draw logo.png
Draw logo.png
Free logo.png
Load logo.png
Draw logo.png

Explanation: The cache holds only weak_ptr entries. When clients drop all shared_ptr references, the image is freed, and the next request reloads it automatically.

Try it yourself

  • Add a capacity limit and evict oldest entries first.
  • Record hits/misses and print cache statistics after each get.

🔹 FAQ: Smart Pointer in C++

Q1. Which smart pointer should be used by default?
unique_ptr. It is lightweight, safe, and expresses exclusive ownership; switch to shared_ptr only when multiple owners are required.

Q2. When is weak_ptr necessary?
Use weak_ptr to observe an object without extending its lifetime, and to break reference cycles between shared_ptr owners.

Q3. Is make_shared always better?
It often is, due to fewer allocations and better locality, but if a custom deleter or special allocator is needed, construct shared_ptr directly.

Q4. Can a raw pointer be stored inside a smart pointer later?
Yes, but let exactly one smart pointer become the sole owner of a given raw pointer and avoid creating multiple owners from the same raw pointer.

Q5. How to pass smart pointers to functions?
For non‑owning APIs, pass T& or const T&/raw T*. Pass shared_ptr<T> only if shared ownership transfer or extension is intended.

Q6. Do smart pointers add runtime cost?
unique_ptr is near zero‑overhead; shared_ptr incurs ref‑count updates (often atomic). Measure before optimizing.

Q7. Is it safe to use shared_from_this() in the constructor?
No. It is only valid after the object is owned by a shared_ptr. Use a factory like make_shared<T>() to create instances.

Q8. How to manage non‑memory resources?
Attach a custom deleter (function object or lambda) to unique_ptr or shared_ptr so the correct release call (e.g., fclose) runs at scope exit.

🔹 Wrapping Up

A well‑applied Smart Pointer in C++ makes code safer, simpler, and exception‑proof. Default to unique_ptr, share deliberately with shared_ptr, observe with weak_ptr, and leverage make_unique/make_shared for clean construction. Practice the “Try it yourself” tasks above to build strong, real‑world instincts.

Leave a Comment

About RadiantRiva

Your go-to resource for coding tutorials, developer guides, and programming tips.

Learn More

Quick Links

Follow Us

Newsletter

Get coding tips, tutorials, and updates straight to your inbox.