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.
这个例子表达了什么关系
这段代码可以看成一个只有两个节点的双向链表:
A ----next----> B
A <---prev----- B
虽然两个节点都能找到对方,但两个方向的所有权含义并不相同:
A通过next拥有B,因此next使用std::shared_ptr<Node>。
B通过prev观察A,但不负责延长A的生命周期,因此prev使用std::weak_ptr<Node>。
可以把它简单理解为:
shared_ptr表示“我拥有这个对象,它暂时不能被销毁”。
weak_ptr表示“我知道这个对象在哪里,但我不负责让它继续存活”。
引用计数是怎样变化的
刚创建两个节点时:
auto a = std::make_shared<Node>("A");
auto b = std::make_shared<Node>("B");
此时 a 拥有 A,b 拥有 B:
|对象|拥有它的 shared_ptr|引用计数|
|:---|:---|:---:|
|A|a|1|
|B|b|1|
执行:
a->next = b;
a->next 也是一个 shared_ptr,它和 b 共同拥有 B,所以 B 的引用计数由 1 变成 2。
接着执行:
b->prev = a;
b->prev 是 weak_ptr,只观察 A,不会增加引用计数。因此最终输出为:
a count = 1
b count = 2
对应关系是:
|对象|拥有它的 shared_ptr|引用计数|
|:---|:---|:---:|
|A|a|1|
|B|b、a->next|2|
为什么反向指针不能也使用 shared_ptr
假设把成员改成:
std::shared_ptr<Node> prev;
那么 A 通过 next 拥有 B,B 又通过 prev 拥有 A:
A ----shared_ptr----> B
A <---shared_ptr---- B
当 main() 结束时,局部变量 a 和 b 虽然会销毁,但两个节点内部仍然互相持有:
A 的引用计数至少为 1:来自 B.prev
B 的引用计数至少为 1:来自 A.next
它们的引用计数都无法降到 0,因此两个对象都不会析构,这就是 shared_ptr 的循环引用问题。
使用 weak_ptr 作为反向指针后,B.prev 不参与 A 的引用计数,循环所有权被打破,对象便可以正常释放。
lock() 的作用是什么
weak_ptr 不拥有对象,因此它不能保证自己观察的对象仍然存在。也正因为如此,不能直接写:
// 错误:weak_ptr 不支持直接使用 -> 访问对象
std::cout << b->prev->name << "\n";
访问前必须调用:
b->prev.lock()
lock() 会尝试根据 weak_ptr 得到一个 shared_ptr:
- 如果对象仍然存在,返回一个有效的
shared_ptr。
- 如果对象已经销毁,返回一个空的
shared_ptr。
因此原代码写成:
if (auto p = b->prev.lock())
{
std::cout << "B prev is " << p->name << "\n";
}
这段代码可以分成三步理解:
b->prev.lock()尝试获得指向A的shared_ptr。
- 返回值保存到局部变量
p中。
if检查p是否非空,只有对象仍然存在时才进入代码块。
在这个例子中,A 仍然由变量 a 管理,所以 lock() 成功,p 指向 A,可以安全访问:
p->name
在 p 存活期间,它也会暂时拥有 A,因此 A 的引用计数会临时从 1 变成 2。离开 if 代码块后,p 被销毁,引用计数再从 2 变回 1。
lock() 失败时会怎样
下面的例子中,A 被销毁以后,weak_ptr 本身仍然可以存在,但它已经处于过期状态:
std::weak_ptr<Node> observer;
{
auto owner = std::make_shared<Node>("A");
observer = owner;
} // owner 销毁,A 的引用计数变成 0,A 被释放
if (auto p = observer.lock())
{
std::cout << p->name << "\n";
}
else
{
std::cout << "observed object has been destroyed\n";
}
这时 observer.lock() 返回空的 shared_ptr,程序进入 else,不会访问已经销毁的对象。
也可以先使用 expired() 判断:
if (!observer.expired())
{
// 对象可能仍然存在
}
不过需要真正访问对象时,仍然应该直接使用 lock() 并检查返回值。因为从调用 expired() 到调用 lock() 之间,对象的生命周期仍可能发生变化;lock() 能把“检查对象是否存在”和“临时取得所有权”合并为一次安全操作。
程序结束时为什么两个节点都能析构
main() 结束时,局部变量按照创建顺序的相反方向销毁,因此先销毁 b,再销毁 a:
b销毁后,B的引用计数从 2 变成 1,因为a->next仍然拥有B。
a销毁后,A的引用计数从 1 变成 0,于是A被析构。
A被销毁时,它的成员next也随之销毁,B的引用计数从 1 变成 0。
- 最后
B被析构。
所以最终可以看到:
Node A destroyed
Node B destroyed
这里最关键的不是“前后指针必须分别使用哪一种智能指针”,而是要先判断所有权关系:真正负责对象生命周期的一方使用 shared_ptr,只负责观察的一方使用 weak_ptr。
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.