Smart pointer
What problem does this section solve?
new/delete allows manual management of dynamic memory, but it requires you to remember to release resources on all code paths. Once a function goes wrong early, throws an exception, or multiple objects hold pointers to each other, it becomes easy to encounter memory leaks, double-free errors, or dangling pointers.
Smart pointers encode "who is responsible for releasing the object" into the type:
| Smart pointer | meaning of ownership | Typical scenarios |
|---|---|---|
std::unique_ptr<T> | Sole ownership | Default selection, an object has only one owner. |
std::shared_ptr<T> | Shared ownership | Objects indeed require multiple owners to collectively extend their lifetime. |
std::weak_ptr<T> | weak references do not own | Observe the managed object of shared_ptr, commonly used to break circular references. |
Smart pointers are essentially RAII: when a smart pointer object goes out of scope, its destructor automatically releases the managed object.
C++ standard version
std::unique_ptr:C++11std::shared_ptr/std::weak_ptr:C++11std::make_unique:C++14std::make_shared:C++11
The required header file is <memory>.
Learning Sequence
- First, examine the manual
new/deletefor issues when returning early. - Use
unique_ptrto solve single ownership and automatic release. - Use
std::moveto transfer ownership ofunique_ptr. - Only use
shared_ptrwhen there is indeed shared ownership. - Use
weak_ptrto observe objects and break circular references. - Function parameters must express true semantics: borrowing, transferring, and sharing are three different things.
Example code
Example 1: Issues with Old Approaches and the RAII Principle of unique_ptr
This example deliberately makes the function return early. In the old version, delete is not executed; in the unique_ptr version, even if the function returns early, the object will automatically destruct.
#include <iostream>
#include <memory>
#include <string>
class Resource
{
std::string name_;
public:
explicit Resource(const std::string& name) : name_(name)
{
std::cout << name_ << " created\n";
}
~Resource()
{
std::cout << name_ << " destroyed\n";
}
void use() const
{
std::cout << name_ << " used\n";
}
};
bool old_style()
{
Resource* res = new Resource("old resource");
res->use();
std::cout << "old_style: early return\n";
return false; // res 没有 delete,析构函数不会执行
}
bool modern_style()
{
auto res = std::make_unique<Resource>("modern resource");
res->use();
std::cout << "modern_style: early return\n";
return false; // res 离开作用域,自动 delete
}
int main()
{
std::cout << std::boolalpha;
bool old_ok = old_style();
std::cout << "old_ok = " << old_ok << "\n";
std::cout << "---\n";
bool modern_ok = modern_style();
std::cout << "modern_ok = " << modern_ok << "\n";
return 0;
}
Results:
old resource created
old resource used
old_style: early return
old_ok = false
---
modern resource created
modern resource used
modern_style: early return
modern resource destroyed
modern_ok = false
Note that in the first paragraph output, there is no old resource destroyed, and this is the most common error that occurs with manual new/delete under complex paths.
Example 2: unique_ptr's exclusive ownership and transfer
unique_ptr cannot be copied, only moved. After being moved, the original pointer becomes a null pointer.
#include <iostream>
#include <memory>
#include <string>
#include <utility>
class Student
{
std::string name_;
public:
explicit Student(const std::string& name) : name_(name)
{
std::cout << "Student " << name_ << " created\n";
}
~Student()
{
std::cout << "Student " << name_ << " destroyed\n";
}
const std::string& name() const
{
return name_;
}
};
// 智能指针负责管理对象生命周期,减少手动释放资源的风险。
void take_ownership(std::unique_ptr<Student> student)
{
std::cout << "take ownership of " << student->name() << "\n";
}
int main()
{
// 程序从 main 函数开始执行,下面的语句会按顺序运行。
auto p = std::make_unique<Student>("Alice");
auto owner = std::move(p);
std::cout << "p is null? " << (p == nullptr ? "yes" : "no") << "\n";
std::cout << "owner has " << owner->name() << "\n";
take_ownership(std::move(owner));
std::cout << "owner is null? " << (owner == nullptr ? "yes" : "no") << "\n";
return 0;
}
Results:
Student Alice created
p is null? yes
owner has Alice
take ownership of Alice
Student Alice destroyed
owner is null? yes
Example 3: Shared Ownership with shared_ptr
shared_ptr manages objects using reference counting. The object is released only after the last shared_ptr is destroyed or reassigned.
#include <iostream>
#include <memory>
#include <string>
class File
{
std::string name_;
public:
explicit File(const std::string& name) : name_(name)
{
std::cout << "File " << name_ << " opened\n";
}
~File()
{
std::cout << "File " << name_ << " closed\n";
}
void read() const
{
std::cout << "read from " << name_ << "\n";
}
};
int main()
{
// 程序从 main 函数开始执行,下面的语句会按顺序运行。
// 智能指针负责管理对象生命周期,减少手动释放资源的风险。
auto file1 = std::make_shared<File>("log.txt");
std::cout << "count after create = " << file1.use_count() << "\n";
{
auto file2 = file1;
std::cout << "count in scope = " << file1.use_count() << "\n";
file2->read();
}
std::cout << "count after scope = " << file1.use_count() << "\n";
file1.reset();
std::cout << "file1 is empty? " << (file1 == nullptr ? "yes" : "no") << "\n";
return 0;
}
Results:
File log.txt opened
count after create = 1
count in scope = 2
read from log.txt
count after scope = 1
File log.txt closed
file1 is empty? yes
shared_ptr is very convenient, but not "a safer default pointer". It has reference counting overhead and will also complicate ownership relationships. By default, prioritize unique_ptr.
Example 4: Using weak_ptr to break circular references of shared_ptr
If two objects reference each other using shared_ptr, their reference counts may never reach zero. A common practice is to use shared_ptr for the primary direction and weak_ptr for the reverse observation relationship.
#include <iostream>
#include <memory>
#include <string>
class Node
{
public:
std::string name;
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev;
explicit Node(const std::string& n) : name(n)
{
std::cout << "Node " << name << " created\n";
}
~Node()
{
std::cout << "Node " << name << " destroyed\n";
}
};
int main()
{
auto a = std::make_shared<Node>("A");
auto b = std::make_shared<Node>("B");
a->next = b; // A 拥有下一个节点
b->prev = a; // B 只观察前一个节点,不增加 A 的引用计数
std::cout << "a count = " << a.use_count() << "\n";
std::cout << "b count = " << b.use_count() << "\n";
if (auto p = b->prev.lock())
{
std::cout << "B prev is " << p->name << "\n";
}
return 0;
}
Results:
Node A created
Node B created
a count = 1
b count = 2
B prev is A
Node A destroyed
Node B destroyed
weak_ptr cannot directly access the object; you must first call lock(). If the object is still alive, lock() returns a temporary shared_ptr; if the object has already been released, it returns a null pointer.
Example 5: Don't overuse shared_ptr as function parameters
Function parameters should express true semantics:
- For temporary use of objects: use
T&orconst T&. - Functions might not have objects: use
T*orconst T*. - The function should take over the object: using
std::unique_ptr<T>. - Functions need to share objects: use
std::shared_ptr<T>.
#include <iostream>
#include <memory>
#include <string>
#include <utility>
class Robot
{
std::string name_;
public:
explicit Robot(std::string name) : name_(std::move(name)) {}
const std::string& name() const
{
return name_;
}
};
void print_robot(const Robot& robot)
{
std::cout << "borrow: " << robot.name() << "\n";
}
// 智能指针负责管理对象生命周期,减少手动释放资源的风险。
void take_robot(std::unique_ptr<Robot> robot)
{
std::cout << "take: " << robot->name() << "\n";
}
void share_robot(std::shared_ptr<Robot> robot)
{
std::cout << "share count inside = " << robot.use_count() << "\n";
}
int main()
{
// 程序从 main 函数开始执行,下面的语句会按顺序运行。
auto robot = std::make_unique<Robot>("R1");
print_robot(*robot);
take_robot(std::move(robot));
std::cout << "robot is null? " << (robot == nullptr ? "yes" : "no") << "\n";
auto shared_robot = std::make_shared<Robot>("R2");
std::cout << "share count before = " << shared_robot.use_count() << "\n";
share_robot(shared_robot);
std::cout << "share count after = " << shared_robot.use_count() << "\n";
return 0;
}
Results:
borrow: R1
take: R1
robot is null? yes
share count before = 1
share count inside = 2
share count after = 1
Key Grammar Explanation
| writing method | Meaning | Precautions |
|---|---|---|
std::make_unique<T>(...) | Create exclusive ownership objects | Available since C++14 |
std::move(p) | Transfer the ownership of unique_ptr. | The original pointer usually becomes null after being moved. |
std::make_shared<T>(...) | Create shared ownership objects | Reference counting has overhead. |
sp.use_count() | View shared reference count | For teaching and debugging purposes, but business logic should not depend on it. |
std::weak_ptr<T> | Weak reference, does not increment the reference count | Access requires lock() |
p.reset() | Release the currently managed object. | For shared_ptr, it decrements the reference count by one. |
Common Errors
- Treat
unique_ptras an ordinary pointer and copy it.unique_ptrcan only be moved, not copied. - The parameter is written as
shared_ptr, which is clearly just a borrowed object. This implies that the function also participates in shared ownership. - Create another
shared_ptrfrom a raw pointer ofshared_ptr. This results in two control blocks, which may eventually lead to duplicate releases. - Two objects hold references to each other using
shared_ptr, resulting in circular references. - Using smart pointers to manage stack objects is problematic. For instance, if the address of a local variable is passed to
unique_ptr, it will incorrectly deallocate the stack memory when leaving the scope.
使用建议
- 明确目标:在开始前确定您的具体需求,以便选择最合适的工具或教程。
- 充分利用资源:参考官方文档、教程和博客,这些资料能帮助您快速上手并解决问题。
- 实践应用:通过动手操作项目或编写代码来巩固学习成果,提升实际操作能力。
- 问题解决:遇到困难时,查阅参考资料或寻求社区支持,逐步培养独立解决问题的能力。
- 分享经验:完成项目后,可以撰写文章或博客分享心得,帮助其他学习者。
如果需要针对特定领域(如单片机、机器人或环境搭建)的进一步建议,请提供更多信息,我将为您细化内容。
- Avoid using pointers when possible; prioritize regular objects and references.
- When dynamic ownership is needed, prioritize using
unique_ptr. - Only use
shared_ptrwhen there are indeed multiple owners. - When only observing objects managed by
shared_ptr, useweak_ptr. - When creating smart pointers, prefer
make_uniqueandmake_shared. - Function parameters must convey semantics: borrowing, transferring, and sharing should not be mixed.
Summary
- Smart pointers are a typical application of RAII.
unique_ptrconveys exclusive ownership and is the default and preferred choice.shared_ptrexpresses shared ownership, don't overuse it just for convenience.weak_ptrdoes not own objects, and is often used to resolve circular references.- The most important thing about smart pointers isn't "automatic delete," but clearly defining the ownership relationships.