【C++】智能指针

【C++】智能指针
前言

        上文我们学到了C++11的异常,了解到了C++与C语言处理错误的区别,异常的特点在于抛出与接收。【C++11】异常-ZEEKLOG博客

        本文我们来学习C++中的下一个功能:智能指针

1.智能指针的使用场景

        在上文我们知道了抛异常的知识,抛异常的“抛”这个动作一般来说是当程序出现了错误,抛出错误信息为了让我们解决。这个原本是解决错误的动作,在某些时候却称为了“铸就”错误的是罪魁祸首。

        比如:我们知道执行throw,这意味着在这个局部域中throw后面的语句将不再执行,跳过一段又一段程序直到找到匹配的catch时,才会从catch这个语句进行向下执行。那么一个局部域中如果在抛出异常时申请了空间,明明可以正常销毁的,但是却因为抛异常跳过了销毁空间的语句。这就导致一个及其严重的事故:内存泄漏!

        在此之前,为了防止出现内存泄漏。我们通常是将抛出的异常再次捕获,执行销毁语句后,将异常重新抛出。但是这种方法并不太好用,所以为了更好的解决这个问题:智能指针诞生了。

2.RAII和智能指针的设计思路 

        RAII(Resource Acquisition Is Initialization)是资源获取立即初始化的缩写。RAII是一种资源管理的类的设计思想,其本质就利用类的声明周期来管理资源,类的生命周期没结束时一直保持资源有效,类的生命周期结束时通过析构函数来释放资源。这样就可以保证出现上述情况时,资源可以正常的销毁,避免内存泄漏。(这里的资源可以是内存、文件指针、网络连接、互斥锁等等)

        智能指针除了会满足RAII的设计思路,还有考虑到访问资源的便捷性,所以智能指针还会重载operator * / operator -> / operator [ ]等运算符,便于访问。

#include<iostream> using namespace std; template<class T> class Smartptr { public: Smartptr(T* ptr) :_ptr(ptr) { } ~Smartptr() { cout << "~Smartptr" << endl; delete[] _ptr; _ptr = nullptr; } T* operator-> () { return _ptr; } T& operator* () { return *_ptr; } T& operator[] (size_t i) { return _ptr[i]; } private: T* _ptr; }; double Divide(int a, int b) { //当除数为0时抛异常 if (b == 0) { string s = "除数为0"; throw s; } return (double)a / b; } void Func() { //使用智能指针,抛异常后也可以正常释放 Smartptr<int> p1 = new int[10]; for (int i = 0; i < 10; i++) p1[i] = i; int a, b; cin >> a >> b; cout << Divide(a, b)<<endl; } int main() { try { Func(); } catch (const string& errid) { cout << errid << endl; } }

        当Func函数生命周期结束时,Smartptr会自动调用析构函数实现资源是释放。 

3.标准库中智能指针的使用

        标准库中的智能指针包含在头文件 <memory>

        智能指针是用于管理资源的,因此对于智能指针的拷贝来说,我们期望是拷贝出现的智能指针和和原指针共同管理这块资源的,而不是让拷贝出来的资源自己又去管理一个新资源。所以智能指针的拷贝只能是浅拷贝。浅拷贝就会面对一个问题:多次析构,而下面之所以有这么多不同的智能指针正式因为解决多次析构的方法不同导致的。

        auto_ptr,这个是C++98中提供的智能指针。它的特点是拷贝时将被拷贝对象的管理权转移给拷贝对象,这会导致拷贝对象悬空,当我们访问拷贝对象时就会报错。这是一个非常不好的设计,许多公司都是明令禁止使用这个智能指针的。

        unique_ptr,是C++11中提供的智能指针。其特点是不支持拷贝,只支持移动。如果不需要拷贝的情景下,非常推荐使用这个

        share_ptr,是C++11中提供的智能指针。其特点是支持拷贝,也支持移动。如果需要拷贝的场景推荐使用这个。其底层是使用引用计数实现的。

        weak_ptr,是C++11中提供的智能指针。虽然叫做智能指针但不太算得上,因为weak_ptr并不支持RAII,也就意味着weak_ptr并不能管理资源。weak_ptr的主要作用在于解决share_ptr的缺陷:循环引用。而循环引用带来的是内存泄漏。

#include<iostream> #include<memory> using namespace std; struct Date { Date(int year = 0,int month = 0,int day = 0) :_day(day) ,_month(month) ,_year(year) { } ~Date() { cout << "~Date" << endl; } int _day; int _month; int _year; }; int main() { auto_ptr<Date> ap1(new Date); //拷贝时,拷贝对象安排ap1会被悬空 auto_ptr<Date> ap2(ap1); //此时访问ap1就会报错 //ap1->_day; unique_ptr<Date> up1(new Date); //不支持拷贝 //unique_ptr<Date> up2(up1); // //支持移动,但是移动后up1也被悬空 unique_ptr<Date> up2(move(up1)); shared_ptr<Date> sp1(new Date); //支持拷贝 shared_ptr<Date> sp2(sp1); shared_ptr<Date> sp3(sp2); cout << sp1.use_count() << endl; cout << sp1->_year << endl; cout << sp2->_year << endl; cout << sp3->_year << endl; // ⽀持移动,但是移动后sp1也悬空,所以使用移动要谨慎 shared_ptr<Date> sp4(move(sp1)); }

         智能指针析构时默认是使用delete释放资源,这就意味这当资源不是通过new申请时,将资源交给智能指针,析构时就会出问题。

        为了解决这一问题,智能指针支持在构造函数里面给一个删除器。删除器其实就是一个可调用对象,我们按照自己想释放资源的方式实现删除器。智能指针接收后就会按照删除里实现的逻辑进行释放资源。

        因为使用delete[]释放资源的情况十分常用。使用库里面给我们专门特化了这个情况(share_ptr、unique_ptr均有特化版本)。只需要单独在尖括号里面加一对中括号即可。

        值得一提的是,删除器只能在构造时给出,并且后续不能修改。当shared_ptr利用已经存在的对象拷贝构造/赋值给新对象,这个新对象会继承其删除器,即使这并没有显示的写出。而unique_ptr则是将删除器移动给新对象。

 shared_ptr<int[]> sp(new int[10]); shared_ptr<Date[]> sp(new Date[10]);
#include<iostream> #include<memory> using namespace std; //总结:使用shared_ptr,建议传Lambda和函数 // 而使用unique_ptr,建议传Lambda struct Date { int _year; int _month; int _day; Date(int year = 1, int month = 1, int day = 1) :_year(year) , _month(month) , _day(day) { } ~Date() { cout << "~Date()" << endl; } }; template<class T> struct Delete { void operator()(T* ptr) { delete[] ptr; } }; int main() { //直接传仿函数 shared_ptr<Date> sp1(new Date[10], Delete<Date>()); //给出Lambda shared_ptr<Date> sp2(new Date[10], [](Date* ptr) {delete[] ptr; }); //值得一提的是unique_ptr与shared_ptr给出删除器的方法是不同的 //由上面我们可以知道shared_ptr是在构造函数里面给出的 //而unique_ptr的删除器是要在模板参数中给出,这就有点搞笑了 unique_ptr<Date, Delete<Date>> up1(new Date[10]); //传函数到还可以,但是这个相传Lambda就麻烦了,因为Lambda并非类型而是对象 //unique_ptr < Date, [](Date* ptr) {delete[] ptr; } > up2(new Date[10]); //具体要这样写才行,大家了解即可 auto la = [](Date* ptr) {delete[] ptr; }; unique_ptr< Date, decltype(la)> up2(new Date[10],la); }

         shared_ptr除了支持用指针构造,还支持使用make_shared直接构造。其好处是避免了空间的碎片化,因为shared_ptr内部成员除了指针还有一个引用计数。使用指针构造只是初始化了指针,还有一个引用计数没有初始化,为此编译器还会再去开辟空间用于初始化引用计数,而要是这种情况过多就会导致空间碎片化,不利用空间利用。而使用make_shared时就会将指针连同引用计数一起开辟,让这两个指针指向的空间连续。

        shared_ptr与unique_ptr都支持operator bool的类型转化。当智能指针的对象是一个空对象,没有管理资源,就会返回false,反之返回true。所以我们可以把智能指针的对象给if让其判断是否为空对象。

        shared_ptr和unique_ptr的构造函数都是用explicit修饰的,其目的是为了防止普通的指针隐式类型转换为智能指针类型。

int main() { shared_ptr<Date> sp1 = make_shared<Date>(1, 2, 3); auto sp2 = make_shared<Date>(1, 2, 3); //自动推导 shared_ptr<Date> sp3; if (sp2) cout << "不是空对象" << endl; if (!sp3) cout << "是空对象" << endl; //报错,不允许隐式类型转化 shared_ptr<Date> sp4 = new Date(1, 2, 3); shared_ptr<Date> sp5 = new Date(1, 2, 3); //仅支持显示类型转化 shared_ptr<Date> sp6 = shared_ptr<Date>(new Date(1, 2, 3)); }

4.智能指针原理

        下面将模拟实现三个智能指针的实现思路

        auto_ptr和unique_ptr比较简单。auto_ptr会将管理权转移,不建议使用。unique_ptr不支持拷贝,仅支持移动,我们将拷贝禁用掉即可。

        重点关注:share_ptr。share_ptr是靠引用计数实现的,share_ptr内部会有一个计数器记录有多少个指针共同管理当前资源。初始化为1,当拷贝时就将计数器++。析构时,当计数器不为1时仅将当前指针赋值为nullptr,当计数器为1时将计数器和资源释放。

//auto_ptr模拟实现 //其特点是管理权转移,不建议使用! template<class T> class auto_ptr { auto_ptr(T* ptr) :_ptr(ptr) { } //管理权限转移 auto_ptr(const auto_ptr<T>& ap) :_ptr(ap._ptr) { ap._ptr = nullptr; } auto_ptr<T>& operator=(const auto_ptr<T>& ap) { //检查是否赋值给自己 if (_ptr != ap._ptr) { //释放当前的资源 if (_ptr) delete _ptr; _ptr = ap._ptr; ap._ptr = nullptr; } } ~auto_ptr() { cout << "~auto_ptr" << endl; delete _ptr; _ptr = nullptr; } //像指针一样使用 T* operator->() { return _ptr; } T& operator*() { return *_ptr; } private: T* _ptr = nullptr; };
//unique_ptr模拟实现 //unique_ptr不支持拷贝,仅支持移动 template<class T> class unique_ptr { unique_ptr(T* ptr) :_ptr(ptr) {} //不支持拷贝(禁用) unique_ptr(const unique_ptr<T>& up) = delete; unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete; //支持移动 unique_ptr(unique_ptr<T>&& up) :_ptr(up._ptr) { up._ptr = nullptr; } unique_ptr<T>& operator=(unique_ptr<T>&& up) { if (_ptr != up._ptr) { if (_ptr) delete _ptr; _ptr = up._ptr; up._ptr = nullptr; } } ~unique_ptr() { cout << "~unique_ptr()" << endl; delete _ptr; _ptr = nullptr; } //像指针一样使用 T* operator->() { return _ptr; } T& operator*() { return *_ptr; } private: T* _ptr = nullptr; };
//模拟实现shared_ptr //其特点是支持拷贝、也支持移动,其底层是通过引用计数实现的 //这里添加了上述我们所将的删除器 template<class T> class shared_ptr { shared_ptr(T* ptr) :_ptr(ptr) , _num(new int(1)) { } //添加删除器 template<class D> shared_ptr(T* ptr,D del) :_ptr(ptr) ,_num(new int(1)) ,_del(del) { } shared_ptr(const shared_ptr<T>& sp) :_ptr(sp._ptr) ,_num(sp._num) ,_del(sp._del) { *(_num)++; } shared_ptr<T>& operator=(shared_ptr<T>& sp) { if (_ptr != sp._ptr) { //如果当前引用计数只有1时,直接释放资源 if (_num == 1) { delete _ptr; delete _num; _ptr = _num = nullptr; } _ptr = sp._ptr; *(_num)--; _num = sp._num; *(_num)++; _del = sp._del; } //返回*this主要是为了实现链式操作:a=b=c return *this; } ~shared_ptr() { if (*(_num) != 1) { *(_num)--; _ptr = nullptr; } else { _del(_ptr); delete _num; _num = _ptr = nullptr; } } int use_count() { return *_num; } T& operator*() { return *_ptr; } T* operator->() { return _ptr; } private: T* _ptr; int* _num; //引用计数 //_del的类型是不确定的,利用包装器实现 function<void(T*)> _del = [](T* ptr) {delete ptr; }; };

5.weak_ptr与shared_ptr

5.1share_ptr的循环引用

        目前有两个share_ptr对象:sp1、sp2。这两个的引用计数分别都为1。当sp1指向sp2时,sp2的引用计数为2,当sp2也指向sp1时,sp1的引用计数为2。这时程序结束,sp1进行销毁,引用计数不为1,仅进行减1操作。sp2进行销毁,引用计数不为1,仅进行减1操作。此时销毁操作结束,资源没有被释放,造成了内存泄漏。这就是循环引用带来的问题。

using namespace std; struct ListNode { int _data; shared_ptr<ListNode> _next; shared_ptr<ListNode> _prev; // 这里改成weak_ptr,当n1->_next = n2时便于赋值 ~ListNode() { cout << "~ListNode()" << endl; } }; int main() { shared_ptr<ListNode> sp1(new ListNode); shared_ptr<ListNode> sp2(new ListNode); cout << sp1.use_count() << endl; cout << sp2.use_count() << endl; //循环引用 sp1->_next = sp2; sp2->_prev = sp1; //引用计数增加至2 cout << sp1.use_count() << endl; cout << sp2.use_count() << endl; }

         我们可以看到结果并没有调用析构函数。则表明循环引用的出现导致了内存泄漏。而C++11为了解决这一问题,设计出了weak_ptr。

5.2weak_ptr

        weak_ptr严格意义上并不算智能指针,因为weak_ptr并不支持RAII,也不支持访问资源。weak_ptr在初始化的时候仅支持使用share_ptr初始化。而weak_ptr并不会增加shared_ptr的引用计数,这就完美解决了上面导致循环引用的原因。

struct ListNode { int _data; //shared_ptr<ListNode> _next; //shared_ptr<ListNode> _prev; // 这里改成weak_ptr,当n1->_next = n2;绑定shared_ptr时 // 不增加n2的引用计数,不参与资源释放的管理,就不会形成循环引用了 weak_ptr<ListNode> _next; weak_ptr<ListNode> _prev; ~ListNode() { cout << "~ListNode()" << endl; } }; int main() { shared_ptr<ListNode> sp1(new ListNode); shared_ptr<ListNode> sp2(new ListNode); cout << sp1.use_count() << endl; cout << sp2.use_count() << endl; //未使用weak_ptr将导致循环引用 sp1->_next = sp2; sp2->_prev = sp1; //未使用weak_ptr引用计数将增加至2 cout << sp1.use_count() << endl; cout << sp2.use_count() << endl; }

 

         weak_ptr是没有重载operator*/operator->之类的运算符的,weak_ptr是不支持访问资源的,因为如果当weak_ptr绑定shared_ptr的资源已经释放了,这个时候weak_ptr再去访问就很危险了。 

        weak_ptr支持expired检查指向的资源是否过期,weak_ptr也可以使用use_count得到shared_ptr的引用计数个数

        如果weak_ptr想要访问资源可以使用lock,lock会返回一个管理资源的shared_ptr。如果资源已经释放,则返回一个空对象。如果没有释放则返回一个shared_ptr的对象。通过lock返回的shared_ptr对象访问资源是安全的。

#include<iostream> #include<memory> using namespace std; int main() { shared_ptr<int> sp1(new int(1)); shared_ptr<int> sp2(sp1); weak_ptr<int> wp1(sp2); cout << wp1.use_count() << endl; //weak_ptr不增加引用计数 cout << wp1.expired() << endl; //资源没有被释放,有效 //通过lock访问资源 auto sp = wp1.lock(); *sp += 9; cout << *sp << endl; cout << *sp1 << endl; //资源被释放后,无效 //引用计数不断的减少,为0时资源释放 sp1 = make_shared<int>(1); cout << wp1.use_count() << endl; cout << wp1.expired() << endl; sp2 = make_shared<int>(1); cout << wp1.use_count() << endl; cout << wp1.expired() << endl; }

Read more

手搓简易 Linux 进程池:从 0 到 1 实现基于管道的任务分发系统

手搓简易 Linux 进程池:从 0 到 1 实现基于管道的任务分发系统

🔥草莓熊Lotso:个人主页 ❄️个人专栏: 《C++知识分享》《Linux 入门到实践:零基础也能懂》 ✨生活是默默的坚持,毅力是永久的享受! 🎬 博主简介: 文章目录 * 前言: * 一. 核心设计思路 * 二. 代码模块拆解 * 2.1 任务定义与随机任务生成 * 2.2 子进程任务处理逻辑 * 2.3 通道(Channel)类:封装父子进程通信 * 2.4 进程池(ProcesspPool)类:核心管理逻辑 * 2.5 主函数:进程池使用示例 * 三. 关键知识点解析 * 3.1 管道通信原理 * 3.2 轮询负载均衡 * 3.3 进程回收的坑

By Ne0inhk
Flutter 三方库 swagger_parser 自动化打通鸿蒙 API 通信(一键将 Swagger 转化为 Dart 模型)

Flutter 三方库 swagger_parser 自动化打通鸿蒙 API 通信(一键将 Swagger 转化为 Dart 模型)

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net 前言 在进行 OpenHarmony 项目开发时,最枯燥的工作莫过于根据后端提供的 Swagger (OpenAPI) 文档手动编写一个个的 Request 类、Response 类和 API Client。这不仅低效,而且极易因文档更新没对齐而导致 Bug。 swagger_parser 是一个强大的命令行工具,它能直接读取本地或网络上的 Swagger JSON/YAML 文件,自动为你生成完整的 Dart 数据类和 Dio/Chopper API 控制器。 一、核心工作流 Swagger JSON / YAML Swagger Parser Dart 数据模型 (JSON Serialized) Dio / Chopper

By Ne0inhk

【汉化中文版】OpenClaw(Clawdbot/Moltbot)第三方开源汉化中文发行版部署全指南:一键脚本/Docker/npm 三模式安装+Ubuntu 环境配置+中文汉化界面适配开源版

OpenClaw这是什么? OpenClaw(曾用名 Clawdbot / Moltbot)是一个开源的个人 AI 助手平台(GitHub 120k+ Stars),可以通过 WhatsApp、Telegram、Discord 等聊天软件与 AI 交互。简单说就是:在你自己的机器上运行一个 AI 助手,通过常用聊天软件跟它对话。 forks项目仓库 :https://github.com/MaoTouHU/OpenClawChinese 文章目录 * OpenClaw这是什么? * 汉化效果预览 * 环境要求 * 安装方式 * 方式 A:一键脚本(推荐新手) * 方式 B:npm 手动安装 * 方式 C:Docker 部署(服务器推荐) * 首次配置 * 运行初始化向导 * 安装守护进程(

By Ne0inhk
易语言核心自动化场景实战:办公、测试、数据抓取与基础游戏脚本开发

易语言核心自动化场景实战:办公、测试、数据抓取与基础游戏脚本开发

十一、易语言核心自动化场景实战:办公、测试、数据抓取与基础游戏脚本开发 11.1 引言 💡 自动化是易语言最受欢迎、应用最广泛的核心场景之一!很多人学习易语言的直接动力,就是为了解放双手,提高工作效率——比如批量处理Excel报表、自动登录办公系统、定时发送邮件、自动抓取网页数据,甚至为自己喜欢的单机/绿色窗口游戏写简单的挂机脚本。 前10篇我们已经掌握了易语言的基础语法、组件库、网络通信、数据库操作、多线程优化和高级底层编程,这些都是自动化开发的技术基础。本章将重点讲解四大高频自动化场景的全流程实战开发,帮助大家将所学知识应用到实际工作和生活中。 11.1.1 学习目标 * 掌握Windows GUI自动化的核心原理(窗口句柄查找、控件操作、消息发送) * 学会使用精易模块和大漠插件实现自动化操作 * 完成办公自动化实战(批量生成工资条、自动发送会议通知邮件) * 完成网页数据抓取自动化实战(自动抓取天气数据、自动抓取招聘网站职位信息) * 完成基础游戏脚本开发实战(⚠️仅用于学习,严禁开发/传播违法违规的游戏外挂) * 掌握自动化项目的通用开发流程,形成自

By Ne0inhk