[读书笔记]《Effective Modern C++》—— 智能指针

前言

大部分同学可能都可以熟练知道,智能指针是管理内存的一种有效手段,shared_ptr 是通过引用计数来管理内存,当引用计数为 0 的时候内存就会自动释放,weak_ptr 则是为了解决 shared_ptr 可能会出现的循环引用的问题出现,unique_ptr 则是有独占的概念的智能指针。

那概念上可能就是上面的概括,继续追问一句,那什么时候应该使用 unique_ptr,什么时候应该使用 shared_ptr,为什么? 没有在实战中真的去留意过这个问题,可能就会有点难以回答,除了对八股文有一些了解,更深入学习背后的内容,多少会对理解它们在实战中的使用场景有所帮助。

下面的内容主要是摘录整理自 《Effective Modern C++》,有需要的同学推荐直接去阅读原书的相关章节。

首先要了解一点,智能指针就是对普通指针的一个封装,相当于一个模板类。

系列推荐阅读:
[读书笔记]《Effective Modern C++》—— 类型推导、auto、decltype
[读书笔记]《Effective Modern C++》—— 移步现代 C++

std::unique_ptr

std::unique_ptr 实现独占的这么概念主要就是相关的类实现中,删除了类的赋值以及拷贝构造函数,只实现了移动语义。

// 示例代码
unique_ptr(unique_ptr const&) = delete;
unique_ptr& operator=(unique_ptr const&) = delete;

_LIBCPP_INLINE_VISIBILITY
~unique_ptr() { reset(); }

效率方面,如果不自定义删除器,std::unique_ptr 的内存和速度基本与原始指针是一致的,因为删除器是 unique_ptr 模板类实例化的一部分,所以自定义删除器可能会多一个指向删除器的指针(使用 lambda 表达式则不会带来额外的内存负担)。

使用方面,根据书中的描述,std::unique_ptr 体现了专有权语义,其常用作继承层次结构中对象的工厂函数返回类型,因为调用方会在对上分配一个对象然后返回指针,调用方在不需要的时候有责任销毁对象,当调用者需要对返回的资源负责(即对该资源的专有所有权),并且 std::unique_ptr 在自己被销毁时会自动销毁指向的内容。
除了上面说的工厂函数,还有一种 pimpl 的机制(point to implementation,一种隐藏实际实现而减弱编译依赖的设计思想),这个放在最后介绍。

下面是 std::unique_ptr 自定义删除器的用法示例,这里不过多展开。

auto delInvmt = [](Investment* pInvestment)         //自定义删除器
                {                                   //(lambda表达式)
                    makeLogEntry(pInvestment);
                    delete pInvestment; 
                };

template<typename... Ts>
std::unique_ptr<Investment, decltype(delInvmt)>     //更改后的返回类型
makeInvestment(Ts&&... params)
{
    std::unique_ptr<Investment, decltype(delInvmt)> //应返回的指针
        pInv(nullptr, delInvmt);
    if (/*一个Stock对象应被创建*/)
    {
        pInv.reset(new Stock(std::forward<Ts>(params)...));
    }
    else if ( /*一个Bond对象应被创建*/ )   
    {     
        pInv.reset(new Bond(std::forward<Ts>(params)...));   
    }   
    else if ( /*一个RealEstate对象应被创建*/ )   
    {     
        pInv.reset(new RealEstate(std::forward<Ts>(params)...));   
    }   
    return pInv;
}

std::unique_ptr 除了用于单个对象,同样可以用于数组(其他智能指针就没有相关支持数组的实现)。并且可以很方便的转化成 std::shared_ptr。


template<typename... Ts>            //返回指向对象的std::unique_ptr,
std::unique_ptr<Investment>  makeInvestment(Ts&&... params); //对象使用给定实参创建

std::shared_ptr<Investment> sp = makeInvestment(arguments); //将std::unique_ptr转为std::shared_ptr
  • std::unique_ptr 是轻量级、快速、只可移动地管理专有权语义资源的智能指针
  • 默认删除资源通过 delete 实现,支持自定义删除器(可能会影响 std::unique_ptr 对象大小)
  • 转换成 shared_ptr 比较方便

std::shared_ptr

首先先抛出一个结论,std::shared_ptr 对象大小一般是原始对象的两倍。一份大小就是传入的原始指针的大小,还有一个指针大小是用来指向控制块的(类似于虚函数表指针的形式),这个控制块是另一块内存,里面存储了计数使用的相关变量,还可能有一些用户自定义的删除器,空间配置器的地址信息。

在这里插入图片描述

上面控制块的创建,有下面 3 种情况:

  • std::make_shared 创建 std::shared_ptr 对象时总会创建一个控制块,它创建一个要指向的新对象,所以可以肯定 std::make_shared 调用时对象不存在其他控制块。
  • 从独占指针(std::unique_ptr 或者 std::auto_ptr)上构造 std::shared_ptr 对象时会创建控制块,因为独占指针没有控制块,所以需要创建一个。
// 示例代码
template<class _Tp>
template <class _Yp, class _Dp>
shared_ptr<_Tp>::shared_ptr(unique_ptr<_Yp, _Dp>&& __r,
                            typename enable_if
                            <
                                !is_lvalue_reference<_Dp>::value &&
                                is_convertible<typename unique_ptr<_Yp, _Dp>::pointer, element_type*>::value,
                                __nat
                            >::type)
    : __ptr_(__r.get())  // 初始化原始指针
{
#if _LIBCPP_STD_VER > 11
    if (__ptr_ == nullptr)
        __cntrl_ = nullptr;
    else
#endif
    { // 额外创建需要的控制块
        typedef typename __shared_ptr_default_allocator<_Yp>::type _AllocT;
        typedef __shared_ptr_pointer<typename unique_ptr<_Yp, _Dp>::pointer, _Dp, _AllocT > _CntrlBlk;
        __cntrl_ = new _CntrlBlk(__r.get(), __r.get_deleter(), _AllocT());
        __enable_weak_this(__r.get(), __r.get());
    }
    __r.release();
}
  • 从原始指针上创建std::shared_ptr 对象时会创建控制块。使用 std::shared_ptr 或者 std::weak_ptr 创建 std::shared_ptr 对象时不会创建控制块。
// 示例代码
template<class _Tp>
inline
shared_ptr<_Tp>::shared_ptr(const shared_ptr& __r) _NOEXCEPT
    : __ptr_(__r.__ptr_),
      __cntrl_(__r.__cntrl_) // 因为本身就有,直接赋值,不会创建新的
{
    if (__cntrl_)
        __cntrl_->__add_shared();
}

因为原始指针直接创建时会创建新的控制块,所以下面的用法就是一个错误的示范:

auto ptr = new Widget;  // ptr 是原始指针
std::shared_ptr<Widget> spw1(ptr); // 为 ptr 创建第一个控制块
std::shared_ptr<Widget> spw2(ptr); // 为 ptr 创建第二个控制块

上面多个控制块意味着多个引用计数值,多个引用计数就意味着对象会被销毁多次。

建议的情况就是使用 std::make_shared 创建,或者由 std::shared_ptr 多次创建。

还有一种情况就是在类中需要往容器中添加 this 指针,因为一个类对象的 this 指针就只有一个,可能出现给 this 指针创建多个控制块的情况。这里需要使用 std::enable_shared_from_this 这个基类模板,其中定义了一个成员函数 shared_from_this(), 可以保证创建指向当前 this 的 shared_ptr 对象并不会创建多余的控制块,当想在成员函数中使用 std::shared_ptr 指向 this 所指对象时都可以使用它。

错误使用:

class Widget {
public:
    …
    void process();
    …
    std::vector<std::shared_ptr<Widget>> processedWidgets;
};

void Widget::process()
{
    …                                       //处理Widget
    processedWidgets.emplace_back(this);    //然后将它加到已处理过的Widget
}                                         //的列表中,这是错的!

正确用法:

class Widget: public std::enable_shared_from_this<Widget> {
public:
    …
    void process();
    …
};

void Widget::process()
{
    … 
    //把指向当前对象的std::shared_ptr加入processedWidgets
    processedWidgets.emplace_back(shared_from_this());
}

以上主要是说明在使用 std::shared_ptr 的时候要注意不要创建多个控制块。有这么多注意事项,并且大小还是原指针的两倍,听着好像 std::shared_ptr 的使用稍高,作为这些轻微开销的交换,你可以得到动态分配的资源的生命周期自动管理的好处,大多数时候,比起手动管理,其管理共享型资源还是比较合适的。如果独占型资源可行或者可能可行,还是优先推荐使用 unique_ptr, 并且 unique_ptr 转 shared_ptr 也很方便。

  • std::shared_ptr 为共享所有权的任意资源提供一种自动垃圾回收的机制
  • 相较于 std::unique_ptr, std::shared_ptr 对象通常大两倍,控制块会产生开销,需要原子性的引用计数修改操作
  • 默认删除资源是 delete, 支持自定义删除器,删除器的类型不会影响大小
  • 避免直接原始指针变量上创建 std::shared_ptr

std::weak_ptr

weak_ptr 不是一个独立的智能指针,通常都从 share_ptr 上创建,创建时 share_ptr 与 weak_ptr 指向相同的对象,但是 std::weak_ptr 不会影响所指对象的引用计数。

auto spw =                      //spw创建之后,指向的Widget的
    std::make_shared<Widget>(); //引用计数(ref count,RC)为1。

…
std::weak_ptr<Widget> wpw(spw); //wpw指向与spw所指相同的Widget。RC仍为1
…
spw = nullptr;                  //RC变为0,Widget被销毁。wpw现在悬空

如果想判断一个 weak_ptr 是否已经过期有一下三种方式:

// 方式1:直接调用 expired 函数,线程不安全
if(wpw.expired())  

// 方式2:使用 lock 函数,创建一个 shared_ptr 然后判空,有原子性,线程安全
std::shared_ptr<Widget> spw1 = wpw.lock();  //如果wpw过期,spw1就为空

// 方式3:直接初始化一个 shared_ptr
std::shared_ptr<Widget> spw3(wpw);          //如果wpw过期,抛出std::bad_weak_ptr异常

以上是对 weak_ptr 功能及用法的一些介绍。可能的适用场景主要包括:

  • 打破 std::shared_ptr 的循环引用,因为weak_ptr 不会引起计数变化。
  • 观察者模式中的观察者列表,因为当一个观察者销毁时,消息产生者要不再使用,所以可以让消息产生者持有一个 std::weak_ptr 的容器指向观察者,这样可以在使用前检查是否悬空。
  • 缓存:主要是在可缓存对象中,调用者应当接受缓存对象的智能指针,并且需要知道缓存对象是否悬空,悬空则销毁。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Coming to grips with C++11 and C++14 is more than a matter of familiarizing yourself with the features they introduce (e.g., auto type declarations, move semantics, lambda expressions, and concurrency support). The challenge is learning to use those features effectively—so that your software is correct, efficient, maintainable, and portable. That’s where this practical book comes in. It describes how to write truly great software using C++11 and C++14—i.e. using modern C++. Topics include: The pros and cons of braced initialization, noexcept specifications, perfect forwarding, and smart pointer make functions The relationships among std::move, std::forward, rvalue references, and universal references Techniques for writing clear, correct, effective lambda expressions How std::atomic differs from volatile, how each should be used, and how they relate to C++'s concurrency API How best practices in "old" C++ programming (i.e., C++98) require revision for software development in modern C++ Effective Modern C++ follows the proven guideline-based, example-driven format of Scott Meyers' earlier books, but covers entirely new material. "After I learned the C++ basics, I then learned how to use C++ in production code from Meyer's series of Effective C++ books. Effective Modern C++ is the most important how-to book for advice on key guidelines, styles, and idioms to use modern C++ effectively and well. Don't own it yet? Buy this one. Now". -- Herb Sutter, Chair of ISO C++ Standards Committee and C++ Software Architect at Microsoft

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值