前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >智能指针

智能指针

作者头像
JIFF
发布2019-08-02 15:24:43
1.1K0
发布2019-08-02 15:24:43
举报
文章被收录于专栏:Toddler的笔记Toddler的笔记
  • 大纲
    • std::shared_ptr
    • std::weak_ptr
    • std::unique_ptr
    • std::enable_shared_from_this
    • std::make_shared
    • 总结: shared_ptr 使用注意事项

刚学编程时,最常听到的一句话是不是“new 的内存用完要记得 delete,不然会造成内存泄漏”?然而事实上是:

a solution involving the phrase "just remember to" is seldom the best solution. Sometimes you won't remember.

std::shared_ptr

shared_ptr 是一个 pointer wrapper,其实就是个模板类。它可以管理 raw pointer,最后自动释放内存。

代码语言:javascript
复制
template<typename T>
class shared_ptr;
  • 以下 ptr 表示 ptr = new T; s 表示 shared_ptr<T> s(ptr);
  • shared_ptr 定义了 operator *() 运算符, 从而用户可以使用 *s 语法。
  • shared_ptr 定义了 operator ->() 运算符, 从而用户可以使用 s->val 语法。
  • 构造函数
    • 其构造函数接受一个 T* ptr 参数,表示接管其生命周期,同时 new 一个 bookkeeping area (alloc 是有可能导致异常的,因此该 ctor 并不保证 noexcept)。最后其析构函数负责"释放"其内存,即默认的 Deleter 调用 operator delete().
    • 默认构造函数,shared_ptr<T> x; 不会 new bookkeeping area(或者称为 control block),因此此函数是保证 noexcept 的。
    • 复制构造也是保证不会 throw 的。因为 bookkeeping area 是共用的,这里不会再 new
    • 其构造函数还可以接受一个 Deleter 和 Allocator(从而可以和 placement new 配合),管理所有类型的 storage duration
  • s.reset(); 让 s 不再管理任何 ptr,并且,如果此时 use_count 为 1,(且 weak_count 为 1),则 Delete 其管理的 ptr。
    • shared_ptr 的构造会让 use_count 和 weak_count 同时加 1,而 weak_ptr 的构造只会让 weak_count 加 1.
  • s.reset(new T(..)); 相当于先调用上面的 .reset(), 再换而管理另外一个 ptr2
    • 需要用 s.reset(ptr2); 来实现这种语义,其实现为 shared_ptr(ptr2).swap(*this);
    • 或者用 s = shared_ptr<T>(ptr2); 这是创建了另外一个 shared_ptr,然后进行 move 和 swap。换言之,该 operator=() 的实现方式为 shared_ptr<T>(std::move(r)).swap(*this)。其中 r 为 rhs(等号的右边 right-hand-side)
    • 注意如果因误操作对原来管理的 ptr 执行这种操作 s.reset(ptr); 或 s = shared_ptr<T>(ptr);,最后在 s 析构时,ptr 会再次被 delete (this is an Undefined Bahaviour, may or may not corrupt the program)
    • 实际上的实现是 shared_ptr(__p).swap(*this);,其中 __p = new T(..)。退出 scope 时临时 rvalue shared_ptr (通过 swap 接管了原来的 ptr) 被析构,因此 ptr 被 delete
    • 从 raw pointer 到 shared_ptr 的构造函数是 explicit 的,意味着 s = ptr2 这种语法非法

std::weak_ptr

  • 以下 w 表示 weak_ptr<T> w;
  • weak_ptr<T> w(s): 使 w 成为 s 的 owning group 的一员。
  • .lock(): Returns a shared_ptr with the information preserved by the weak_ptr object if it is not expired.
    • 例如,如果只存在一个 s,并发生了 s 过期,或者 s.reset(ptr2),甚至 s.reset(ptr),都认为 object 过期。w 的 bka 共用原 s 的 bka,而原 s 的 bka 显示的 use_count 为 0,因此 lock 会失败,返回一个 default-constructed shared_ptr。
    • 对于 shared_ptr,如果有 alive 的 weak_ptr 指向了它,则当 s 过期时,其 bka 并不立马析构,而是会等到 w 的析构再一起析构。
    • 其实现源代码见 bits/shared_ptr_base.h _Sp_counted_base::_M_release
    • This operation is executed atomically.
    • 如果 weak_ptr 关联的 bookkeeping area(bka) 已经过期了,即 s 曾为 ptr 建立的 bka 过期了,则返回 empty shared_ptr
    • 如果没有过期,则返回一个 shared_ptr(意味着增加了引用计数),防止被 release

std::unique_ptr

  • 以下 sp 表示 shared_ptr<T> sp; up 表示 unique_ptr<T> up;
  • unique_ptr 在 boost 中叫做 scoped_ptr
  • 先看个对比:
    • shared_ptr sp(...); sp = sp2; 此时 sp 先前所管理的 ptr 引用计数减一,转而管理 sp2
    • unique_ptr up(...); up = up2; 编译违法,unique_ptr 只接受 move ctor (即 move constructor) 和 move = (即 move assignment operator)
    • shared_ptr 的 ctor 接受 右值 unique_ptr,即 shared_ptr ps(move(up)); 然后 up 失去对 ptr 的 ownership,成为一个 empty unique_ptr。
  • shared_ptr 从C++17开始可以正确处理new[]了,在此之前需要手动更改deleter()函数为delete[]
    • 例如在 17 以前需要 shared_ptr sp(new T[3], [](T* p){ delete[] p; });,否则 sp 析构时会默认用 delete p,而不是 delete[] p,导致问题
    • 而从 17 开始可以 shared_ptr sp(new T[3]);,sp 析构时会正确调用 delete[] p;
  • 而 unique_ptr 从 C++11 开始一直都可以正确处理 new[]

std::enable_shared_from_this

  • 如果类的方法想返回指向 this 的 shared_ptr,使用return std::shared_ptr<T>(this);是错误的。这样每次会返回一个新的 shared_ptr,如果前面就存在一个指向该 object 的 shared_ptr,那么最后该 object 会被析构两次。
  • 因此,public 继承 std::enale_shared_from_this 就可以解决这个问题,使用return shared_from_this();就可以返回一个和前面已经存在的 shared_ptr 管理同一个引用计数的 shared_ptr
  • 语法:class **T** : public std::enable_shared_from_this<**T**> { .. };

std::make_shared

shared_ptr<T> x(new T(args...));shared_ptr<T> x = make_shared(args...); 的区别:

  • shared_ptr<T> x(new T(args...)); 会有两次 heap allocation:第一次是 new T,第二次是 shared_ptr 内部的 control block allocation
  • shared_ptr<T> x = make_shared(args...); 中 new T 和 new control block 是在一个函数中完成的。引用 cppreference 的内容: The object is constructed as if by the expression ::new (pv) T(std::forward<Args>(args)...), where pv is an internal void* pointer to storage suitable to hold an object of type T. The storage is typically larger than sizeof(T) in order to use one allocation for both the control block of the shared pointer and the T object.
  • 为什么推荐使用 make_shared 而不推荐 shared_ptr x(new T(args...))? Consider this example,
代码语言:javascript
复制
F(std::shared_ptr<Lhs>(new Lhs("foo")), std::shared_ptr<Rhs>(new Rhs("bar")));

Because C++ allows arbitrary order of evaluation of subexpressions, one possible ordering is:

代码语言:javascript
复制
new Lhs("foo"))
new Rhs("bar"))
std::shared_ptr<Lhs>
std::shared_ptr<Rhs>

Now, suppose we get an exception thrown at step 2 (e.g., out of memory exception, Rhs constructor threw some exception). We then lose memory allocated at step 1, since nothing will have had a chance to clean it up. The core of the problem here is that the raw pointer didn't get passed to the std::shared_ptr constructor immediately.

因此,一般如果已经提供了构造函数参数的,一般会优先选用 make_shared,make_unique,emplace 等操作执行。

总结: shared_ptr 使用注意事项

  • a single std::shared_ptr instance is not thread safe.
    • A shared pointer is a pair of two pointers, one to the object and one to a control block. If you assign a new object to a std::shared_ptr while another thread uses it, it might end up with the new object pointer but still using a pointer to the control block of the old object => CRASH.
  • 多个 shared_ptr 是线程安全的。
    • the reference counters are typically incremented using an equivalent of std::atomic::fetch_add with std::memory_order_relaxed.
    • decrementing requires stronger ordering to safely destroy the control block
  • 不要把一个原生指针给多个 shared_ptr 管理
  • 不要把 this 指针给 shared_ptr
    • 解决方法: 用 shared_from_this
  • 当心循环引用。即类A有 shared_ptr<B> member,而类B有 shared_ptr<A> member
    • 解决方法: 应当至少使用一个 weak_ptr,具体情况具体分析。
  • 在函数实参列表中使用时的问题:fun(shared_ptr<T>(new T), g); 有可能先 new T,再调用 g(),而一旦 g() 异常,则 new T 的内存泄露。
    • 解决方法: 用 make_shared
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-06-08,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Toddler的笔记 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • std::shared_ptr
  • std::weak_ptr
  • std::unique_ptr
  • std::enable_shared_from_this
  • std::make_shared
  • 总结: shared_ptr 使用注意事项
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档