C++如何正确使用智能指针?看完这4个点你就明白了

C++11 中推出了三种智能指针 , unique_ptr、shared_ptr 和 weak_ptr , 同时也将 auto_ptr 置为废弃(deprecated) 。
但是在实际的使用过程中 , 很多人都会有这样的问题:

  1. 不知道三种智能指针的具体使用场景
  2. 无脑只使用 shared_ptr
  3. 认为应该禁用 raw pointer(裸指针 , 即 Widget*这种形式) , 全部使用智能指针
本文试图理清楚三种智能指针的具体使用场景 , 并讲解三种智能指针背后的性能消耗 。
C++如何正确使用智能指针?看完这4个点你就明白了

文章插图
C/C++
对象所有权首先需要理清楚的概念就是对象所有权的概念 。所有权在 rust 语言中非常严格 , 写 rust 的时候必须要清楚自己创建的每个对象的所有权 。
但是 C++比较自由 , 似乎我们不需要明白对象的所有权 , 写的代码也能正常运行 。但是明白了对象所有权 , 我们才可以正确管理好对象生命周期和内存问题 。
C++引入了智能指针 , 也是为了更好的描述对象所有权 , 简化内存管理 , 从而大大减少我们 C++内存管理方面的犯错机会 。
unique_ptr:专属所有权我们大多数场景下用到的应该都是 unique_ptr 。unique_ptr 代表的是专属所有权 , 即由 unique_ptr 管理的内存 , 只能被一个对象持有 。所以 , unique_ptr 不支持复制和赋值 , 如下:
auto w = std::make_unique<Widget>();auto w2 = w; // 编译错误如果想要把 w 复制给 w2, 是不可以的 。因为复制从语义上来说 , 两个对象将共享同一块内存 。
因此 , unique_ptr 只支持移动, 即如下:
auto w = std::make_unique<Widget>();auto w2 = std::move(w); // w2获得内存所有权 , w此时等于nullptrunique_ptr 代表的是专属所有权 , 如果想要把一个 unique_ptr 的内存交给另外一个 unique_ptr 对象管理 。只能使用 std::move 转移当前对象的所有权 。转移之后 , 当前对象不再持有此内存 , 新的对象将获得专属所有权 。
如上代码中 , 将 w 对象的所有权转移给 w2 后 , w 此时等于 nullptr , 而 w2 获得了专属所有权 。
 
性能
因为 C++的 zero cost abstraction 的特点 , unique_ptr 在默认情况下和裸指针的大小是一样的 。所以内存上没有任何的额外消耗 , 性能是最优的 。
使用场景 1:忘记 delete
unique_ptr 一个最简单的使用场景是用于类属性 。代码如下:
class Box{public: Box() : w(new Widget()) {} ~Box() { // 忘记delete w }private: Widget* w;};如果因为一些原因 , w 必须建立在堆上 。如果用裸指针管理 w , 那么需要在析构函数中delete w; 这种写法虽然没什么问题 , 但是容易漏写 delete 语句 , 造成内存泄漏 。
如果按照 unique_ptr 的写法 , 不用在析构函数手动 delete 属性 , 当对象析构时 , 属性w将会自动释放内存 。
使用场景 2:异常安全
假如我们在一段代码中 , 需要创建一个对象 , 处理一些事情后返回 , 返回之前将对象销毁 , 如下所示:
void process(){ Widget* w = new Widget(); w->do_something(); // 可能会发生异常 delete w;}在正常流程下 , 我们会在函数末尾 delete 创建的对象 w , 正常调用析构函数 , 释放内存 。
但是如果 w->do_something()发生了异常 , 那么delete w将不会被执行 。此时就会发生内存泄漏 。我们当然可以使用 try...catch 捕捉异常 , 在 catch 里面执行 delete , 但是这样代码上并不美观 , 也容易漏写 。
如果我们用 std::unique_ptr , 那么这个问题就迎刃而解了 。无论代码怎么抛异常 , 在 unique_ptr 离开函数作用域的时候 , 内存就将会自动释放 。
shared_ptr:共享所有权在使用 shared_ptr 之前应该考虑 , 是否真的需要使用 shared_ptr, 而非 unique_ptr 。
shared_ptr 代表的是共享所有权 , 即多个 shared_ptr 可以共享同一块内存 。因此 , 从语义上来看 , shared_ptr 是支持复制的 。如下:
auto w = std::make_shared<Widget>();{ auto w2 = w; cout << w.use_count() << endl; // 2}cout << w.use_count() << endl; // 1shared_ptr 内部是利用引用计数来实现内存的自动管理 , 每当复制一个 shared_ptr , 引用计数会+1 。当一个 shared_ptr 离开作用域时 , 引用计数会-1 。当引用计数为 0 的时候 , 则 delete 内存 。


推荐阅读