09 Smart Pointers & Memory Management

Overview: Safe Memory Management

C++ uses Smart Pointers to implement safe memory management. These RAII classes wrap raw heap pointers, automatically deleting the pointed-to object when the smart pointer goes out of scope.


1. Raw Pointers and their Risks

In C and traditional C++, heap allocation requires manual memory management:

Dog* ptr = new Dog(); // Allocate
ptr->bark();
delete ptr;           // Deallocate (easily forgotten, leading to leaks)

If an exception is thrown between new and delete, the memory is leaked. Modern C++ uses smart pointers to automate this process.


2. Sole Ownership: std::unique_ptr

std::unique_ptr maintains sole ownership of a dynamically allocated resource.

std::unique_ptr<Dog> doggo = std::make_unique<Dog>();

Key Mechanics:

  • Non-Copyable: The copy constructor is explicitly deleted. You cannot copy a unique_ptr because having two unique pointers manage the same raw pointer would lead to double-free bugs.
  • Moveable: You can transfer ownership of the resource to another unique_ptr using std::move().

3. Shared Ownership: std::shared_ptr & The Control Block

std::shared_ptr implements shared ownership of a resource.

std::shared_ptr<Dog> p1 = std::make_shared<Dog>();
std::shared_ptr<Dog> p2 = p1; // Copying increments the reference counter

The Control Block:

Under the hood, shared_ptr allocates a separate Control Block on the heap, which is shared by all instances pointing to the same object. This block contains:

  1. The Strong Reference Count: Tracks active shared_ptr instances.
  2. The Weak Reference Count: Tracks active weak_ptr observers.
  3. The custom allocator/deleter.

When a shared_ptr goes out of scope, it decrements the strong counter. When the count reaches 0, the resource is deleted.


4. Resolving Cycles: std::weak_ptr

A major issue with reference counting is the Circular Reference memory leak:

[Object A] --- shared_ptr ---> [Object B]
[Object A] <--- shared_ptr --- [Object B]

Even if all external pointers to A and B are destroyed, their reference counts remain at 1 because they point to each other. They will leak permanently.

std::weak_ptr as the Observer:

std::weak_ptr acts as a non-owning observer. It references an object managed by a shared_ptr but does not increment the strong reference count.

To access the observed object, you must convert the weak_ptr to a temporary shared_ptr using the .lock() method. This checks if the resource still exists (resolving to nullptr if it has been deleted).


💻 Conceptual Code Demonstration

#include <iostream>
#include <memory>
 
class Node {
public:
    std::string name;
    // Using weak_ptr for the back-pointer breaks the reference cycle
    std::weak_ptr<Node> neighbor; 
    
    Node(const std::string& n) : name(n) { std::cout << name << " constructed\n"; }
    ~Node() { std::cout << name << " destroyed\n"; }
};
 
int main() {
    std::cout << "--- 1. Unique Pointer transfer ---\n";
    {
        std::unique_ptr<Node> ptr1 = std::make_unique<Node>("UniqueNode");
        // std::unique_ptr<Node> ptr2 = ptr1; // ERROR: Copying is disabled
        std::unique_ptr<Node> ptr2 = std::move(ptr1); // Safely transfers ownership
        if (!ptr1) {
            std::cout << "ptr1 is now empty/null.\n";
        }
    } // ptr2 leaves scope -> UniqueNode is destroyed
 
    std::cout << "\n--- 2. Weak Pointer Cycle Breaker ---\n";
    {
        auto nodeA = std::make_shared<Node>("NodeA");
        auto nodeB = std::make_shared<Node>("NodeB");
        
        // Connect them
        nodeA->neighbor = nodeB; // A observes B (weak_ptr)
        nodeB->neighbor = nodeA; // B observes A (weak_ptr)
        
        // Accessing via lock()
        if (auto sharedNeighbor = nodeA->neighbor.lock()) {
            std::cout << "NodeA's neighbor is: " << sharedNeighbor->name << '\n';
        }
    } // Both nodes go out of scope and are successfully destroyed (no leak!)
 
    return 0;
}