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_ptrbecause 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_ptrusingstd::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 counterThe 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:
- The Strong Reference Count: Tracks active
shared_ptrinstances. - The Weak Reference Count: Tracks active
weak_ptrobservers. - 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;
}