Smart pointers in C++ are special classes that manage dynamically allocated memory, automatically freeing it when it’s no longer in use.
They help prevent memory leaks by ensuring that dynamically allocated resources are properly deleted. C++11 introduced three types of smart pointers in the <memory> library: std::unique_ptr, std::shared_ptr, and std::weak_ptr.
In this tutorial, we’ll cover:
1. std::unique_ptr — Exclusive Ownership
std::unique_ptr is a smart pointer that provides exclusive ownership of a dynamically allocated object. This means only one unique_ptr can own the resource at any time. When the unique_ptr goes out of scope, the resource is automatically deallocated.
Basic Syntax and Usage
To use std::unique_ptr, include the <memory> header.
#include <iostream> #include <memory> int main() { // Create a unique_ptr to an integer std::unique_ptr<int> ptr = std::make_unique<int>(10); std::cout << "Value of ptr: " << *ptr << std::endl; return 0; }
Output:
Value of ptr: 10
In this example:
- std::make_unique<int>(10) creates a new integer with value 10 and returns a unique_ptr to it.
- *ptr dereferences the unique_ptr to access the stored integer.
Transferring Ownership with std::move
To transfer ownership of a unique_ptr, use std::move.
#include <iostream> #include <memory> int main() { std::unique_ptr<int> ptr1 = std::make_unique<int>(20); std::unique_ptr<int> ptr2 = std::move(ptr1); // Transfer ownership to ptr2 if (!ptr1) { std::cout << "ptr1 is now null." << std::endl; } std::cout << "Value of ptr2: " << *ptr2 << std::endl; return 0; }
Output:
ptr1 is now null. Value of ptr2: 20
Note: After transferring ownership using std::move, ptr1 becomes nullptr.
Custom Deleter
You can specify a custom deleter for a unique_ptr to customize how the resource is released.
#include <iostream> #include <memory> void customDeleter(int* ptr) { std::cout << "Custom deleting integer: " << *ptr << std::endl; delete ptr; } int main() { std::unique_ptr<int, decltype(&customDeleter)> ptr(new int(30), customDeleter); std::cout << "Value of ptr: " << *ptr << std::endl; return 0; }
Output:
Value of ptr: 30 Custom deleting integer: 30
2. std::shared_ptr — Shared Ownership
std::shared_ptr is a smart pointer that allows multiple pointers to share ownership of a single resource. When the last shared_ptr to an object is destroyed, the resource is automatically freed. It maintains a reference count to keep track of how many shared_ptrs are sharing the same resource.
Basic Syntax and Usage
To use std::shared_ptr, include the <memory> header.
#include <iostream> #include <memory> int main() { std::shared_ptr<int> ptr1 = std::make_shared<int>(50); std::shared_ptr<int> ptr2 = ptr1; // Shared ownership std::cout << "Value of ptr1: " << *ptr1 << std::endl; std::cout << "Value of ptr2: " << *ptr2 << std::endl; std::cout << "Reference count: " << ptr1.use_count() << std::endl; return 0; }
Output:
Value of ptr1: 50 Value of ptr2: 50 Reference count: 2
In this example:
- std::make_shared<int>(50) creates a shared_ptr to an integer with a value of 50.
- ptr2 = ptr1 shares ownership of the same integer.
- use_count() returns the number of shared_ptrs sharing ownership of the resource (2 in this case).
Resetting a shared_ptr
You can reset a shared_ptr to release ownership, which decreases the reference count. If the count reaches zero, the resource is deallocated.
#include <iostream> #include <memory> int main() { std::shared_ptr<int> ptr1 = std::make_shared<int>(100); std::shared_ptr<int> ptr2 = ptr1; std::cout << "Reference count before reset: " << ptr1.use_count() << std::endl; ptr1.reset(); // Release ownership from ptr1 std::cout << "Reference count after reset: " << ptr2.use_count() << std::endl; return 0; }
Output:
Reference count before reset: 2 Reference count after reset: 1
Custom Deleter
You can also specify a custom deleter for shared_ptr just like with unique_ptr.
#include <iostream> #include <memory> void customDeleter(int* ptr) { std::cout << "Custom deleting integer: " << *ptr << std::endl; delete ptr; } int main() { std::shared_ptr<int> ptr(new int(200), customDeleter); std::cout << "Value of ptr: " << *ptr << std::endl; return 0; }
Output:
Value of ptr: 200 Custom deleting integer: 200
3. std::weak_ptr — Non-owning Reference to shared_ptr
std::weak_ptr is a non-owning smart pointer that works with std::shared_ptr. It allows you to reference a resource managed by a shared_ptr without affecting the reference count. It’s often used to break circular dependencies in situations where two shared_ptrs reference each other.
Basic Syntax and Usage
To use std::weak_ptr, include the <memory> header. A weak_ptr does not directly access the resource but can be converted to a shared_ptr using the lock function, which returns a shared_ptr if the resource is still valid.
#include <iostream> #include <memory> int main() { std::shared_ptr<int> sharedPtr = std::make_shared<int>(300); std::weak_ptr<int> weakPtr = sharedPtr; // Weak reference to sharedPtr std::cout << "Reference count: " << sharedPtr.use_count() << std::endl; // Accessing value via weak_ptr if (auto ptr = weakPtr.lock()) { // lock() returns a shared_ptr if valid std::cout << "Value of sharedPtr through weakPtr: " << *ptr << std::endl; } else { std::cout << "Resource is no longer available." << std::endl; } sharedPtr.reset(); // Deallocate the shared resource // Attempt to access value again if (auto ptr = weakPtr.lock()) { std::cout << "Value of sharedPtr through weakPtr: " << *ptr << std::endl; } else { std::cout << "Resource is no longer available." << std::endl; } return 0; }
Output:
Reference count: 1 Value of sharedPtr through weakPtr: 300 Resource is no longer available.
In this example:
- weakPtr.lock() attempts to create a shared_ptr from weakPtr. If successful, it provides access to the resource.
- After sharedPtr.reset() releases the resource, weakPtr.lock() returns nullptr.
Breaking Circular Dependencies
weak_ptr is often used to break circular dependencies between two objects that reference each other with shared_ptrs.
#include <iostream> #include <memory> class Node { public: std::shared_ptr<Node> next; std::weak_ptr<Node> prev; // Use weak_ptr to break circular reference Node() { std::cout << "Node created." << std::endl; } ~Node() { std::cout << "Node destroyed." << std::endl; } }; int main() { std::shared_ptr<Node> node1 = std::make_shared<Node>(); std::shared_ptr<Node> node2 = std::make_shared<Node>(); node1->next = node2; node2->prev = node1; // Prevent circular dependency with weak_ptr return 0; }
Output:
Node created. Node created. Node destroyed. Node destroyed.
Without weak_ptr, there would be a circular dependency, preventing the reference count from reaching zero and causing a memory leak. By using weak_ptr, we avoid this issue.
Summary
In this tutorial, we covered:
- std::unique_ptr: Provides exclusive ownership of an object. We learned how to transfer ownership with std::move and use custom deleters.
- std::shared_ptr: Allows shared ownership, where multiple pointers can refer to the same resource. We learned how to check the reference count with use_count and reset pointers.
- std::weak_ptr: A non-owning pointer used alongside shared_ptr, often to break circular dependencies.
Smart pointers in C++ make resource management safer and easier by automating memory cleanup, reducing the chance of memory leaks and dangling pointers.