In today’s article, we will revisit smart pointers in C++.
In resource management in C++, manually using <span>new</span>/<span>delete</span> can easily lead to memory leaks, dangling pointers, and exception safety issues. The smart pointers introduced in C++11 encapsulate ordinary pointers with RAII (Resource Acquisition Is Initialization): automatically releasing resources when the object goes out of scope, making programs safer and easier to maintain.
1. Types of Smart Pointers
The standard library <span><memory></span> provides three core types of smart pointers:
| Type | Ownership Model | Characteristics | Typical Scenarios |
|---|---|---|---|
<span>std::unique_ptr</span> |
Exclusive | Uniquely owns the resource, movable but not copyable | Exclusive resource management (files, sockets, mutexes) |
<span>std::shared_ptr</span> |
Shared | Multiple pointers share ownership, reference counting | Shared caches, plugin systems |
<span>std::weak_ptr</span> |
Observer | Does not own the resource, only observes <span>shared_ptr</span> |
Breaking circular references, weak reference caches |
The earlier
<span>std::auto_ptr</span>has been deprecated in C++11 and is no longer used.
2. std::unique_ptr — Exclusive Ownership
<span>unique_ptr</span> represents a pointer that uniquely owns the resource. Characteristics:
- Copying is prohibited, it can only be moved (
<span>std::move</span>). - Automatically calls
<span>delete</span>or a custom deleter when the scope ends.
Basic Usage
#include <memory>
#include <iostream>
struct Foo
{
Foo() { std::cout << "Foo()\n"; }
~Foo() { std::cout << "~Foo()\n"; }
};
int main()
{
std::unique_ptr<Foo> p1 = std::make_unique<Foo>(); // Recommended way
/* p1 is null, no longer pointing to the object, p2 goes out of scope, automatically delete */
std::unique_ptr<Foo> p2 = std::move(p1); // Transfer ownership
}
Scenarios
- RAII Encapsulation: Commonly used for exclusive resources such as file descriptors, sockets, mutexes, and other system resources.
- Factory Function Returns: Clearly indicates ownership transfer.
3. std::shared_ptr — Reference Counting Shared
<span>shared_ptr</span> allows multiple pointers to share ownership of the same object. It internally maintains a control block that contains:
- Reference count (thread-safe)
- Deleter
Basic Usage
#include <memory>
#include <iostream>
struct Foo
{
Foo() { std::cout << "Foo()\n"; }
~Foo() { std::cout << "~Foo()\n"; }
};
int main()
{
auto sp1 = std::make_shared<Foo>();
std::shared_ptr<Foo> sp2 = sp1; // Reference count +1
std::cout << sp1.use_count() << '\n'; // Output 2
// The last shared_ptr destructor automatically deletes
}
Circular Reference Scenario
#include<iostream>
#include<memory>
class Test2;
class Test1
{
public:
std::shared_ptr<Test2> test2_ptr;
~Test1()
{
std::cout << "Test1 has been destroyed."<< std::endl;
}
};
class Test2
{
public:
std::shared_ptr<Test1> test1_ptr;
~Test2()
{
std::cout << "Test2 has been destroyed."<< std::endl;
}
};
int main()
{
std::shared_ptr<Test1> test1 = std::make_shared<Test1>();
std::shared_ptr<Test2> test2 = std::make_shared<Test2>();
test1->test2_ptr = test2; // Test1 references Test2
test2->test1_ptr = test1; // Test2 references Test1
// Due to circular references, the destructors of Test1 and Test2 will not be called, leading to memory leaks
return 0;
}
Typical Applications
- Multi-module Shared Resources: For example, cache objects, configuration centers.
- Observer Pattern: Event subscribers hold the same subject object.
Considerations
- Circular References: Objects referencing each other with
<span>shared_ptr</span>will never be released, requiring<span>weak_ptr</span>to resolve. - Overhead: Additional control block allocation and atomic operations.
4. std::weak_ptr — Weak Reference Observer
<span>weak_ptr</span> does not own the object, it only serves as an observer of <span>shared_ptr</span>.
- Does not increase the reference count.
- Can use
<span>lock()</span>to safely obtain a<span>shared_ptr</span>, returning null if the object has been destroyed.
Example
#include <memory>
#include <iostream>
struct Foo
{
Foo()
{
std::cout<<"Foo()\n";
}
~Foo()
{
std::cout<<"~Foo()\n";
}
};
int main()
{
auto sp = std::make_shared<Foo>();
std::weak_ptr<Foo> wp = sp; // Does not increase reference count
if (auto p = wp.lock())
{ // Attempt to promote
std::cout << "Object still exists\n";
}
} // After sp destructs, wp.lock() returns null
Typical Scenarios
- Breaking Circular References: In bidirectional associated objects, one side holds a
<span>shared_ptr</span>, while the other holds a<span>weak_ptr</span>. - Caching/Observer: Only need to check if the object is alive, without affecting its lifecycle.
5. Common Methods and Techniques
-
Prefer using
<span>std::make_unique</span>/<span>std::make_shared</span>
- Avoid raw
<span>new</span>, better exception safety. <span>make_shared</span>allocates the object and control block in one go, more efficient.
Custom Deleters are suitable for file handles, database connections:
std::shared_ptr<FILE> fp(fopen("a.txt","r"), fclose);
Performance Considerations If sharing is not needed, avoid the atomic counting overhead of <span>shared_ptr</span>.
Lifecycle Management Smart pointers are just tools for automatic release; ensure that they are not released too early or too late logically.
6. Application Example: Bidirectional Tree Node
struct Node
{
int value;
std::shared_ptr<Node> left;
std::weak_ptr<Node> parent; // Prevent circular references
};
<span>left</span> owns child nodes; <span>parent</span> only observes the parent node, avoiding circular references that lead to leaks.
Below, we use <span>std::shared_ptr</span> and <span>std::weak_ptr</span> to construct bidirectional (parent-child mutual reference) tree nodes, preventing memory leaks caused by circular references.
Example
#include <iostream>
#include <memory>
#include <string>
// Bidirectional tree node
struct Node
{
std::string name;
std::shared_ptr<Node> left; // Owns child nodes
std::shared_ptr<Node> right; // Owns child nodes
std::weak_ptr<Node> parent; // Only observes the parent node, avoiding circular references
explicit Node(std::string n) : name(std::move(n))
{
std::cout << "Construct: " << name << "\n";
}
~Node()
{
std::cout << "Destruct : " << name << "\n";
}
};
// Helper function: establish parent-child relationship
void setChild(const std::shared_ptr<Node>& parent,
const std::shared_ptr<Node>& child,
bool isLeft)
{
if (isLeft) parent->left = child;
else parent->right = child;
child->parent = parent; // weak_ptr points to the parent node
}
int main()
{
// Create root node
auto root = std::make_shared<Node>("root");
// Create left and right child nodes and establish relationships
auto leftChild = std::make_shared<Node>("left");
auto rightChild = std::make_shared<Node>("right");
setChild(root, leftChild, true);
setChild(root, rightChild, false);
// Access parent node: need to lock first
if (auto p = leftChild->parent.lock())
{
std::cout << leftChild->name << "'s parent is " << p->name << "\n";
}
// After the scope ends, root/left/right will all be automatically destructed
// Because parent is weak_ptr, it will not form circular references
return 0;
}
Run Output
Construct: root
Construct: left
Construct: right
left's parent is root
Destruct : left
Destruct : right
Destruct : root
As we can see:
- All nodes are correctly destructed, with no memory leaks.
<span>parent</span><span> uses </span><code><span>weak_ptr</span>, even if<span>child</span>points to<span>parent</span>, it does not increase the reference count, thus avoiding circular references.
Key Points Explanation
-
The parent pointer must be
<span>weak_ptr</span>If the parent pointer is<span>shared_ptr</span>,<span>root</span>and<span>leftChild</span>will hold each other, and the reference count will never reach zero, leading to memory leaks. -
Use
<span>lock()</span>when accessing the parent node<span>weak_ptr::lock()</span>will return a temporary<span>shared_ptr</span>, allowing safe access when the object exists; if the object has been destroyed,<span>lock()</span>returns a null pointer. -
RAII Automatic Management No need for manual
<span>delete</span><span>, all resources are automatically released when going out of scope, ensuring exception safety.</span>
Through this method, we can easily implement bidirectional trees, graph structures, or any object model requiring bidirectional references, while avoiding the most common pitfalls of smart pointers—circular references.
7. Selection
| Requirement | Recommendation |
|---|---|
| Exclusive Resource | <span>unique_ptr</span> |
| Multiple Sharing | <span>shared_ptr</span> |
| Only Observing | <span>weak_ptr</span> |
Rule of Thumb: Use
<span>unique_ptr</span>whenever possible instead of<span>shared_ptr</span>.
Conclusion
The core of smart pointers is RAII + Ownership Model:
- Exclusive Ownership:
<span>unique_ptr</span> - Reference Counting Shared:
<span>shared_ptr</span> - Weak Observation:
<span>weak_ptr</span>
Choosing smart pointers wisely can greatly reduce the risk of memory leaks, simplify resource management, and write safe, concise, and modern C++ code.
Reading a hundred times, its meaning will naturally appear. Coding is the same, always fresh.

END
Author:YuLinMuRong
Source:Linux ArmoryCopyright belongs to the original author, please contact for deletion if there is infringement..▍Recommended ReadingWhy is C++ rarely used in microcontroller development?Xiaomi is really stingy, a single MCU actually handles all functionsUploaded a PCB photo with only 2 lines of silk screen, GPT-5 helped me solve everything!→ Follow for more updates ←