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

文章目录

      • 前言
      • std::unique_ptr
      • std::shared_ptr
      • std::weak_ptr

前言

大部分同学可能都可以熟练知道,智能指针是管理内存的一种有效手段,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
std::unique_ptr     //更改后的返回类型
makeInvestment(Ts&&... params)
{
    std::unique_ptr //应返回的指针
        pInv(nullptr, delInvmt);
    if (/*一个Stock对象应被创建*/)
    {
        pInv.reset(new Stock(std::forward(params)...));
    }
    else if ( /*一个Bond对象应被创建*/ )   
    {     
        pInv.reset(new Bond(std::forward(params)...));   
    }   
    else if ( /*一个RealEstate对象应被创建*/ )   
    {     
        pInv.reset(new RealEstate(std::forward(params)...));   
    }   
    return pInv;
}

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


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

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

std::shared_ptr

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

[读书笔记]《Effective Modern C++》—— 智能指针_第1张图片

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

  • std::make_shared 创建 std::shared_ptr 对象时总会创建一个控制块,它创建一个要指向的新对象,所以可以肯定 std::make_shared 调用时对象不存在其他控制块。
  • 从独占指针(std::unique_ptr 或者 std::auto_ptr)上构造 std::shared_ptr 对象时会创建控制块,因为独占指针没有控制块,所以需要创建一个。
// 示例代码
template
template 
shared_ptr<_Tp>::shared_ptr(unique_ptr<_Yp, _Dp>&& __r,
                            typename enable_if
                            <
                                !is_lvalue_reference<_Dp>::value &&
                                is_convertible::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::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
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 spw1(ptr); // 为 ptr 创建第一个控制块
std::shared_ptr 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> processedWidgets;
};

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

正确用法:

class Widget: public std::enable_shared_from_this {
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(); //引用计数(ref count,RC)为1。

…
std::weak_ptr 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 spw1 = wpw.lock();  //如果wpw过期,spw1就为空

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

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

  • 打破 std::shared_ptr 的循环引用,因为weak_ptr 不会引起计数变化。
  • 观察者模式中的观察者列表,因为当一个观察者销毁时,消息产生者要不再使用,所以可以让消息产生者持有一个 std::weak_ptr 的容器指向观察者,这样可以在使用前检查是否悬空。
  • 缓存:主要是在可缓存对象中,调用者应当接受缓存对象的智能指针,并且需要知道缓存对象是否悬空,悬空则销毁。

你可能感兴趣的:(读书笔记,c++,开发语言,后端)