C++ weak_ptr除了解决循环引用还能做什么?

很多人对std::weak_ptr的认识只是不增加std::shared_ptr的引用计数,可以用来解决std::shared_ptr可能造成的循环引用问题。但是,实际对它的认识还是不够深刻,本文将从几个实际应用场景讲解,深入了解这种智能指针。比如,你是否知道可以通过std::weak_ptr来实现缓存, 还可以用来实现单例模式?

1. 认识std::weak_ptr

1.1 底层原理:

std::weak_ptr是一种智能指针,它指向一个std::shared_ptr管理的对象。但是,它不会增加对象的引用计数,因此,它不会影响对象的生命周期。这种指针的主要作用是协助std::shared_ptr工作,它可以访问std::shared_ptr管理的对象,但是它不拥有该对象。std::weak_ptr可以从std::shared_ptr或另一个std::weak_ptr对象构造,它的构造和析构不会引起引用计数的增加或减少。

cpp 复制代码
std::shared_ptr<int> sp = std::make_shared<int>(10);
std::weak_ptr<int> wp(sp);
std::cout << "wp.use_count() = " << wp.use_count() << std::endl; // 1

1.2 简而言之:

std::weak_ptr只有指向对象的使用权,而没有管理权

2. 如何使用std::weak_ptr

2.1 如何读取引用对象?

weak_ptr对它所指向的shared_ptr所管理的对象没有所有权,不能对它解引用,因此若要读取引用对象,必须要转换成shared_ptr。 C++中提供了lock函数来实现该功能。如果对象存在,lock()函数返回一个指向共享对象的shared_ptr,否则返回一个空shared_ptr

2.2 如何判断weak_ptr指向对象是否存在呢?

weak_ptr提供了一个成员函数expired()来判断所指对象是否已经被释放。如果所指对象已经被释放,expired()返回true,否则返回false。

程序示例:

c++ 复制代码
std::shared_ptr<int> sp1(new int(22));
std::shared_ptr<int> sp2 = sp1;
std::weak_ptr<int> wp = sp1; // point to sp1
std::cout<<wp.use_count()<<std::endl; // 2
if(!wp.expired()){
    std::shared_ptr<int> sp3 = wp.lock();
    std::cout<<*sp3<<std::endl; // 22
}

2.3 由std::weak_ptr构造std::shared_ptr

std::weak_ptr可以作为std::shared_ptr的构造函数参数,但如果std::weak_ptr指向的对象已经被释放,那么std::shared_ptr的构造函数会抛出std::bad_weak_ptr异常。

C++ 复制代码
std::shared_ptr<int> sp1(new int(22));
std::weak_ptr<int> wp = sp1; // point to sp1
std::shared_ptr<int> sp2(wp);
std::cout<<sp2.use_count()<<std::endl; // 2
sp1.reset();
std::shared_ptr<int> sp3(wp); // throw std::bad_weak_ptr

2.4 std::weak_ptr 一些内置方法

方法 用途
use_count() 返回与之共享对象的shared_ptr的数量
expired() 检查所指对象是否已经被释放
lock() 返回一个指向共享对象的shared_ptr,若对象不存在则返回空shared_ptr
owner_before() 提供所有者基于的弱指针的排序
reset() 释放所指对象
swap() 交换两个weak_ptr对象

3. 应用场景

3.1 用于实现缓存

weak_ptr可以用来缓存对象,当对象被销毁时,weak_ptr也会自动失效,不会造成野指针。

假设我们有一个Widget类,我们需要从文件中加载Widget对象,但是Widget对象的加载是比较耗时的。

cpp 复制代码
std::shared_ptr<Widget> loadWidgetFromFile(int id); 
// a factory function which returns a shared_ptr, which is expensive to call
// may perform file or database I/O

因此,我们希望Widget对象可以缓存起来,当下次需要Widget对象时,可以直接从缓存中获取,而不需要重新加载。这个时候,我们就可以使用std::weak_ptr来缓存Widget对象,实现快速访问。如以下代码所示:

cpp 复制代码
std::shared_ptr<Widget> fastLoadWidget(int id) {
    static std::unordered_map<int, std::weak_ptr<Widget>> cache; // long lifetime
    auto objPtr = cache[id].lock(); 
    if (!objPtr) {
        objPtr = loadWidgetFromFile(id);
        cache[id] = objPtr; // use std::shared_ptr to construct std::weak_ptr
    }
    return objPtr;
}

当对应id的Widget对象已经被缓存时,cache[id].lock()会返回一个指向Widget对象的std::shared_ptr,否则cache[id].lock()会返回一个空的std::shared_ptr,此时,我们就需要重新加载Widget对象,并将其缓存起来,这一步会由std::shared_ptr构造std::weak_ptr

为什么不直接存储std::shared_ptr呢?静态的std::unordered_map具有长生命周期,其中存储的std::shared_ptr会导致缓存中的对象永远不会被销毁,因为std::shared_ptr的引用计数永远不会为0。而std::weak_ptr不会增加对象的引用计数,因此,当缓存中的对象没有被其他地方引用时,std::weak_ptr会自动失效,从而导致缓存中的对象被销毁。

3.2 避免循环引用

std::weak_ptr可以用来解决使用std::shared_ptr时可能导致的循环引用问题。

  • 什么是"循环引用" ?

循环引用是指两个或多个对象之间通过shared_ptr相互引用,形成了一个环,导致它们的引用计数都不为0,从而导致内存泄漏。

在观察者模式中使用shared_ptr可能会出现循环引用,在下面的程序中,Observer对象和Subject对象相互引用,导致它们的引用计数都无法减到0,从而可能导致内存泄漏。

C++ 复制代码
class IObserver {
public:
    virtual void update(const string& msg) = 0;
};

class Subject {
public:
    void attach(const std::shared_ptr<IObserver>& observer) {
        observers_.emplace_back(observer);
    }
    void detach(const std::shared_ptr<IObserver>& observer) {
        observers_.erase(std::remove(observers_.begin(), observers_.end(), observer), observers_.end());
    }
    void notify(const string& msg) {
        for (auto& observer : observers_) {
            observer->update(msg);
        }
    }
private:
    std::vector<std::shared_ptr<IObserver>> observers_;
};

class ConcreteObserver : public IObserver {
public:
    ConcreteObserver(const std::shared_ptr<Subject>& subject) : subject_(subject) {}
    void update(const string& msg) override {
        std::cout << "ConcreteObserver " << msg<< std::endl;
    }
private:
    std::shared_ptr<Subject> subject_;
};

int main() {
    std::shared_ptr<Subject> subject = std::make_shared<Subject>();
    std::shared_ptr<IObserver> observer = std::make_shared<ConcreteObserver>(subject);
    subject->attach(observer);
    subject->notify("update");
    return 0;
}
  • std::weak_ptr避免循环引用的方法

将Observer类中的subject_成员变量改为weak_ptr,这样就不会导致内存无法正确释放了。

3.3 用于实现单例模式

单例模式是指一个类只能有一个实例,且该类能自行创建这个实例的一种模式。单例模式的实现方式有很多种,其中一种就是使用std::weak_ptr

cpp 复制代码
class Singleton {
public:
    static std::shared_ptr<Singleton> getInstance() {
        std::shared_ptr<Singleton> instance = m_instance.lock();
        if (!instance) {
            instance.reset(new Singleton());
            m_instance = instance;
        }
        return instance;
    }
private:
    Singleton() {}
    static std::weak_ptr<Singleton> m_instance;
};

std::weak_ptr<Singleton> Singleton::m_instance;

std::weak_ptr实现单例模式的优点:

  1. 避免循环应用:避免了内存泄漏。
  2. 访问控制:可以访问对象,但是不会延长对象的生命周期。
  3. 可以在单例对象不被使用时,自动释放对象。

3.4 用于实现enable_shared_from_this模板类

std::enable_shared_from_this是一个模板类,它的作用是为了解决在类成员函数中获取std::shared_ptr的问题。它提供了一个成员函数shared_from_this(),该函数返回一个指向当前对象的std::shared_ptr

  • 作用:用于在类对象的内部中获得一个指向当前对象的 shared_ptr 对象

  • 解决问题: 如果通过this指针创建shared_ptr时,相当于通过一个裸指针创建shared_ptr,多次创建会导致多个shared_ptr对象管理同一个内存。当shared_ptr对象销毁时,会释放this指向的内存,但是this指针可能还会被使用,导致程序崩溃。

C++ 复制代码
class A {
public:
    std::shared_ptr<A> get_shared_ptr() {
        return std::shared_ptr<A>(this); // error
    }
};
  • 使用方法: 继承enable_shared_from_this类;通过shared_from_this()方法返回。
C++ 复制代码
class A : public std::enable_shared_from_this<A> {
public:
    std::shared_ptr<A> get_shared_ptr() {
        return shared_from_this();
    }
};
  • 原理:在类中维护一个weak_ptr,将weak_ptr作为参数传入shared_ptr的构造函数,返回一个shared_ptr对象。
C++ 复制代码
template<typename _Tp>
class enable_shared_from_this
{
protected:
    constexpr enable_shared_from_this() noexcept = default;
    enable_shared_from_this(const enable_shared_from_this&) noexcept = default;
    enable_shared_from_this& operator=(const enable_shared_from_this&) noexcept = default;
    ~enable_shared_from_this() = default;
public:
    shared_ptr<_Tp> shared_from_this()
    {
        shared_ptr<_Tp> __p(_M_weak_this);
        return __p;
    }

    shared_ptr<const _Tp> shared_from_this() const
    {
        shared_ptr<const _Tp> __p(_M_weak_this);
        return __p;
    }

    weak_ptr<_Tp> weak_from_this() noexcept // C++17
    {
        return _M_weak_this;
    }

    weak_ptr<const _Tp> weak_from_this() const noexcept // C++17
    {
        return _M_weak_this;
    }

    template<typename _Up> friend class shared_ptr;
};
  • 限制:只能用于继承自enable_shared_from_this的类。

  • 适用场景:在类的内部需要获得一个指向当前对象的shared_ptr对象时,可以使用enable_shared_from_this模板类。

总结

本文主要介绍了std::weak_ptr的底层原理和使用方法,以及它的几个应用场景,比如实现缓存、 避免循环引用、实现单例模式、实现enable_shared_from_this模板类等。std::weak_ptr是一种智能指针,它指向一个std::shared_ptr管理的对象。但是,它不会增加对象的引用计数,因此,它不会影响对象的生命周期。这种指针的主要作用是协助std::shared_ptr工作,它可以访问std::shared_ptr管理的对象,但是它不拥有该对象。

参考

  1. Effective Modern C++: 42 Specific Ways to Improve Your Use of C++11 and C++14. Scott Meyers. O'Reilly Media. 2014.
  2. en.cppreference.com/w/cpp/memor...
  3. en.cppreference.com/w/cpp/memor...

你好,我是七昂,计算机科学爱好者,致力于分享C/C++、操作系统、系统架构等计算机基础知识。希望我们能一起探索程序员修炼之道,最终能站得更高,走得更远。如果你有任何问题或者建议,欢迎随时与我交流。如果我的创作内容对您有帮助,请点赞关注。感谢你的阅读。

相关推荐
唐诺4 小时前
几种广泛使用的 C++ 编译器
c++·编译器
冷眼看人间恩怨5 小时前
【Qt笔记】QDockWidget控件详解
c++·笔记·qt·qdockwidget
红龙创客5 小时前
某狐畅游24校招-C++开发岗笔试(单选题)
开发语言·c++
Lenyiin5 小时前
第146场双周赛:统计符合条件长度为3的子数组数目、统计异或值为给定值的路径数目、判断网格图能否被切割成块、唯一中间众数子序列 Ⅰ
c++·算法·leetcode·周赛·lenyiin
yuanbenshidiaos7 小时前
c++---------数据类型
java·jvm·c++
十年一梦实验室7 小时前
【C++】sophus : sim_details.hpp 实现了矩阵函数 W、其导数,以及其逆 (十七)
开发语言·c++·线性代数·矩阵
taoyong0017 小时前
代码随想录算法训练营第十一天-239.滑动窗口最大值
c++·算法
这是我588 小时前
C++打小怪游戏
c++·其他·游戏·visual studio·小怪·大型·怪物
fpcc8 小时前
跟我学c++中级篇——C++中的缓存利用
c++·缓存
呆萌很8 小时前
C++ 集合 list 使用
c++