Menu

C++ 智能指针详解:unique_ptr 与 shared_ptr

智能指针拥有堆内存,并会自动释放它。学习 unique_ptr、shared_ptr、make_unique 和 make_shared,以及为什么你几乎再也不需要写 new/delete。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

智能指针要解决的问题

在上一页中,你用 new 分配内存,再用 delete 释放它。这样做可行,但担子全落在你身上:每个 new 都需要一个对应的 delete,在每一条代码路径上都要有,包括中途抛出异常的那些路径。漏掉一个就会内存泄漏;delete 两次就会破坏堆。

智能指针的解决办法,是把堆内存的生命周期绑定到栈上的一个普通对象。当这个对象离开作用域时,它的析构函数会替你执行 delete,而且有保证,即使异常导致栈展开也一样。这个思想叫做 RAII(资源获取即初始化),智能指针位于头文件 <memory> 中。

*p 和 p->member 的用法和裸指针完全一样。区别在于你永远不用调用 delete:智能指针会替你去做。

unique_ptr:一个所有者,不能共享

unique_ptr 是你默认就该选用的智能指针。它代表独占所有权:同一时刻恰好只有一个 unique_ptr 拥有这个对象,这个指针销毁时,对象也随之销毁。与裸指针相比,它没有任何运行时开销。

用 make_unique(C++14)创建它。它接收构造函数的参数,交给你一个可以直接使用的指针:

因为只能有一个所有者,unique_ptr 不能被拷贝。试图拷贝它会导致编译错误,而这个错误正是语言在保护你,防止两个所有者都去 delete 同一个对象:

auto a = make_unique<int>(10);
auto b = a;   // error: call to deleted copy constructor of unique_ptr

要把所有权交给别人,就用 std::move 移动它。移动之后,原来的指针变为空(它持有 nullptr):

这就是你在大多数时候想要的模型:始终有且只有一个明确的所有者,并由编译器来保证。

shared_ptr:靠引用计数共享所有权

有时程序中的好几个部分确实需要共享同一个对象,而谁也不知道哪一个最后用完。这正是 shared_ptr 的用途。它维护一个引用计数:每次拷贝计数加一,每次销毁计数减一,只有计数归零时对象才会被释放。

用 make_shared 创建它们:

和 unique_ptr 不同,拷贝 shared_ptr 完全没问题,这正是它存在的意义。代价在于开销:引用计数存放在堆上,并以原子方式更新(线程安全),所以 shared_ptr 比 unique_ptr 更重。只有在所有权确实需要共享时才用它,而不是为了省去思考谁拥有什么。

make_shared 也比 shared_ptr<T>(new T(...)) 更高效:它在一次分配中同时分配对象和控制块,而不是两次。

weak_ptr 与打破循环引用

shared_ptr 有一个经典陷阱:如果两个对象互相持有指向对方的 shared_ptr,它们的引用计数永远不会归零,谁也不会被释放。即使用了智能指针,还是会内存泄漏。

struct Node {
    shared_ptr<Node> next;   // if two nodes point at each other,
};                           // they keep each other alive forever

解决办法是 weak_ptr:一个不拥有对象的 shared_ptr 观察者。它不会增加引用计数,所以永远不会让对象一直存活。要使用对象时,调用 .lock():如果对象还在,它会给你一个 shared_ptr;如果对象已经没了,就给你一个空的。

“反向指针”和缓存适合用 weak_ptr:凡是你想引用一个对象、又不想声明拥有它的地方都可以用。

常见错误与陷阱

智能指针消除了大多数内存 bug,但仍有几个陷阱:

不要让智能指针和裸指针同时拥有同一块内存。 永远不要用同一个裸指针构造两个智能指针,它们都会试图 delete 它:

int* raw = new int(5);
unique_ptr<int> a(raw);
unique_ptr<int> b(raw);   // disaster: both will delete the same int (double free)

这正是你应该优先使用 make_unique/make_shared 的原因:没有游离在外、可能被误用的裸指针。

unique_ptr 只能移动,所以要转移所有权就按值传递它。 如果一个函数只应该使用对象而不拥有它,就改为接收普通引用或裸 T*:一个只负责观察的裸指针完全没问题:

void consume(unique_ptr<int> p);   // takes ownership (move into it)
void observe(int* p);              // just looks, owns nothing

不要默认就用 shared_ptr。 它能随意拷贝,所以很诱人,但原子引用计数会带来实实在在的性能开销,而且共享所有权让生命周期更难推断。默认用 unique_ptr,只有真正需要多个所有者时才升级为 shared_ptr。

用于数组的 unique_ptr 需要数组形式。 make_unique<int[]>(n) 会给你一个 unique_ptr<int[]>,它会正确地调用 delete[]。实际开发中,动态数组优先用 std::vector:它替你管理内存,还额外记录了大小。

下一节:字符串

现在你已经掌控了内存管理:智能指针让你在堆上分配内存,又不会泄漏。你最常分配和传来传去的东西之一就是文本,而 C++ 为此提供了比裸 char* 缓冲区安全得多的工具。下一页讲解 std::string:它如何自动增长,你每天都会用到的操作,以及为什么它能让你彻底摆脱手动管理内存。

常见问题

C++ 中的智能指针是什么?

智能指针是 <memory> 中的一类对象(unique_ptr、shared_ptr、weak_ptr),它们包装一个裸指针,并在离开作用域时自动 delete 那块内存。有了它们,你可以在堆上分配内存,而不必手动 delete,也不会因为忘记释放而造成内存泄漏。

unique_ptr 和 shared_ptr 有什么区别?

unique_ptr 是对象唯一的所有者:它不能被拷贝,只能被移动,并且在自己销毁的那一刻释放内存。shared_ptr 通过引用计数实现共享所有权:多个 shared_ptr 可以指向同一个对象,只有最后一个被销毁时对象才会被释放。除非你确实需要共享所有权,否则优先用 unique_ptr。

现代 C++ 中应该用 make_unique 还是 new?

用 make_unique 和 make_shared。它们一步完成对象的分配和包装,所以不存在一个在交给智能指针之前就可能泄漏的裸 new 结果。按照经验法则,现代 C++ 代码库中应该几乎看不到裸的 new 或 delete。

Coddy 编程语言插图

和 Coddy 一起学编程

开始