C++ weak_ptr除了解决循环引用还能做什么?
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
对象构造,它的构造和析构不会引起引用计数的增加或减少。
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。
程序示例:
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
异常。
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对象的加载是比较耗时的。
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对象,实现快速访问。如以下代码所示:
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,从而可能导致内存泄漏。
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
。
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
实现单例模式的优点:
- 避免循环应用:避免了内存泄漏。
- 访问控制:可以访问对象,但是不会延长对象的生命周期。
- 可以在单例对象不被使用时,自动释放对象。
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指针可能还会被使用,导致程序崩溃。
class A {
public:
std::shared_ptr<A> get_shared_ptr() {
return std::shared_ptr<A>(this); // error
}
};
- 使用方法: 继承enable_shared_from_this类;通过shared_from_this()方法返回。
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对象。
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
管理的对象,但是它不拥有该对象。
参考
- Effective Modern C++: 42 Specific Ways to Improve Your Use of C++11 and C++14. Scott Meyers. O'Reilly Media. 2014.
- https://en.cppreference.com/w/cpp/memory/weak_ptr
- https://en.cppreference.com/w/cpp/memory/enable_shared_from_this
你好,我是七昂,计算机科学爱好者,致力于分享C/C++、操作系统、系统架构等计算机基础知识。希望我们能一起探索程序员修炼之道,最终能站得更高,走得更远。如果你有任何问题或者建议,欢迎随时与我交流。如果我的创作内容对您有帮助,请点赞关注。感谢你的阅读。
C++ weak_ptr除了解决循环引用还能做什么?的更多相关文章
- 【C++】智能指针简述(五):解决循环引用的weak_ptr
总结一下前文内容: 1.智能指针通过RAII方法来管理指针:构造对象时,完成资源初始化;析构对象时,对资源进行清理及汕尾. 2.auto_ptr,通过“转移所有权”来防止析构一块内存多次.(如何转移? ...
- 深入研究Block用weakSelf、strongSelf、@weakify、@strongify解决循环引用(上)
深入研究Block捕获外部变量和__block实现原理 前言 在上篇中,仔细分析了一下Block的实现原理以及__block捕获外部变量的原理.然而实际使用Block过程中,还是会遇到一些问题,比如R ...
- swift闭包中解决循环引用的问题
swift中可以通过三种方法解决循环引用的问题 利用类似oc方法解决循环引用weak var weakSelf = self weak var weakSelf = self loadData = { ...
- Flask-分开Models解决循环引用
Flask-分开Models解决循环引用 在之前我们测试中,所有语句都在同一个文件中,但随着项目越来越大,管理起来有所不便,所以将Models分离.基本的文件结构如下 \—–app.py\—–mode ...
- Spring如何解决循环引用
概念 什么是循环引用? 故名思义,多个对象形成环路. 有哪几种循环引用? 在Spring中存在如下几种循环引用,一一举例分析一下 注入循环引用(Set注入 注解注入) package c.q.m; i ...
- block为什么用copy以及如何解决循环引用
在完成项目期间,不可避免的会使用到block,因为block有着比delegate和notification可读性更高,而且看起来代码也会很简洁.于是在目前的项目中大量的使用block. 之前给大家介 ...
- 深入研究Block用weakSelf、strongSelf、@weakify、@strongify解决循环引用(下)
深入研究Block捕获外部变量和__block实现原理 EOCNetworkFetcher.h typedef void (^EOCNetworkFetcherCompletionHandler)(N ...
- 如何在 iOS 中解决循环引用的问题
稍有常识的人都知道在 iOS 开发时,我们经常会遇到循环引用的问题,比如两个强指针相互引用,但是这种简单的情况作为稍有经验的开发者都会轻松地查找出来. 但是遇到下面这样的情况,如果只看其实现代码,也很 ...
- [HMLY]10.深入研究Block用weakSelf、strongSelf、@weakify、@strongify解决循环引用
前言 在上篇中,仔细分析了一下Block的实现原理以及__block捕获外部变量的原理.然而实际使用Block过程中,还是会遇到一些问题,比如Retain Circle的问题. 目录 1.Retain ...
- Swift-闭包使用及解决循环引用问题
Swift中闭包使用参考OC中block使用,基本一致 // 闭包类型 首先写(参数列表)->(返回值类型) func loadData(callBack : (jsonData:String) ...
随机推荐
- SpringBoot 整合 Sharding-JDBC 分库分表
导读 分库分表的技术有:数据库中间件Mycat(点我直达),当当网开源的Sharding-JDBC:我们公司用的也是sharding-jdbc,自己也搭建一个完整的项目,直接可以拿来用.下面附源码(C ...
- 【算法】在vue3的ts代码中分组group聚合源数据列表
有一个IList<any>()对象列表, 示例数据为[{id:'1',fieldName:'field1',value:'1'},{id:'1',fieldName:'field2',va ...
- 将传统应用带入浏览器的开源先锋「GitHub 热点速览」
现代浏览器已经不再是简单的浏览网页的工具,其潜能正在通过技术不断地被挖掘和扩展.得益于 WebAssembly 等技术的出现,让浏览器能够以接近原生的速度执行非 JavaScript 语言编写的程序, ...
- 常用IDE(开发工具)
一.开发工具 Visual Studio Microsoft Visual Studio(简称VS)是微软公司提供的IDE,可以在VS上编写C.C++.C#等多种语言的项目,所写的代码适用于微软支持的 ...
- Nuxt.js头部魔法:轻松自定义页面元信息,提升用户体验
扫描二维码关注或者微信搜一搜:编程智域 前端至全栈交流与成长 useHead 函数概述 useHead是一个用于在 Nuxt 应用中自定义页面头部属性的函数.它由Unhead库提供支持,允许开发者以编 ...
- 移动web开发适配秘籍Rem
目录 移动web开发的特点 Rem 布局适配原理 Media Query(媒体查询) scss 工程使用函数计算 JS动态获取屏幕的宽度 直接将html 的 font-size 设置成 100px 移 ...
- ABC361-D题解
背景 保佑LC能来一中. 题意 给你一个长度为 \(n\) 的初始字符串和目标字符串,都由 W 和 B 两种字符构成. 现在初始字符串末尾接有两个空格字符,每次你可以在该字符串中选出连续两个非空格字符 ...
- Python Kafka客户端confluent-kafka学习总结
实践环境 Python 3.6.2 confluent-kafka 2.2.0 confluent-kafka简介 Confluent在GitHub上开发和维护的confluent-kafka-pyt ...
- Nuxt.js 环境变量配置与使用
title: Nuxt.js 环境变量配置与使用 date: 2024/7/25 updated: 2024/7/25 author: cmdragon excerpt: 摘要:"该文探讨了 ...
- el-date-picker 时间日期格式,选择范围限制
背景:ElementUI的 el-date-picker 使用时,有时候想要限制用户选择的时间范围,但是用的是datetimerange类型的选择器,不想单独写两个起始的选择器.我发现网上的方案大部分 ...