IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> [读书笔记]《Effective Modern C++》—— 智能指针 -> 正文阅读

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

前言

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

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

下面的内容主要是摘录整理自 《Effective Modern 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 的容器指向观察者,这样可以在使用前检查是否悬空。
  • 缓存:主要是在可缓存对象中,调用者应当接受缓存对象的智能指针,并且需要知道缓存对象是否悬空,悬空则销毁。
  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-02-09 20:30:33  更:2022-02-09 20:31:11 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 7:15:07-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码