智能指针与所有权关系:谁拥有,谁只是看 / Smart Pointers and Ownership Graphs
智能指针不是“更聪明的指针”这么简单。
它们真正解决的是所有权表达:谁负责释放对象,谁只是临时观察对象。
1. 裸指针的问题是语义不够
看接口:
void process(Widget* widget);单看这个函数声明,你不知道:
widget 能不能是 nullptr?
process 会不会 delete widget?
process 会不会把 widget 保存起来?
调用者用完后还要不要释放?裸指针能表达“一个地址”。
但它不能自动表达“谁拥有这个地址上的对象”。
智能指针的价值在于把常见所有权关系写进类型。
std::unique_ptr<T>
独占拥有
std::shared_ptr<T>
共享拥有
std::weak_ptr<T>
观察 shared_ptr 管理的对象,不参与拥有2. unique_ptr:一个对象,一个明确拥有者
最常用的智能指针是 std::unique_ptr。
#include <memory>
#include <string>
int main() {
auto name = std::make_unique<std::string>("Alice");
}这里的关系是:
name
|
v
std::string objectname 离开作用域时,unique_ptr 析构。
它会释放自己拥有的 std::string。
unique_ptr 不能复制:
auto a = std::make_unique<int>(42);
// auto b = a; // 错因为复制会造成两个 owner 都以为自己拥有同一个对象。
它可以移动:
auto a = std::make_unique<int>(42);
auto b = std::move(a);移动后的所有权关系:
before
a ---> int(42)
after
a ---> null
b ---> int(42)a 仍然是有效的 unique_ptr 对象。
但它不再拥有那个 int。
3. 函数参数怎样表达 unique ownership
借用对象:
void print(const Widget& widget);可能为空的借用:
void print(const Widget* widget);接管所有权:
void install(std::unique_ptr<Widget> widget);创建并返回所有权:
std::unique_ptr<Widget> create_widget() {
return std::make_unique<Widget>();
}这比裸指针更清楚。
参数是 unique_ptr
函数获得所有权
返回 unique_ptr
调用者获得所有权
参数是引用或裸指针
通常只是借用,除非接口文档另有说明4. shared_ptr:共享拥有
有时对象确实需要多个 owner。
比如多个任务共享同一份配置对象:
#include <memory>
#include <string>
#include <vector>
struct Config {
std::string path;
};
int main() {
auto config = std::make_shared<Config>();
std::shared_ptr<Config> a = config;
std::shared_ptr<Config> b = config;
}关系:
config ----+
a ---------+----> control block ----> Config object
b ---------+shared_ptr 通常有一个控制块。
控制块里会记录:
有多少 shared_ptr 正在拥有对象
有多少 weak_ptr 正在观察控制块
如何删除对象当最后一个 shared_ptr 离开时,对象会被销毁。
5. shared_ptr 的代价
shared_ptr 很方便,但不是默认答案。
它的成本包括:
需要维护引用计数
复制 shared_ptr 会增加计数
销毁 shared_ptr 会减少计数
多线程环境下计数更新通常需要原子操作
所有权关系可能变得不清楚
循环引用会导致泄漏所以优先顺序通常是:
能用局部对象,就用局部对象
能用容器拥有,就用容器
需要独占动态对象,用 unique_ptr
确实需要共享生命周期,再用 shared_ptr6. 循环引用为什么会泄漏
看一个双向关系:
#include <memory>
struct Node {
std::shared_ptr<Node> next;
std::shared_ptr<Node> prev;
};
int main() {
auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b;
b->prev = a;
}离开 main() 时,局部变量 a 和 b 会销毁。
但对象内部还有彼此的 shared_ptr。
a object --shared--> b object
^ |
| |
+------shared------+引用计数不会降到 0。
两个对象互相把对方“留住”。
这就是循环引用。
7. weak_ptr:观察但不拥有
解决上面的双向关系,通常让一边不拥有:
#include <memory>
struct Node {
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev;
};weak_ptr 不增加 shared owner 计数。
使用时需要先 lock():
if (auto p = node.prev.lock()) {
// p 是 shared_ptr,说明对象还活着
}为什么要 lock()?
因为 weak_ptr 只是观察。
它观察的对象可能已经被最后一个 shared_ptr 销毁。
lock() 的含义是:
如果对象还活着,就临时拿到一个 shared_ptr。
如果对象已经没了,就得到空 shared_ptr。8. make_unique 和 make_shared
推荐:
auto p = std::make_unique<Widget>();
auto q = std::make_shared<Widget>();而不是:
std::unique_ptr<Widget> p(new Widget);
std::shared_ptr<Widget> q(new Widget);原因包括:
代码更短,所有权更清楚
减少手写裸 new
make_shared 通常能把对象和控制块一起分配,效率更好
异常安全更容易保证但 make_shared 也不是永远最好。
如果对象很大,且还有 weak_ptr 长时间存在,对象存储可能因为控制块还在而延迟释放。
普通学习阶段先记住:
默认用 make_unique。
需要共享所有权时用 make_shared。9. 本篇总结
智能指针要按所有权选择:
unique_ptr
一个明确 owner,最常用
shared_ptr
多个 owner 共同决定生命周期
weak_ptr
观察 shared_ptr 管理的对象,打破环
raw pointer / reference
借用对象,不负责释放不要把 shared_ptr 当成“保险起见”。
它表达的是共享生命周期。
如果代码只是临时使用对象,引用或裸指针反而更准确。