智能指针要解决的问题
在上一页中,你用 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。