现代C一文读懂智能指针

https://m.toutiao.com/is/JHS4MVf/

智能指针

C++11 引入了 3 个智能指针类型:

  1. std::unique_ptr<T> :独占资源所有权的指针。
  2. std::shared_ptr<T> :共享资源所有权的指针。
  3. std::weak_ptr<T> :共享资源的观察者,需要和 std::shared_ptr 一起使用,不影响资源的生命周期。

std::auto_ptr 已被废弃。

std::unique_ptr

简单说,当我们独占资源的所有权的时候,可以使用 std::unique_ptr 对资源进行管理——离开 unique_ptr 对象的作用域时,会自动释放资源。这是很基本的 RAII 思想。

std::unique_ptr 的使用比较简单,也是用得比较多的智能指针。这里直接看例子。

  1. 使用裸指针时,要记得释放内存。
{ int* p = new int(100); // ... delete p; // 要记得释放内存}
  1. 使用 std::unique_ptr 自动管理内存。
{    std::unique_ptr<int> uptr = std::make_unique<int>(200);    //...    // 离开 uptr 的作用域的时候自动释放内存}
  1. std::unique_ptr 是 move-only 的。
{ std::unique_ptr<int> uptr = std::make_unique<int>(200); std::unique_ptr<int> uptr1 = uptr; // 编译错误,std::unique_ptr<T> 是 move-only 的 std::unique_ptr<int> uptr2 = std::move(uptr); assert(uptr == nullptr);}
  1. std::unique_ptr 可以指向一个数组。
{    std::unique_ptr<int[]> uptr = std::make_unique<int[]>(10);    for (int i = 0; i < 10; i++) {        uptr[i] = i * i;    }       for (int i = 0; i < 10; i++) {        std::cout << uptr[i] << std::endl;    }   }
  1. 自定义 deleter。
{ struct FileCloser { void operator()(FILE* fp) const { if (fp != nullptr) { fclose(fp); } } }; std::unique_ptr<FILE, FileCloser> uptr(fopen('test_file.txt', 'w'));}
  1. 使用 Lambda 的 deleter。
{    std::unique_ptr<FILE, std::function<void(FILE*)>> uptr(        fopen('test_file.txt', 'w'), [](FILE* fp) {            fclose(fp);        });}

std::shared_ptr

std::shared_ptr 其实就是对资源做引用计数——当引用计数为 0 的时候,自动释放资源。

{ std::shared_ptr<int> sptr = std::make_shared<int>(200); assert(sptr.use_count() == 1); // 此时引用计数为 1 { std::shared_ptr<int> sptr1 = sptr; assert(sptr.get() == sptr1.get()); assert(sptr.use_count() == 2); // sptr 和 sptr1 共享资源,引用计数为 2 } assert(sptr.use_count() == 1); // sptr1 已经释放}// use_count 为 0 时自动释放内存

和 unique_ptr 一样,shared_ptr 也可以指向数组和自定义 deleter。

{    // C++20 才支持 std::make_shared<int[]>    // std::shared_ptr<int[]> sptr = std::make_shared<int[]>(100);    std::shared_ptr<int[]> sptr(new int[10]);    for (int i = 0; i < 10; i++) {        sptr[i] = i * i;    }       for (int i = 0; i < 10; i++) {        std::cout << sptr[i] << std::endl;    }   }{    std::shared_ptr<FILE> sptr(        fopen('test_file.txt', 'w'), [](FILE* fp) {            std::cout << 'close ' << fp << std::endl;            fclose(fp);        });}

std::shared_ptr 的实现原理

一个 shared_ptr 对象的内存开销要比裸指针和无自定义 deleter 的 unique_ptr 对象略大。

std::cout << sizeof(int*) << std::endl; // 输出 8std::cout << sizeof(std::unique_ptr<int>) << std::endl; // 输出 8std::cout << sizeof(std::unique_ptr<FILE, std::function<void(FILE*)>>) << std::endl; // 输出 40std::cout << sizeof(std::shared_ptr<int>) << std::endl; // 输出 16std::shared_ptr<FILE> sptr(fopen('test_file.txt', 'w'), [](FILE* fp) { std::cout << 'close ' << fp << std::endl; fclose(fp);}); std::cout << sizeof(sptr) << std::endl; // 输出 16

无自定义 deleter 的 unique_ptr 只需要将裸指针用 RAII 的手法封装好就行,无需保存其它信息,所以它的开销和裸指针是一样的。如果有自定义 deleter,还需要保存 deleter 的信息。

shared_ptr 需要维护的信息有两部分:

  1. 指向共享资源的指针。
  2. 引用计数等共享资源的控制信息——实现上是维护一个指向控制信息的指针。

所以,shared_ptr 对象需要保存两个指针。shared_ptr 的 的 deleter 是保存在控制信息中,所以,是否有自定义 deleter 不影响 shared_ptr 对象的大小。

当我们创建一个 shared_ptr 时,其实现一般如下:

std::shared_ptr<T> sptr1(new T);

复制一个 shared_ptr :

std::shared_ptr<T> sptr2 = sptr1;

为什么控制信息和每个 shared_ptr 对象都需要保存指向共享资源的指针?可不可以去掉 shared_ptr 对象中指向共享资源的指针,以节省内存开销?

答案是:不能。因为 shared_ptr 对象中的指针指向的对象不一定和控制块中的指针指向的对象一样。

来看一个例子。

struct Fruit {    int juice;};struct Vegetable {    int fiber;};struct Tomato : public Fruit, Vegetable {    int sauce;}; // 由于继承的存在,shared_ptr 可能指向基类对象std::shared_ptr<Tomato> tomato = std::make_shared<Tomato>();std::shared_ptr<Fruit> fruit = tomato;std::shared_ptr<Vegetable> vegetable = tomato;

另外,std::shared_ptr 支持 aliasing constructor。

template< class Y >shared_ptr( const shared_ptr<Y>& r, element_type* ptr ) noexcept;

Aliasing constructor,简单说就是构造出来的 shared_ptr 对象和参数 r 指向同一个控制块(会影响 r 指向的资源的生命周期),但是指向共享资源的指针是参数 ptr。看下面这个例子。

using Vec = std::vector<int>;std::shared_ptr<int> GetSPtr() {    auto elts = {0, 1, 2, 3, 4};    std::shared_ptr<Vec> pvec = std::make_shared<Vec>(elts);    return std::shared_ptr<int>(pvec, &(*pvec)[2]);}std::shared_ptr<int> sptr = GetSPtr();for (int i = -2; i < 3; ++i) {    printf('%d\n', sptr.get()[i]);}

看上面的例子,使用 std::shared_ptr 时,会涉及两次内存分配:一次分配共享资源对象;一次分配控制块。C++ 标准库提供了 std::make_shared 函数来创建一个 shared_ptr 对象,只需要一次内存分配。

这种情况下,不用通过控制块中的指针,我们也能知道共享资源的位置——这个指针也可以省略掉。

std::weak_ptr

std::weak_ptr 要与 std::shared_ptr 一起使用。一个 std::weak_ptr 对象看做是 std::shared_ptr 对象管理的资源的观察者,它不影响共享资源的生命周期:

  1. 如果需要使用 weak_ptr 正在观察的资源,可以将 weak_ptr 提升为 shared_ptr。
  2. 当 shared_ptr 管理的资源被释放时,weak_ptr 会自动变成 nullptr。
void Observe(std::weak_ptr<int> wptr) { if (auto sptr = wptr.lock()) { std::cout << 'value: ' << *sptr << std::endl; } else { std::cout << 'wptr lock fail' << std::endl; }}std::weak_ptr<int> wptr;{ auto sptr = std::make_shared<int>(111); wptr = sptr; Observe(wptr); // sptr 指向的资源没被释放,wptr 可以成功提升为 shared_ptr}Observe(wptr); // sptr 指向的资源已被释放,wptr 无法提升为 shared_ptr

当 shared_ptr 析构并释放共享资源的时候,只要 weak_ptr 对象还存在,控制块就会保留,weak_ptr 可以通过控制块观察到对象是否存活。

enable_shared_from_this

一个类的成员函数如何获得指向自身(this)的 shared_ptr?看看下面这个例子有没有问题?

class Foo { public:  std::shared_ptr<Foo> GetSPtr() {    return std::shared_ptr<Foo>(this);  }};auto sptr1 = std::make_shared<Foo>();assert(sptr1.use_count() == 1);auto sptr2 = sptr1->GetSPtr();assert(sptr1.use_count() == 1);assert(sptr2.use_count() == 1);

上面的代码其实会生成两个独立的 shared_ptr,他们的控制块是独立的,最终导致一个 Foo 对象会被 delete 两次。

成员函数获取 this 的 shared_ptr 的正确的做法是继承 std::enable_shared_from_this 。

class Bar : public std::enable_shared_from_this<Bar> { public: std::shared_ptr<Bar> GetSPtr() { return shared_from_this(); }};auto sptr1 = std::make_shared<Bar>();assert(sptr1.use_count() == 1);auto sptr2 = sptr1->GetSPtr();assert(sptr1.use_count() == 2);assert(sptr2.use_count() == 2);

一般情况下,继承了 std::enable_shared_from_this 的子类,成员变量中增加了一个指向 this 的 weak_ptr。这个 weak_ptr 在第一次创建 shared_ptr 的时候会被初始化,指向 this。

似乎继承了 std::enable_shared_from_this 的类都被强制必须通过 shared_ptr 进行管理。

auto b = new Bar;auto sptr = b->shared_from_this();

在我的环境下(gcc 7.5.0)上面的代码执行的时候会直接 coredump,而不是返回指向 nullptr 的 shared_ptr:

terminate called after throwing an instance of 'std::bad_weak_ptr' what(): bad_weak_ptr

小结

智能指针,本质上是对资源所有权和生命周期管理的抽象:

  1. 当资源是被独占时,使用 std::unique_ptr 对资源进行管理。
  2. 当资源会被共享时,使用 std::shared_ptr 对资源进行管理。
  3. 使用 std::weak_ptr 作为 std::shared_ptr 管理对象的观察者。
  4. 通过继承 std::enable_shared_from_this 来获取 this 的 std::shared_ptr 对象。

参考资料

  1. Back to Basics: Smart Pointers
  2. cppreference: unique_ptr
  3. cppreference: shared_ptr
  4. cppreference: weak_ptr
  5. cppreference: enable_shared_from_this
(0)

相关推荐

  • C 11中智能指针的原理、使用、实现

    目录 理解智能指针的原理 智能指针的使用 智能指针的设计和实现 1.智能指针的作用 C 程序设计中使用堆内存是非常频繁的操作,堆内存的申请和释放都由程序员自己管理.程序员自己管理堆内存可以提高了程序的 ...

  • enable_shared_from_this 实现

    template<class T> class enable_shared_from_this { protected: enable_shared_from_this() {} enab ...

  •  C++11中shared

    在C++中,动态内存的管理是通过一对运算符来完成的:new,在动态内存中为对象分配空间并返回一个指向该对象的指针,可以选择对对象进行初始化:delete,接受一个动态对象的指针,销毁该对象,并释放与之 ...

  • C 11 shared_ptr概念及用法

    基本概念 shared_ptr 是C 11提供的一种智能指针类,主要用于消除内存泄漏和悬空指针的问题,方便资源的管理,自动释放没有指针引用的资源. shared_ptr的大小是原始指针的两倍,因为它的 ...

  • c 11新特性之智能指针

    很多人谈到c++,说它特别难,可能有一部分就是因为c++的内存管理吧,不像java那样有虚拟机动态的管理内存,在程序运行过程中可能就会出现内存泄漏,然而这种问题其实都可以通过c++11引入的智能指针来 ...

  • C++内存管理之shared

     ----------------------------------------shared_ptr--------------------------------------- 引子 c++中动态 ...

  • 收藏!一文读懂智能大灯光源技术路线

    收藏!一文读懂智能大灯光源技术路线

  • 一文读懂华为全屋智能:这是智能家居的未来吗?鸿蒙的家是什么?

    我菊的叙事风格还是太过硬核. 比如你把发布会内容单独拿出来看,感受一下,你根本不知道在说什么.当然,全屋智能确实本身就比较难讲. 因此,这篇文章主要回答一个问题:什么是全屋智能?它和智能家居的区别到底 ...

  • MWC上海展新花样:鸿蒙OS智能家居、OPPO隔空充电,5G+智能设备最吸睛|一文读懂

    整理|吴祎珺 MWC终于回归了. MWC,即世界移动通信大会(英文名:Mobile World Congress),是全球最具影响力的移动通信领域的展览会,至今已有26年历史.2020年,巴塞罗那MW ...

  • 一文读懂石墨烯智能穿戴

    产业链角度 市场交易角度 根据中国科学院文献情报中心和投中研究院今年公布的<2018可穿戴传感器产业技术报告>显示,全球的可穿戴设备的出货量与营收规模保持稳定增长的状态,预计2020年,该 ...

  • 一文读懂中国智能制造发展现状及趋势分析

    ☝ 先进制造业·导读 智能制造--制造业数字化.网络化.智能化,是我国制造业创新发展的主要抓手,是我国制造业转型升级的主要路径,是我国加快建设制造强国的主攻方向. 本文将分析中国智能制造发展现状及趋势 ...

  • 重磅|一文彻底读懂智能对话系统!当前研究综述和未来趋势

    选择"星标"公众号 重磅干货,第一时间送达! 笔者在最近的研究中发现了一篇非常好的有关对话系统的论文,<A Survey on Dialogue Systems:Recent ...

  • 一文读懂王清任的五逐瘀汤

    至道汇 今天 瘀血 王清任三步辨瘀血法及其临床应用 一.首辨瘀血典型症状的有无 无论瘀血为病,证候有多么复杂多变,其外在表现总会有一定规律可循.其中表现突出且有别于他证的特异性症状,便可视为瘀血的典型 ...

  • 一文读懂硬度试验

    硬度,硬度测量,硬度公式,硬度换算,材料硬度,钢材硬度,布氏硬度,硬度试验 来源:北京赛亿科技有限公司

  • 一文读懂:广东人从哪里来?

    岭南韵味  广府情怀 粤曲| 粤剧 | 名家 | 名段 很多外省的朋友们不是很了解广东人的组成及来源,也不知道为何叫做"广东".更会有一些朋友会奇怪: ◆为什么秦汉时期的" ...