Intermediate 13 min

Smart Pointers - shared_ptr

Master shared_ptr for automatic memory management with shared ownership

Learn how to use shared_ptr for automatic memory management when multiple owners need to share the same object.

A Simple Example

#include <iostream>
#include <memory>
#include <string>

class Asset {
    std::string name;

public:
    explicit Asset(const std::string& n) : name{n} {
        std::cout << "Asset " << name << " created\n";
    }

    ~Asset() {
        std::cout << "Asset " << name << " destroyed\n";
    }

    void use() const {
        std::cout << "Using asset: " << name << "\n";
    }
};

int main() {
    std::shared_ptr<Asset> ptr1;

    {
        auto ptr2{std::make_shared<Asset>("Texture")};
        std::cout << "Reference count: " << ptr2.use_count() << "\n";

        ptr1 = ptr2;  // Now two owners
        std::cout << "Reference count: " << ptr2.use_count() << "\n";

        ptr2->use();
    }  // ptr2 destroyed, but asset still alive (ptr1 owns it)

    std::cout << "After inner scope\n";
    std::cout << "Reference count: " << ptr1.use_count() << "\n";
    ptr1->use();

    return 0;
}  // Asset destroyed when ptr1 goes out of scope

Breaking It Down

Reference Counting Mechanism

  • Each shared_ptr maintains a reference count - how many shared_ptrs share the object
  • Creating new shared_ptr to same object increments count
  • Destroying shared_ptr decrements count
  • When count reaches zero, object is automatically deleted
  • Remember: use_count() returns the current reference count

Creating shared_ptr

  • Preferred: auto ptr = std::make_shared<T>(args); - single allocation
  • Alternative: std::shared_ptr<T> ptr{new T{args}}; - two allocations
  • Why make_shared: More efficient, exception-safe
  • Remember: Never create multiple shared_ptrs from the same raw pointer

Shared Ownership

  • Copy assignment creates shared ownership: ptr1 = ptr2;
  • Both pointers manage the same object
  • Object lives until ALL shared_ptrs are destroyed
  • Remember: Perfect for cache, observers, shared resources

Usage and Access

  • Arrow operator: ptr->method() to access members
  • Dereference: *ptr to get reference to object
  • Check validity: if (ptr) or if (ptr != nullptr)
  • Get raw pointer: ptr.get() - use sparingly
  • Remember: Treat like a regular pointer with automatic cleanup

Why This Matters

  • Sometimes multiple parts of your code need to access the same object, and you can't predict which part will finish with it last.
  • shared_ptr uses reference counting to track how many owners exist - the object is only deleted when the last shared_ptr is destroyed.
  • This enables safe shared ownership without manual tracking, preventing both memory leaks and dangling pointers.

Critical Insight

shared_ptr's control block stores the reference count on the heap, separate from the object itself. This is why std::make_shared is more efficient than shared_ptr<T>(new T) - make_shared allocates the object and control block together in one allocation instead of two. For frequently-created objects, this difference matters!

The control block also enables weak_ptr to detect when an object has been destroyed, making weak_ptr perfect for breaking circular references.

Best Practices

Always use make_shared: std::make_shared<T>(args) is more efficient and exception-safe than new.

Share by copying: Create shared ownership by copying shared_ptrs, not from raw pointers.

Avoid circular references: Use weak_ptr when objects reference each other to prevent memory leaks.

Check before dereferencing: Use if (ptr) to ensure the pointer is valid before accessing.

Common Mistakes

Creating shared_ptr from this: Don't do shared_ptr<MyClass>(this) - use shared_from_this() instead.

Circular references: Two objects with shared_ptr to each other never get deleted - use weak_ptr to break the cycle.

Mixing new and shared_ptr: Don't create multiple shared_ptrs from same raw pointer - they'll have separate reference counts.

Thread safety misconception: Reference count is thread-safe, but the object itself is not protected.

Debug Challenge

This code has a dangerous bug. Click the highlighted line to fix it:

1 int main() {
2 Asset* raw = new Asset{"Texture"};
3
4 std::shared_ptr<Asset> ptr1{raw};
5 std::shared_ptr<Asset> ptr2{raw};
6
7 return 0;
8 }

Quick Quiz

  1. When is an object owned by shared_ptr deleted?
When all shared_ptrs are destroyed
When the first shared_ptr is destroyed
Never, until program ends
  1. Why use std::make_shared instead of new?
It's required by the standard
It's more efficient (one allocation vs two)
It's slower but safer
  1. How do you break circular references with shared_ptr?
Use unique_ptr instead
Manually call reset()
Use weak_ptr for one direction

Practice Playground

Time to try out what you just learned! Play with the example code below, experiment by making changes and running the code to deepen your understanding.

Lesson Progress

  • Fix This Code
  • Quick Quiz
  • Practice Playground - run once