Smart pointers are an important feature introduced in C++11 and later versions, used for automatic management of dynamically allocated memory, avoiding memory leaks and dangling pointer issues. Smart pointers utilize RAII (Resource Acquisition Is Initialization) technique to automatically release the managed resources when the object’s lifecycle ends. Below is a detailed introduction to smart pointers in C++:
1. Why Do We Need Smart Pointers?
Traditional manual memory management in C++ (using new and delete) has the following issues:
-
Memory Leak: Forgetting to call delete.
-
Dangling Pointer: The object pointed to by the pointer has been released, but it is still in use.
-
Exception Safety: delete may not be executed when an exception occurs.
Smart pointers solve these problems by automatically releasing memory.
2. Types of Smart Pointers in C++
The C++ standard library provides three main types of smart pointers:
-
std::unique_ptr
-
std::shared_ptr
-
std::weak_ptr
3. std::unique_ptr
Characteristics
-
Exclusive Ownership: Only one unique_ptr can point to the object at any time.
-
Lightweight: Almost no additional overhead, performance is close to raw pointers.
-
Non-Copyable: But ownership can be transferred using std::move.
Example
#include <memory>
void unique_ptr_example() {
// Creation method 1: direct initialization
std::unique_ptr<int> ptr1(new int(42));
// Creation method 2 (recommended): use make_unique (C++14 and later)
auto ptr2 = std::make_unique<int>(42);
// Transfer ownership
std::unique_ptr<int> ptr3 = std::move(ptr1); // ptr1 is now null
// Access object
std::cout << *ptr3 << std::endl; // Output: 42
// Custom deleter (optional)
auto deleter = [](int* p) {
std::cout << "Custom deleting..." << std::endl;
delete p;
};
std::unique_ptr<int, decltype(deleter)> ptr4(new int(100), deleter);
} // ptr3 and ptr4 automatically release memory when they go out of scope
Application Scenarios
Managing dynamically allocated resources (such as file handles, network connections).
As container elements (e.g., std::vector<std::unique_ptr<Base>> to implement polymorphic containers).
4. std::shared_ptr
Characteristics
-
Shared Ownership: Manages multiple pointers pointing to the same object through reference counting.
-
Thread-Safe: Incrementing and decrementing the reference count is atomic.
-
Higher Overhead: Each shared_ptr maintains a reference count and weak reference count.
Example
#include <memory>
class MyClass {
public:
~MyClass() { std::cout << "Destroying MyClass" << std::endl; }};
void shared_ptr_example() {
// Creation method 1: direct initialization
std::shared_ptr<MyClass> ptr1(new MyClass);
// Creation method 2 (recommended): use make_shared (more efficient)
auto ptr2 = std::make_shared<MyClass>();
// Shared ownership
std::shared_ptr<MyClass> ptr3 = ptr1; // Reference count +1
// Check reference count
std::cout << "Use count: " << ptr1.use_count() << std::endl; // Output: 2
// Custom deleter (needs to use shared_ptr constructor)
auto file_deleter = [](FILE* f) {
if (f) fclose(f);
};
std::shared_ptr<FILE> file_ptr(fopen("test.txt", "r"), file_deleter);
} // All shared_ptrs are destroyed when they go out of scope, reference count goes to 0
Circular Reference Problem
When two objects reference each other through shared_ptr, it leads to memory leaks:
class B;
class A {
public:
std::shared_ptr<B> b_ptr;
~A() { std::cout << "A destroyed" << std::endl; }};
class B {
public:
std::shared_ptr<A> a_ptr; // Should use std::weak_ptr to avoid circular reference
~B() { std::cout << "B destroyed" << std::endl; }};
void circular_reference() {
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->b_ptr = b;
b->a_ptr = a; // Circular reference: reference counts of a and b will never be 0
} // Memory leak!
5. std::weak_ptr
Characteristics
-
Weak Reference: Does not control the object’s lifecycle, only observes the object managed by shared_ptr.
-
Solves Circular Reference: Breaks the circular dependency of shared_ptr.
-
Requires Conversion: Access to the object is obtained through lock() to get shared_ptr.
Example
class B;
class A {
public:
std::weak_ptr<B> b_ptr; // Use weak_ptr to avoid circular reference
~A() { std::cout << "A destroyed" << std::endl; }};
class B {
public:
std::weak_ptr<A> a_ptr; // Use weak_ptr to avoid circular reference
~B() { std::cout << "B destroyed" << std::endl; }};
void weak_ptr_example() {
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->b_ptr = b;
b->a_ptr = a; // No circular reference: reference counts of a and b are both 1
// Use weak_ptr to access the object
if (auto shared_b = a->b_ptr.lock()) { // Check if the object exists
std::cout << "b is still alive" << std::endl;
}} // a and b are destroyed normally
6. Common Methods of Smart Pointers
|
Method |
Description |
|
get() |
Returns the raw pointer |
|
reset() |
Releases the currently managed object, can specify a new object |
|
swap() |
Swaps the contents of two smart pointers |
|
use_count() |
Returns the reference count (only for shared_ptr and weak_ptr) |
|
expired() |
Checks if the object has been destroyed (only for weak_ptr) |
|
lock() |
Gets shared_ptr from weak_ptr (only for weak_ptr) |
7. Custom Deleters
Smart pointers allow specifying custom deleters for releasing non-memory resources:
// File handle management
auto file_deleter = [](FILE* f) {
if (f) fclose(f);
};
std::unique_ptr<FILE, decltype(file_deleter)> file(fopen("test.txt", "r"), file_deleter);
// Array management
std::unique_ptr<int[]> array(new int[10]); // Automatically uses delete[]
// Custom resource management
struct Resource {
void acquire() { /* ... */ }
void release() { /* ... */ }};
auto resource_deleter = [](Resource* r) {
if (r) r->release();
};
std::unique_ptr<Resource, decltype(resource_deleter)> res(new Resource, resource_deleter);
8. Pitfalls and Precautions of Smart Pointers
1. Avoid mixing raw pointers and smart pointers:
int* raw = new int;
std::shared_ptr<int> ptr1(raw);
std::shared_ptr<int> ptr2(raw); // Error: double deletion!
2. Avoid creating shared_ptr from this:
class Bad {
public:
std::shared_ptr<Bad> get_shared() {
return std::shared_ptr<Bad>(this); // Error: multiple independent shared_ptrs pointing to the same object
}
}; // Correct way: inherit from std::enable_shared_from_this
3. Prefer using make_shared and make_unique:
-
More efficient (single memory allocation).
-
Exception safe.
4. Understand special handling of arrays:
// C++17 and later
std::unique_ptr<int[]> arr = std::make_unique<int[]>(10); // Correct
std::shared_ptr<int[]> arr2(new int[10], [](int* p) { delete[] p; }); // Needs custom deleter
9. Application Scenarios of Smart Pointers
-
Resource Management: File handles, network connections, locks, etc.
-
Container Elements: Storing polymorphic objects
(e.g., std::vector<std::unique_ptr<Shape>>).
-
Avoiding Memory Leaks: In complex control flows or exception handling.
-
Cache Systems: Using weak_ptr to implement caches, avoiding impact on object lifecycle.
10. Improvements in C++20 and Later
std::make_shared_for_overwrite: Used for initializing objects that do not require default construction.
std::expected: Used in conjunction with smart pointers to handle error returns.
std::atomic<std::shared_ptr>: Atomic operation smart pointers for multi-threaded environments.
11. Conclusion
Smart pointers are core tools for managing dynamic memory in C++. Proper use of them can significantly enhance code safety and maintainability. It is recommended to follow these principles:
Prefer using std::unique_ptr: When object ownership is clear.
Use std::shared_ptr: When shared ownership is needed.
Use std::weak_ptr: When weak references or breaking circular references are needed.
Avoid using raw pointers: Unless necessary (e.g., for interface compatibility).