专栏首页linjinhe的专栏现代 C++:一文读懂智能指针

现代 C++:一文读懂智能指针

智能指针

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;  // 输出 8
  std::cout << sizeof(std::unique_ptr<int>) << std::endl;  // 输出 8
  std::cout << sizeof(std::unique_ptr<FILE, std::function<void(FILE*)>>)
            << std::endl;  // 输出 40

  std::cout << sizeof(std::shared_ptr<int>) << std::endl;  // 输出 16
  std::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);

image

复制一个 shared_ptr :

std::shared_ptr<T> sptr2 = sptr1;

image

为什么控制信息和每个 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;

image

另外,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]);
}

image

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

image

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

image

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

image

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

image

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 两次。

image

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

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<T> 的子类,成员变量中增加了一个指向 this 的 weak_ptr。这个 weak_ptr 在第一次创建 shared_ptr 的时候会被初始化,指向 this。

image

似乎继承了 std::enable_shared_from_this<T> 的类都被强制必须通过 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. unique_ptr
  3. shared_ptr
  4. weak_ptr
  5. enable_shared_from_this

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 现代C++之手写智能指针

    这个类可以完成智能指针的最基本的功能:对超出作用域的对象进行释放。但它缺了点东 西:

    公众号guangcity
  • 通俗易懂学习C++智能指针

    智能指针主要用于管理在堆上分配的内存,它将普通的指针封装为一个栈对象。当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏。

    海盗船长
  • C++智能指针原理和实现

      在C++中,动态内存的管理是由程序员自己申请和释放的,用一对运算符完成:new和delete。

    C语言与CPP编程
  • C++ 智能指针的简单实现

    C++ 中经常会出现因为没有 delete 指针而造成的内存泄漏,例如有一个 Object 模板类:

    joelzychen
  • C++ RCSP智能指针简单实现与应用

    智能指针的实现代码来源博客:《http://blog.csdn.net/to_be_better/article/details/53570910》

    jianghaibobo
  • c语言智能指针 附完整示例代码

    资源获取即初始化 (Resource Acquisition Is Initialization, RAII),RAII是一种资源管理机制,资源的有效期与持有资...

    cpuimage
  • 本周阅读打卡--c++设计新思维-智能指针

    For implementing self-ownership, smart pointers must carefu

    程序员小王
  • C++ 新特性学习(一) -- 概述+智能指针(smart_ptr)

    C++ 0x/11 终于通过了,真是个很爽的消息。于是乎我决定对新的东西系统学习一下。

    owent
  • 面试题:简单实现一个shared_ptr智能指针

    为了确保用 new 动态分配的内存空间在程序的各条执行路径都能被释放是一件麻烦的事情。C++ 11 模板库的 <memory> 头文件中定义的智能指针,即 sh...

    海盗船长
  • C++ 引用计数技术及智能指针的简单实现

    Tencent JCoder
  • 一张图读懂新一代人工智能发展规划

    一张图读懂国务院关于印发《新一代人工智能发展规划的通知》 ? 附录: 专栏1 基础理论 1.大数据智能理论。研究数据驱动与知识引导相结合的人工智能新方法、以自然...

    安智客
  • 从零开始学C++之boost库(一):详解 boost 库智能指针

    版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/...

    s1mba
  • Modern C++,学炸了!!

    很多熟悉其他语言的同学看 C++ 的代码一般也能看的懂,然后找几个例子熟悉下语法写了几行 C++ 代码,然后就产生了一个种错误:我也能写好 C++。

    范蠡
  • 一文读懂人工智能的前世今生(建议收藏)

    虽然现在有了诸如Siri、Cortana、IBM Watson等各类人工智能产品,也有像DeepBlue、AlphaGo人机大战等人工智能的新闻和事件不时出现,...

    华章科技
  • 收藏 | 这是一份文科生也能读懂的AI指南

    根据麦肯锡的数据,从现在到2030年这十几年间,人工智能将会为美国新创造大约13万亿美元的国内生产总值。相比之下,2017年整个美国的国内生产总值约为19万亿美...

    CDA数据分析师
  • 【AlphaGo之后会是什么】一文读懂人工智能打德扑

    作者:邓侃 【新智元导读】攻克围棋后,什么是AI的下一个征程?打扑克!相比信息完全可见的围棋,能够猜疑、虚张声势的德扑要困难得多。冷扑大师Libratus是首个...

    新智元
  • 一文读懂BI商业智能与大数据应用的区别

    之所以要区分大数据应用与BI(商业智能),是因为大数据应用与BI、数据挖掘等,并没有一个相对完整的认知。 BI(BusinessIntelligence)即商务...

    机器学习AI算法工程
  • 一图读懂产业互联时代的政企智能安全管理

    产业互联网高速发展,在各行各业深化数字化转型的关键节点,政企机构的网络安全建设面临着更高要求:

    腾讯安全
  • 排名第 1 ,Python 到底有什么魔力 !?

    根据 PYPL 发布的 7 月编程语言指数榜,Python 保持上涨趋势,8月流行指数再次上涨 5.5%,以 23.59% 的份额甩开 Java 排名第一,并逐...

    小小詹同学

扫码关注云+社区

领取腾讯云代金券