C++进阶:智能指针之weak_ptr

3,257 阅读8分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天,点击查看活动详情

概念

和 shared_ptr、unique_ptr 类型指针一样,weak_ptr 智能指针也是以模板类的方式实现的。weak_ptr<T>( T 为指针所指数据的类型)定义在<memory>头文件,并位于 std 命名空间中。

该类型指针通常不单独使用(没有实际用处),只能和 shared_ptr 类型指针搭配使用。可以视为 shared_ptr 指针的一种辅助工具。借助 weak_ptr 类型指针可以获取 shared_ptr 指针的一些状态信息,比如有多少指向相同的 shared_ptr 指针、通过expired()判断shared_ptr 指针指向的堆内存是否已经被释放等等。

weak_ptr 类型指针并不会影响所指堆内存空间的引用计数。当 weak_ptr 指针的指向和某一 shared_ptr 指针相同时,weak_ptr 指针并不会使所指堆内存的引用计数加 1;同样,当 weak_ptr 指针被释放时,之前所指堆内存的引用计数也不会因此而减 1。它不具有普通指针的行为,没有重载operator*->,这也就意味着,weak_ptr 类型指针只能访问所指的堆内存,而无法修改它。

weak_ptr模板类

template<class T> class weak_ptr {
  public:
    using element_type = remove_extent_t<T>;
 
    // 构造函数
    constexpr weak_ptr() noexcept;
    template<class Y>
      weak_ptr(const shared_ptr<Y>& r) noexcept;
    weak_ptr(const weak_ptr& r) noexcept;
    template<class Y>
      weak_ptr(const weak_ptr<Y>& r) noexcept;
    weak_ptr(weak_ptr&& r) noexcept;
    template<class Y>
      weak_ptr(weak_ptr<Y>&& r) noexcept;
 
    // 析构函数
    ~weak_ptr();
 
    // 赋值
    weak_ptr& operator=(const weak_ptr& r) noexcept;
    template<class Y>
      weak_ptr& operator=(const weak_ptr<Y>& r) noexcept;
    template<class Y>
      weak_ptr& operator=(const shared_ptr<Y>& r) noexcept;
    weak_ptr& operator=(weak_ptr&& r) noexcept;
    template<class Y>
      weak_ptr& operator=(weak_ptr<Y>&& r) noexcept;
 
    // 其中 x 表示一个同类型的 weak_ptr 类型指针,
    // 该函数可以互换 2 个同类型 weak_ptr 指针的内容。
    void swap(weak_ptr& r) noexcept;
    // 将当前 weak_ptr 指针置为空指针。
    void reset() noexcept;
 
    // 查看指向和当前 weak_ptr 指针相同的 shared_ptr 指针的数量。
    long use_count() const noexcept;
    // 判断当前 weak_ptr 指针为否过期(指针为空,或者指向的堆内存已经被释放)。
    bool expired() const noexcept;
    // 如果当前 weak_ptr 已经过期,则该函数会返回一个空的 shared_ptr 指针;
    // 反之,该函数返回一个和当前 weak_ptr 指向相同的 shared_ptr 指针。
    shared_ptr<T> lock() const noexcept;
    template<class U>
      bool owner_before(const shared_ptr<U>& b) const noexcept;
    template<class U>
      bool owner_before(const weak_ptr<U>& b) const noexcept;
  };

std::weak_ptr构造

#include <iostream>
#include <memory>

int main() {
    // 创建一个空 weak_ptr 指针
    std::weak_ptr<int> wp1;

    // 凭借已有的 weak_ptr 指针,可以创建一个新的 weak_ptr 指针
    // 若 wp1 为空指针,则 wp2 也为空指针;反之,
    // 如果 wp1 指向某一 shared_ptr 指针拥有的堆内存,
    // 则 wp2 也指向该块存储空间(可以访问,但无所有权)。
    std::weak_ptr<int> wp2 (wp1);  // 拷贝构造函数
    std::weak_ptr<int> wp2 = wp1;  // 拷贝构造函数

    // weak_ptr 指针更常用于指向某一 shared_ptr 指针拥有的堆内存,
    // 因为在构建 weak_ptr 指针对象时,可以利用已有的 shared_ptr 指针为其初始化。
    // 由此,wp3 指针和 sp 指针有相同的指针。
    std::shared_ptr<int> sp (new int);
    std::weak_ptr<int> wp3 (sp); // shared_ptr直接构造
    std::weak_ptr<int> wp3 = sp; // 拷贝赋值函数
}

std::weak_ptr用法

weak_ptr是为了配合shared_ptr而引入的一种智能指针,因为它不具有普通指针的行为,没有重载operator*->,它的最大作用在于协助shared_ptr工作,像旁观者那样观测资源的使用情况。weak_ptr可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权。但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。使用weak_ptr的成员函数use_count()可以观测资源的引用计数,另一个成员函数expired()的功能等价于use_count()==0,但更快,表示被观测的资源(也就是shared_ptr的管理的资源)已经不复存在。weak_ptr可以使用一个非常重要的成员函数lock()从被观测的shared_ptr获得一个可用的shared_ptr对象, 从而操作资源。但当expired()==true的时候,lock()函数将返回一个存储空指针的shared_ptr。

#include <iostream>
#include <memory>

int main() {
    {
        std::shared_ptr<int> sh_ptr = std::make_shared<int>(10);
        std::cout << sh_ptr.use_count() << std::endl;  // 输出1

        std::weak_ptr<int> wp(sh_ptr);
        std::cout << wp.use_count() << std::endl;  // 赋值给weak_ptr后还是输出1

        if(!wp.expired()){ // 检查sh_ptr是否还有效
            std::shared_ptr<int> sh_ptr2 = wp.lock(); //将sh_ptr赋值给sh_ptr2
            *sh_ptr = 100;
            std::cout << wp.use_count() << std::endl;  // 输出2
        }
    } //delete memory
       
    std::weak_ptr<int> wp;
    {
        std::shared_ptr<int> sh_ptr = std::make_shared<int>(10);
        wp = sh_ptr;
        std::cout << wp.expired() << std::endl;  // 输出false
    } //delete memory

    std::cout << wp.expired() << std::endl;  // 输出true 
}

判断weak_ptr指向对象是否存在

既然weak_ptr并不改变其所共享的shared_ptr实例的引用计数,那就可能存在weak_ptr指向的对象被释放掉这种情况。这时,我们就不能使用weak_ptr直接访问对象。那么我们如何判断weak_ptr指向对象是否存在呢?C++中提供了lock函数来实现该功能。如果对象存在,lock()函数返回一个指向共享对象的shared_ptr,否则返回一个空shared_ptr。

#include <iostream>
#include <memory>

int main(int argc, const char* argv[]) {
    std::shared_ptr<int> sp(new int(10));
    std::weak_ptr<int> wp(sp);
    //sp.reset();

    if (std::shared_ptr<int> pa = wp.lock()) {
        std::cout << *pa << std::endl;
    }
    else {
        std::cout << "wp指向对象为空" << std::endl;
    }


    sp.reset();
    if (std::shared_ptr<int> pa = wp.lock()) {
        std::cout << *pa << std::endl;
    }
    else {
        std::cout << "wp指向对象为空" << std::endl;
    }
}

输出:

10
wp指向对象为空

weak_ptr并没有重载operator->operator *操作符,因此不可直接通过weak_ptr使用对象,典型的用法是调用其lock函数来获得shared_ptr示例,进而访问原始对象。

循环引用

weak_ptr的一个作用是解决share_ptr的循环引用问题。如下面代码所示,class A中含有指向class B的shared指针, class B 中含有指向class A的shared指针,这样形成了循环引用。spa和spb的强引用计数永远大于等于1,所以直到程序退出前都不会被退出,这种情况有时候在正常的业务逻辑中是不可避免的,而解决循环引用的方法是改用weak_ptr:

#include <iostream>
#include <memory>

class B;
class A
{
public:
    A() { std::cout << "A's constructor ..." << std::endl; }
    ~A() { std::cout << "A's destructor ..." << std::endl; }
    
    std::shared_ptr<B> b;//class A中含有指向class B的shared指针
};

class B
{
public:
    B() { std::cout << "B's constructor ..." << std::endl; }
    ~B() { std::cout << "B's destructor ..." << std::endl; }

    std::shared_ptr<A> a; //class B 中含有指向class A的shared指针
};

int main() 
{
    std::shared_ptr<A> spa = make_shared<A>();
    std::shared_ptr<B> spb = make_shared<B>();

    spa->b = spb;// bb 计数器来到了 2
    std::cout << spb.use_count() << std::endl;
    spb->a = spa;// spa 计数器来到了 2
    std::cout << spa.use_count() << std::endl;

    return 0;
}
// 上面main函数中spa和spb计数器都加到了2,而出了main函数作用域,
// 其计数都会-1,变成了1,而不是0,所以它们都没有被析构(销毁)。

所以,这种情况我们就需要使用weak_ptr替代shared_ptr来避免循环引用导致内存泄漏问题,看下面例子:

#include <iostream>
#include <memory>

class B;
class A
{
public:
    A() { std::cout << "A's constructor ..." << std::endl; }
    ~A() { std::cout << "A's destructor ..." << std::endl; }
    
    std::weak_ptr<B> b;
};

class B
{
public:
    B() { std::cout << "B's constructor ..." << std::endl; }
    ~B() { std::cout << "B's destructor ..." << std::endl; }

    std::weak_ptr<A> a;
};
int main(int argc, const char* argv[]) {
    std::shared_ptr<A> spa = std::make_shared<A>();
    std::shared_ptr<B> spb = std::make_shared<B>();
    cout << "spa = " << spa.use_count() << endl;
    cout << "spb = " << spb.use_count() << endl;
    spa->b = spb; //spb强引用计数为2,弱引用计数为1
    spb->a = spa; 
    //如果spb->a为weak_ptr类型,不会增加强引用计数,
    //所以spa强引用计数为1,弱引用计数为2
    cout << "spa = " << spa.use_count() << endl;
    cout << "spb = " << spb.use_count() << endl;
    return 0;
} //main函数退出后,spa先释放,spb再释放,循环解开了

weak_ptr不会增加shared_ptr的计数器,从而离开mian函数作用域时,shared_ptr spa& spb计数器都 -1 ,成为0, 具备销毁条件,调用析构函数销毁自己和所指对象,因此,我们终于可以说weak_ptr具备避免内存泄漏的功能了。

使用场景

下面我们来介绍几个个weak_ptr的使用场景:

共享对象的线程安全问题

假设线程A和线程B访问一个共享的对象,如果线程A正在析构这个对象的时候,线程B又要调用该共享对象的成员方法,此时可能线程A已经把对象析构完了,线程B再去访问该对象,就会发生不可预期的错误。

#include <iostream>
#include <memory>
#include <thread>

class Test {
  public:
    Test(int id) : m_id(id) {}
    void showID() {
      std::cout << m_id << std::endl;
    }
  private:
    int m_id;
};

void thread1(Test* t) {
  std::this_thread::sleep_for(std::chrono::seconds(2));
  t->showID();
}

int main()
{
  Test* t = new Test(10);
  std::thread t1(thread1, t);
  delete t;  //仅用于假设t被析构了
  std::cout << "delete t" << std::endl;
  t1.join();

  return 0;
}

在上面例子中,由于thread1等待了2s,此时,main线程早已经把t对象析构了。打印m_id,自然不能打印出10了。但是可以通过shared_ptr和weak_ptr来解决共享对象的线程安全问题。

#include <iostream>
#include <memory>
#include <thread>

class Test {
  public:
    Test(int id) : m_id(id) {}
    ~Test() {std::cout << "~Test" << std::endl;}
    void showID() {
      std::cout << "m_id: " << m_id << std::endl;
    }
  private:
    int m_id;
};

void thread2(std::weak_ptr<Test> t) {
  std::this_thread::sleep_for(std::chrono::seconds(1));
  std::shared_ptr<Test> sp = t.lock();
  if(sp)
    sp->showID(); // 打印结果:2
  else
    std::cout << "sp null" << std::endl;
}

int main()
{
    std::thread t2;
    // {
        std::shared_ptr<Test> sp = std::make_shared<Test>(2);
        std::weak_ptr<Test> t(sp);
        t2 = std::thread(thread2, t);
        std::cout << "use_count: " << sp.use_count() << std::endl;
    // }
    t2.join();

  return 0;
}

如果想访问对象的方法,先通过t的lock方法进行提升操作,把weak_ptr提升为shared_ptr强智能指针。提升过程中,是通过检测它所观察的强智能指针保存的Test对象的引用计数,来判定Test对象是否存活。sp如果为nullptr,说明Test对象已经析构,不能再访问;如果sp!=nullptr,则可以正常访问Test对象的方法。

如果main函数中设置了sp的作用域,当t2执行前sp智能指针离开作用域,被析构,进而把Test对象析构,此时showID方法已经不会被调用,因为在thread2方法中,提升到sp时,lock方法判定Test对象已经析构,就不会调用showID了!

带缓存功能的工厂函数

我们直接看下面的程序:

#include <iostream>
#include <unordered_map>
#include <memory>
using namespace std;

class Person {
public:
    virtual ~Person() = default;
    virtual void Action() = 0;
};

class Student : public Person {
public:
    ~Student() {
        cout << "~Student() called....\n";
    }
    virtual void Action() override {
        cout << "Student learning....\n";
    }
};

class Engineer : public Person {
public:
    ~Engineer() {
        cout << "~Engineer() called....\n";
    }
    virtual void Action() override {
        cout << "Engineer coding....\n";
    }
};

enum class InvestType {
    INVEST_TYPE_STUDENT,
    INVEST_TYPE_ENGINEER,
};

// 工厂函数
shared_ptr<Person> CreateFactory(InvestType type) {
    // 自定义析构器, 这里以lambda表达式的形式给出
    auto CustomDelete = [](Person *ptr) {
        cout << "custom delete called...." << ptr << endl;

        // 注意:ptr可能为空指针,比如默认为空,
        // 然后调用reset赋值时,会先调用一遍析构
        if (ptr) {
            // TODO 自定义析构时想干的事
            delete ptr;
        }
    };

    // 待返回的指针, 初始化为空指针
    shared_ptr<Person> sp(nullptr);

    // 注意这里用reset来指定sp获取从new产生的对象的所有权, 不能用=赋值
    switch (type) {
    case InvestType::INVEST_TYPE_STUDENT:
        // 注意:自定义析构器是随对象一起指定的,这里区别于unique_ptr
        sp.reset(new Student, CustomDelete);
        break;
    case InvestType::INVEST_TYPE_ENGINEER:
        // 如果不指定自定义析构器的话,则不会调用
        sp.reset(new Engineer);
        break;
    }

    // 返回智能指针
    return sp;
}

// 带缓存的工厂函数
// 调用工厂函数CreateFactory成本高昂, 并且type会频繁的重复调用
shared_ptr<Person> FastCreateFactory(InvestType type)
{
    // 定义一个做缓存的容器,注意这里存的内容是weak_ptr
    // 使用weak_ptr的好处是,它不会影响所指涉对象的引用计数
    // 如果这里改为shared_ptr的话,则函数外边永远不会析构掉这个对象, 
    // 因为缓存中至少保证其引用计数为1。这就背离的我们的设计
    static unordered_map<InvestType, weak_ptr<Person>> s_cache;
    // 将weak_ptr生成shared_ptr
    auto sp = s_cache[type].lock();

    // 如果缓存中没有的话,则调用工厂函数创建一个新对象,并且加入到缓存中去
    if (!sp) {
        cout << "create new person..\n";
        sp = CreateFactory(type);
        s_cache[type] = sp;
    }

    return sp;
}

int main() {
    {
        auto pInv = FastCreateFactory(InvestType::INVEST_TYPE_ENGINEER);
        pInv->Action();
    }
    cout << "-------------------------------\n";
    {
        auto pInv = FastCreateFactory(InvestType::INVEST_TYPE_ENGINEER);
        pInv->Action();
    }
    cout << "-------------------------------\n";
    {
        auto pInv = FastCreateFactory(InvestType::INVEST_TYPE_STUDENT);
        pInv->Action();
        auto pInv2 = FastCreateFactory(InvestType::INVEST_TYPE_STUDENT);
        pInv2->Action();
    }
}

由调用者去决定这些对象(工厂函数返回的对象)的生存期(std::shared_ptr做不到),然而函数内存的缓存管理器也需要一个指涉到这些对象的指针(std::unique_ptr做不到),并且缓存管理器的指针需要能够校验它们何时会空悬(裸指针做不到)。所以需要weak_ptr。

观察者设计模式问题

观察者模式就是,当观察者观察到某事件发生时,需要通知监听者进行事件处理的一种设计模式。

在多数实现中,观察者通常都在另一个独立的线程中,这就涉及到在多线程环境中,共享对象的线程安全问题(解决方法就是使用上文的智能指针)。这是因为在找到监听者并让它处理事件时,其实在多线程环境中,肯定不明确此时监听者对象是否还存活,或是已经在其它线程中被析构了,此时再去通知这样的监听者,肯定是有问题的。

也就是说,当观察者运行在独立的线程中时,在通知监听者处理该事件时,应该先判断监听者对象是否存活,如果监听者对象已经析构,那么不用通知,并且需要从map表中删除这样的监听者对象。其中的主要代码为:

#include <iostream>
#include <unordered_map>
#include <memory>
#include <list>

using namespace std;

class Listener {
public:
    ~Listener() {std::cout << "~Listener()" << std::endl;}
    void Action(int id) {
        std::cout << "msg id: " << id << std::endl; 
    }
};

// 存储监听者注册的感兴趣的事件
unordered_map<int, list<weak_ptr<Listener>>> listenerMap;

// 观察者观察到事件发生,转发到对该事件感兴趣的监听者
void dispatchMessage(int msgid) {
  auto it = listenerMap.find(msgid);
  if (it != listenerMap.end()) {
    for (auto it1 = it->second.begin(); it1 != it->second.end(); ++it1) {
      // 智能指针的提升操作,用来判断监听者对象是否存活
      shared_ptr<Listener> ps = it1->lock(); 
       // 监听者对象如果存活,才通知处理事件
      if (ps != nullptr) {
        std::cout << "handle msg: " << msgid << std::endl;
        ps->Action(msgid);
      } else {
        // 监听者对象已经析构,从map中删除这样的监听者对象
        std::cout << "rm msg: " << msgid << std::endl;
        it1 = it->second.erase(it1);
      }
    }
  } else {
      std::cout << "not found msg: " << msgid << std::endl;
  }
}


int main()
{
    std::shared_ptr<Listener> sp = std::make_shared<Listener>();
    weak_ptr<Listener> wp(sp);
    list<weak_ptr<Listener>> lwp;
    lwp.push_back(wp);
    listenerMap[1] = lwp;
    
    dispatchMessage(1);
}

总结

智能指针三兄弟就学习到这里啦,虽然它们性格各异,unque_ptr是独来独往,shared_ptr是左拥右抱,而weak_ptr生来就不是为了单打独斗,了解之后你会发现他总是和shared_ptr出双入对的。

最后,弱指针是另一种智能指针,它们不是直接指向资源,而是指向另一个指针(弱或共享)。弱指针无法直接访问对象,但是它们可以判断该对象是否仍然存在或是否已过期。弱指针可以临时转换为共享指针以访问指向的对象(前提是它仍然存在)。

std::weak_ptr在多线程环境中也是一个了不起的工具,因为它不拥有对象,因此不能阻止在其他线程中删除。而且std::weak_ptr::lock()是原子操作,多线程下是安全的。

参考

(67条消息) C++设计模式 - 观察者Observer模式_大秦坑王的博客-CSDN博客_c++ observer