我的编程空间,编程开发者的网络收藏夹
学习永远不晚

C++11 shared_ptr 与 make_shared源码剖析详解

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

C++11 shared_ptr 与 make_shared源码剖析详解

0. 前言

所谓智能指针,可以从字面上理解为“智能”的指针。具体来讲,智能指针和普通指针的用法是相似的,不同之处在于,智能指针可以在适当时机自动释放分配的内存。也就是说,使用智能指针可以很好地避免“忘记释放内存而导致内存泄漏”问题出现。由此可见,C++ 也逐渐开始支持垃圾回收机制了,尽管目前支持程度还有限。

c++11 中发布了shared_ptrunique_ptrweak_ptr 用以资源的管理,都是定义在memory 这个头文件中。

  • std::shared_ptr 允许多个shared_ptr 实例指向同一个对象,通过计数管理;
  • std::unique_ptr 是独占对象;
  • weak_ptr 是辅助类,是一种弱引用,指向shared_ptr 所管理的对象。

1. 源码分析

1.1 头文件


#include <memory>

1.2 构造


    constexpr shared_ptr() noexcept;
    template<class Y> explicit shared_ptr(Y* p);
    template<class Y, class D> shared_ptr(Y* p, D d);
    template<class Y, class D, class A> shared_ptr(Y* p, D d, A a);
    template <class D> shared_ptr(nullptr_t p, D d);
    template <class D, class A> shared_ptr(nullptr_t p, D d, A a);
    template<class Y> shared_ptr(const shared_ptr<Y>& r, T *p) noexcept;
    shared_ptr(const shared_ptr& r) noexcept;
    template<class Y> shared_ptr(const shared_ptr<Y>& r) noexcept;
    shared_ptr(shared_ptr&& r) noexcept;
    template<class Y> shared_ptr(shared_ptr<Y>&& r) noexcept;
    template<class Y> explicit shared_ptr(const weak_ptr<Y>& r);
    template<class Y> shared_ptr(auto_ptr<Y>&& r);          // removed in C++17
    template <class Y, class D> shared_ptr(unique_ptr<Y, D>&& r);
    shared_ptr(nullptr_t) : shared_ptr() { }

构造函数比较多啊,抽一个看看源码。

1.2.1 shared_ptr 的移动构造函数


template<class _Tp>
inline
shared_ptr<_Tp>::shared_ptr(shared_ptr&& __r) _NOEXCEPT
    : __ptr_(__r.__ptr_),
      __cntrl_(__r.__cntrl_)
{
    __r.__ptr_ = 0;
    __r.__cntrl_ = 0;
}

大概知道,shared_ptr 中存放一个对象的指针__ptr_ 和用以计数的__cntrl_,这两是shared_ptr 的私有成员变量:


template<class _Tp>
class shared_ptr {
    typedef _Tp element_type;
 
private:
    element_type*      __ptr_;
    __shared_weak_count* __cntrl_;
 
    ...
}

另外,移动构造函数因为只是move,所以只是将旧的shared_ptr 转移到新的里。

1.2.2 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();
}

与移动构造相同,shared_ptr 实例,需要从参数中获得__ptr_ 和 __cntrl。

但是,与移动构造函数不同的是,拷贝构造时增加对象计数的。

下面举例shared_ptr 通常的创建方式:


std::shared_ptr<int> p1;                  //不传入任何实参
std::shared_ptr<int> p2(nullptr);         //传入空指针 nullptr
std::shared_ptr<int> p3(new int(10));     //指定指针为参数
std::shared_ptr<int> p4(p3);              //或者 std::shared_ptr<int> p4 = p3;
std::shared_ptr<int> p5(std::move(p4));   //或者 std::shared_ptr<int> p5 = std::move(p4);

1.3 赋值重载


    shared_ptr& operator=(const shared_ptr& r) noexcept;
    template<class Y> shared_ptr& operator=(const shared_ptr<Y>& r) noexcept;
    shared_ptr& operator=(shared_ptr&& r) noexcept;
    template<class Y> shared_ptr& operator=(shared_ptr<Y>&& r);
    template<class Y> shared_ptr& operator=(auto_ptr<Y>&& r); // removed in C++17
    template <class Y, class D> shared_ptr& operator=(unique_ptr<Y, D>&& r);

1.4 修改的接口


    void swap(shared_ptr& r) noexcept;
    void reset() noexcept;
    template<class Y> void reset(Y* p);
    template<class Y, class D> void reset(Y* p, D d);
    template<class Y, class D, class A> void reset(Y* p, D d, A a);

 reset 基本上是对应构造

1.5 获取


    T* get() const noexcept;
    T& operator*() const noexcept;
    T* operator->() const noexcept;
    long use_count() const noexcept;
    bool unique() const noexcept;
    explicit operator bool() const noexcept;

对于shared_ptr 的成员函数总结如下:

成员方法名

功 能

operator=()

重载赋值号,使得同一类型的 shared_ptr 智能指针可以相互赋值。

operator*()

重载 * 号,获取当前 shared_ptr 智能指针对象指向的数据。

operator->()

重载 -> 号,当智能指针指向的数据类型为自定义的结构体时,通过 -> 运算符可以获取其内部的指定成员。

swap()

交换 2 个相同类型 shared_ptr 智能指针的内容。

reset()

当函数没有实参时,该函数会使当前 shared_ptr 所指堆内存的引用计数减 1,同时将当前对象重置为一个空指针;当为函数传递一个新申请的堆内存时,则调用该函数的 shared_ptr 对象会获得该存储空间的所有权,并且引用计数的初始值为 1。

get()

获得 shared_ptr 对象内部包含的普通指针。

use_count()

返回同当前 shared_ptr 对象(包括它)指向相同的所有 shared_ptr 对象的数量。

unique()

判断当前 shared_ptr 对象指向的堆内存,是否不再有其它 shared_ptr 对象再指向它。

operator bool()

判断当前 shared_ptr 对象是否为空智能指针,如果是空指针,返回 false;反之,返回 true。

除了上面的成员函数外,C++11 标准还支持同一类型的 shared_ptr 对象,或者 shared_ptr 和 nullptr 之间,进行 ==,!=,,>= 运算。

注意:

  • shared_ptr用以共享一个对象的所有权,通过计数确认自动回收;
  • shared_ptr 共享的对象所有权,通过存储对象的指针,并通过get() 获取存储的对象的指针;
  • 可能在多个线程中同时用不同的shared_ptr实例,而这些实例可能指向同一个对象。多线程在没有同步的情况访问同一个shared_ptr 实例,并调用其non-const 成员函数时,有可能存在数据竞争。可以使用std::atomic* 函数保护数据竞争;

 2. make_shared


template<class T, class... Args>
    shared_ptr<T> make_shared(Args&&... args);

c++11 中针对shared_ptr 还提供了make_shared 这个外部函数,用以创建一个shared_ptr 实例。

c++ 建议尽可能使用make_shared 创建shared_ptr 实例。

下面来说明下make_shared 的优缺点。

2.1 make_shared 优点

2.1.1 效率高

shared_ptr 需要维护引用计数的信息:

  • 强引用, 用来记录当前有多少个存活的 shared_ptrs 正持有该对象. 共享的对象会在最后一个强引用离开的时候销毁( 也可能释放).
  • 弱引用, 用来记录当前有多少个正在观察该对象的 weak_ptrs. 当最后一个弱引用离开的时候, 共享的内部信息控制块会被销毁和释放 (共享的对象也会被释放, 如果还没有释放的话).

如果你通过使用原始的 new 表达式分配对象, 然后传递给 shared_ptr (也就是使用 shared_ptr 的构造函数) 的话, shared_ptr 的实现没有办法选择, 而只能单独的分配控制块:


auto p = new widget();
shared_ptr sp1{ p }, sp2{ sp1 };

如果选择使用 make_shared 的话, 情况就会变成下面这样:


auto sp1 = make_shared(), sp2{ sp1 };

内存分配的动作, 可以一次性完成. 这减少了内存分配的次数, 而内存分配是代价很高的操作。

2.1.2 异常安全


void F(const std::shared_ptr<Lhs>& lhs, const std::shared_ptr<Rhs>& rhs) {  }
F(std::shared_ptr<Lhs>(new Lhs("foo")),
std::shared_ptr<Rhs>(new Rhs("bar")));

C++ 是不保证参数求值顺序, 以及内部表达式的求值顺序的, 所以可能的执行顺序如下:

  • new Lhs(“foo”))
  • new Rhs(“bar”))
  • std::shared_ptr
  • std::shared_ptr

 好了, 现在我们假设在第 2 步的时候, 抛出了一个异常 (比如 out of memory, 总之, Rhs 的构造函数异常了), 那么第一步申请的 Lhs 对象内存泄露了. 这个问题的核心在于, shared_ptr 没有立即获得裸指针

我们可以用如下方式来修复这个问题.


auto lhs = std::shared_ptr<Lhs>(new Lhs("foo"));
auto rhs = std::shared_ptr<Rhs>(new Rhs("bar"));
F(lhs, rhs);

当然, 推荐的做法是使用 std::make_shared 来代替:


F(std::make_shared<Lhs>("foo"), std::make_shared<Rhs>("bar"));

2.2 make_shared缺点 

构造函数是保护或私有时,无法使用 make_shared

make_shared 虽好, 但也存在一些问题, 比如, 当我想要创建的对象没有公有的构造函数时, make_shared 就无法使用了, 当然我们可以使用一些小技巧来解决这个问题, 比如这里 How do I call ::std::make_shared on a class with only protected or private constructors?

对象的内存可能无法及时回收

make_shared 只分配一次内存, 这看起来很好. 减少了内存分配的开销. 问题来了, weak_ptr 会保持控制块(强引用, 以及弱引用的信息)的生命周期, 而因此连带着保持了对象分配的内存, 只有最后一个 weak_ptr 离开作用域时, 内存才会被释放. 原本强引用减为 0 时就可以释放的内存, 现在变为了强引用, 若引用都减为 0 时才能释放, 意外的延迟了内存释放的时间. 这对于内存要求高的场景来说, 是一个需要注意的问题

无法像shared_ptr 的构造那样添加一个deleter

3. 举例


#include <iostream>
#include <memory>
using namespace std;
 
int main()
{
    //构建 2 个智能指针
    std::shared_ptr<int> p1(new int(10));
    std::shared_ptr<int> p2(p1);
    //输出 p2 指向的数据
    cout << *p2 << endl;
    p1.reset();//引用计数减 1,p1为空指针
    if (p1) {
        cout << "p1 不为空" << endl;
    }
    else {
        cout << "p1 为空" << endl;
    }
    //以上操作,并不会影响 p2
    cout << *p2 << endl;
    //判断当前和 p2 同指向的智能指针有多少个
    cout << p2.use_count() << endl;
    return 0;
}

运行结果:

10
p1 为空
10
1

参考:

C++11 std::shared_ptr总结与使用示例代码详解

总结

本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注编程网的更多内容!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

C++11 shared_ptr 与 make_shared源码剖析详解

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

Java网络编程与NIO详解11:Tomcat中的Connector源码分析(NIO)

本文转载 https://www.javadoop.com本系列文章将整理到我在GitHub上的《Java面试指南》仓库,更多精彩内容请到我的仓库里查看https://github.com/h3pl/Java-Tutorial喜欢的话麻烦点
2023-06-02

React源码分析之useCallback与useMemo及useContext详解

这篇文章主要介绍了ReactuseCallback与useMemo及useContext源码分析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习吧
2022-11-13

C++ 函数库详解:系统功能外延与代码性能分析

答案: c++++ 函数库可增强程序功能并优化代码性能。详情:系统功能外延: 函数库提供预先实现的功能,例如容器、算法和 gui 工具。代码性能分析: 性能分析工具可识别函数库调用中消耗时间的区域。代码优化: 优化技术(如内联和避免复制)可
C++ 函数库详解:系统功能外延与代码性能分析
2024-05-01

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录