写点什么

C++ 三种智能指针的使用场景

作者:行者孙
  • 2021 年 12 月 14 日
  • 本文字数:4255 字

    阅读完需:约 14 分钟

C++98 中引入auto_ptr,但是实现有缺陷(使用 copy 语义转移资源),现已弃用,在实际项目中不应该使用。本文提到的三种智能指针主要指的得是std::unique_ptrstd::shared_ptrstd::weak_ptr

TL,DL

  • 智能指针的基本哲学是 RAII(Resource Acquisition Is Initialization). 将内存申请放在对象构造的时候,而在对象析构的时候自动释放。

  • RAII 的思想还应用到例如std::lock_guard 之类的用于对锁、线程、socket、handle 等资源的管理

  • unique_ptr 把握 unique 含义,资源的所有权不是共享的,而是被某个对象独占的时候。当资源所有权发生转移,可以通过 move 或者 release 进行转移

  • shared_ptr 把握 share 的含义,对象的所有权是共享的,可以被拷贝

  • weak_ptr 把握 weak 的含义,是一种“弱指针”,就是指向的资源可能是不可用的(可能是个垂悬指针),需要通过lock() 或者expire() 方法检查,利用这个特性,在可能会失效的场合可以使用(例如缓存、订阅者等)。需要搭配shared_ptr使用,并且可以防止循环引用。

一、RAII

RAII(Resource Acquisition Is Initialization) 资源获得即初始化。首先解释资源的概念:


  • 资源: 程序中需要获取后才能用,然后需要隐式(implictly)或者显式(explicitly)释放的东西。例如内存、文件句柄、socket 和锁等。如果没有释放,可能会造成资源泄漏或者程序出错。

  • 资源泄漏在简单调试中可能无法发现,但是在长期运行的时候可能会吃尽系统的可获得的资源,导致程序崩溃


CppCoreGuidelines (github.com) 中的一段代码为例:


void send(X* x, string_view destination){    auto port = open_port(destination);    my_mutex.lock();    // ... 1    send(port, x);    // ... 2    my_mutex.unlock();    close_port(port);    delete x; }
复制代码


在这个函数中需要程序手动释放锁、关闭端口、并且 delete x 的内存。这样依靠程序员的自觉自律很容易发生遗漏,更糟糕的是,如果在 1 或者 2 处抛出了异常; 2 后面的代码将不会被执行,也就是说释放资源的代码会被跳过,将导致资源泄漏!因此这样无法保证异常安全


所以 RAII 的思想被提出来了,RAII 的理念是将资源的获取放在类的构造函数里,资源的释放放在类的析构函数里。在类的生存期结束的时候,析构函数会被自动调用,对应的资源将会释放。


例如刚刚的例子可以改成:


void send(unique_ptr<X> x, string_view destination)  // x owns the X{    Port port{destination};            // port owns the PortHandle    lock_guard<mutex> guard{my_mutex}; // guard owns the lock    // ...    send(port, x);    // ...} // automatically unlocks my_mutex and deletes the pointer in x
复制代码


Port类的构造函数中获得资源,并且在析构的时候释放:


class Port {    PortHandle port;public:    Port(string_view destination) : port{open_port(destination)} { }    ~Port() { close_port(port); }    operator PortHandle() { return port; }
// port handles can't usually be cloned, so disable copying and assignment if necessary Port(const Port&) = delete; Port& operator=(const Port&) = delete;};
复制代码


RAII 是 C++ best practice 最重要的思想之一, 在实际的开发中我们应该尽可能使用。这样才能保证资源安全异常安全


智能指针 unique_ptrshared_ptr 就是 RAII 在内存管理上的实践。

二、unique_ptr vs shared_ptr vs weak_ptr

智能指针的语义是拥有(own)一个对象的所有权,并且控制其生存期。

2.1 unique_ptr

  • unique_ptr 没有拷贝语义(unique_ptr),不可以通过拷贝赋值和构造,但是可以通过移动语义进行资源所有权的转移。

  • unique_ptr 的使用场景,最常见的就是拥有一个一个对象的所有权,就像传统 C 指针的最基础用法,保存一个申请于堆上的对象

  • 构造,使用工厂函数 make_unique,更加简洁, 这是更加异常安全的使用方式(在复杂的表达式中,ref: exception-safety and make_unique )


  auto q = std::make_unique<Foo>(1);//better
复制代码

2.2 shared_ptr

  • shared_ptr 内部有引用计数,在对象所有权需要共享的时候(share)用,shared_ptr 具有赋值拷贝的语义。

  • 用法:

  • 作为需要保存在容器里的对象,同时避免频繁创建引起性能上的开销

  • 如果一个类的创建需要比较多的资源(例如比较大的的内存和拷贝),如果我们直接保存在容器里可能会在拷贝时产生比较大的性能损失,这个时候可以考虑使用shared_ptr,然后将shared_ptr保存于容器。


     vector<std::shared_ptr<Foo>> foos;     // ...     for(auto &foo : foos){         process_func(*foo);     }
复制代码


  1. 定制删除器

  2. shared_ptr 支持在构造的时候传入一个定制删除器,替代 delete 在生命周期结束时调用。可以以此实现 RAII 的思想。


     // tranditionally      FILE *fp = fopen("./1.txt","r");     // ...     // ...     fclose(fp);     //-------     // 通过使用定制删除器, 将删除器作为回调函数传入     shared_ptr<FILE> fp1(fopen("./1.txt","r"),fclose);
复制代码


例如上面这个例子,在 fp1 生命周期结束的时候,将会调用fclose(fp)而不是delete(fp)。 这个例子参考了 C++智能指针:shared_ptr用法详解_Tonson_的博客-CSDN博客

2.2 weak_ptr

  • weak_ptr 的语义是并不真正 own 一个对象的所有权,而是需要在使用的时候检查一下指针的有效性,可以应用于可能失效的场景,例如缓存、观察者模式的订阅者等等。也应用于打破 shared_ptr 代理的循环引用无法析构的问题。

  • 例 1


  #include <iostream>  #include <memory> // for std::shared_ptr  #include <string>    class Person  {    std::string m_name;    std::shared_ptr<Person> m_partner; // initially created empty    public:      Person(const std::string &name): m_name(name)    {      std::cout << m_name << " created\n";    }    ~Person()    {      std::cout << m_name << " destroyed\n";    }      friend bool partnerUp(std::shared_ptr<Person> &p1, std::shared_ptr<Person> &p2)    {      if (!p1 || !p2)        return false;        p1->m_partner = p2;      p2->m_partner = p1;        std::cout << p1->m_name << " is now partnered with " << p2->m_name << "\n";        return true;    }  };    int main()  {    auto lucy = std::make_shared<Person>("Lucy") ; // create a Person named "Lucy"    auto ricky = std::make_shared<Person>("Ricky"); // create a Person named "Ricky"      partnerUp(lucy, ricky); // Make "Lucy" point to "Ricky" and vice-versa      return 0;  }
复制代码


​ 在这个例子中产生了循环引用 ,在析构的时候将会尬住,lucky 和 ricky 都无法正确析构。跑一下这个程序会发现析构函数没有被调用。解决办法就是使用 weak_ptr 来替代shared_ptr


  • 例子 2, 使用weak_ptr 保存二叉树的 parent 节点,作用于 1 相似,也是用于打破循环引用带来的资源泄漏。


    #include <memory>        struct node    {       std::shared_ptr<node> left_child;       std::shared_ptr<node> right_child;       std::weak_ptr<node> parent;       foo data;       };
复制代码


  • 例子 3. 带缓存的工厂函数

  • 在一些代价高昂的场景,例如操作了文件或者数据库 I/O, 并且 ID 会被频繁重复使用,加上缓存可以优化性能,但是缓存需要在一定期限过期。

  • 因为要让调用者决定对象生存期,所以不能用 shared_ptr

  • 因为工厂内部需要保存一个对象的缓存,所以不能用unique_ptr

  • 因为缓存管理器需要检查指针是否空悬,所以不能用裸指针


    class Base{      //...      };    class Foo:public Base{      //  ...    };    class Bar:public Base{       // ...    }    enum Type{        k_FOO,        k_Bar    }    std::shared_ptr<Base> MakeInstance(Type t){        switch(t){            case k_FOO:               return make_shared<Foo>();            case k_BAR:               return make_shared<BAR>();            default:                return nullptr;        }    }    std::shared_ptr<Base> FooFactory(Type t){       static std::unordered_map<Type t, std::weak_ptr<Base>> cache;       auto p = cache[t].lock();       if(!p){           p = MakeInstance(t);           cache[t] = p;       }       return p;    }
复制代码


  • 例子 4. 观察者模式的订阅者

  • 观察者模式中,每个 topic 可以用容器保存一个观察者的weak_ptr指针,在有消息更新的时候,推送给订阅者。

  • 这个时候使用 weak_ptr的好处是,当观察者结束其生存期的时候,topic 可以检查其是否 expired,如果已经失效,则不去访问和推送。


​ 注: 例 3 和例 4 参考了std::weak_ptr用法 - 简书 (jianshu.com)

Best Practices

下面关于 Best Practices 的一些条款援引自 C++ Core Guidelines,建议细细阅读,可以转为实际的 coding 经验。


其他 Tips

  • 对于 Qt 的 QObject 及子类对象,Qt 框架使用 parent-children 的方式进行内存回收,此时不应该使用智能指针, 遵循 Qt 的设计即可

发布于: 1 小时前阅读数: 8
用户头像

行者孙

关注

Nothing replaces hard work 2018.09.17 加入

充满好奇心,终身学习者。 博客:https://01io.tech

评论

发布
暂无评论
C++ 三种智能指针的使用场景