C++之智能指针

一、智能指针的使用及其场景分析

        下面程序中我们可以看到,new了以后,我们也delete了,但是因为抛异常导致后面的delete没有得到执行而导致内存泄漏。所以我们需要new以后再捕获异常,捕获到异常后delete内存,再把异常抛出,但是new本身也可能抛异常,连续的两个new和下面的Divide都可能抛异常,让我们处理起来很麻烦,智能指针就是来解决这种由于异常会跳转导致的内存泄漏问题。

#define _CRT_SECURE_NO_WARNINGS #include<iostream> using namespace std; double Divide(int x, int y) { if (y == 0) { string s("y is zero"); throw(s); } else { return (double)x / (double)y; } } void Func() { //若除数等于0,抛出异常,则array1和array2都没有释放掉。 //这里捕获后并不处理,只是先释放内存,然后交给外面处理。 //但如果array2抛异常,array1就不能被释放掉了,还得有一层捕获释放的逻辑 //这里就出来智能指针这种更好的解决方式 int* array1 = new int[10]; int* array2 = new int[10]; try { int a, b; cin >> a >> b; cout << Divide(a, b) << endl; } catch (...) { cout << "delete []" << array1 << endl; cout << "delete []" << array2 << endl; delete[] array1; delete[] array2; throw; // 异常重新抛出,捕获到什么抛出什么 } cout << "delete []" << array1 << endl; delete[] array1; cout << "delete []" << array2 << endl; delete[] array2; } int main() { try { Func(); } catch (const char* errmsg) { cout << errmsg << endl; } catch (const exception& e) { cout << e.what() << endl; } catch (...) { cout << "Unknow Errmsg" << endl; } }

二、RALL和智能指针的设计思路

        *RALL是Resource Acquisition Is Initialization的缩写,它是一种管理资源的类的设计思想,本质是一种利用对象生命周期来管理获取到的动态资源,避免资源泄露。这里资源代指内存、文件指针、网络连接,互斥锁等。RALL在获取资源时把资源委托给一个对象,接着控制对资源的访问,资源在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源,这样保证了资源的正常释放,避免资源泄露问题。

        *智能指针类除了满足RALL设计思路,还要方便资源的访问,所以智能指针还会像迭代器类一样,重载operator*/operator->/operator[]等运算符,方便访问资源。

template<class T> class SmartPtr{ public: SmartPtr(T* ptr) :_ptr(ptr) {} ~SmartPtr() { cout << "delete[]" << _ptr << endl; delete[] _ptr; } T& operator[](int i) { return _ptr[i]; } T& operator*() { return *_ptr; } T* operator->() { return _ptr; } private: T* _ptr; };

三、C++标准库智能指针的使用

        *C++标准库中的智能指针都在<memory>这个头文件下面,我们包含<memory>就是可以使用了,智能指针有多种,除了weak_ptr,其他都符合RALL和向指针一样的访问行为,原理上而言主要是解决智能指针拷贝时的思路不同。

        *auto_ptr是C++98时设计出来的智能指针,他的特点是拷贝时把被拷贝对象的资源管理权转给拷贝对象(这会导致被拷贝对象悬空)可能会发生访问报错,C++11后设计出新的智能指针后,强烈不建议使用auto_ptr

        *unique_ptr是C++11设计出来的智能指针,他的名字翻译出来是唯一指针,他的特点是不支持拷贝,只能移动。如果不需要拷贝的场景就非常适合他。

        *shared_ptr是C++11设计出来的智能指针,他的名字翻译出来是共享指针,他的特点是支持拷贝,也支持移动。底层是用引用计数的方式实现的。

        *weak_ptr是C++11设计出来的智能指针,他的名字翻译出来是弱指针,他与上面的指针完全不同,不支持RALL,也不能直接访问资源,其产生的本质是为了解决shared_ptr的一个循环引用而导致的内存泄漏问题。

        *智能指针析构时默认是进行delete释放资源,这也就意味着如果不是new出来的资源,交给智能指针来管理,析构就会崩溃。智能指针支持在构造的时候给一个删除器,所谓删除器就是一个可调用对象,这个可调用对象中实现你想要的释放资源的方式,当构造智能指针的时候,给了定制的删除器,在智能指针析构时就会调用删除器去释放资源。因为new[]经常使用,所以unique_ptr和shared_ptr都特化了一份[]版本(unique_ptr up1(new Date[5]);shared_ptr sp1(new Date[5])就可以管理new[]资源。

        *template<class T,class... Args>  shared_ptr<T> make_shared (Args&&... args);

        *shared_ptr除了支持指向资源的指针构造,还支持make_shared用初始化资源对象的值直接构造。

        *shared_ptr和unique_ptr都支持operator bool的类型转换,如果智能指针对象是一个空对象没有管理,则返回false

        *shared_ptr和unique_ptr都得构造函数使用explicit修饰,防止普通指针隐式类型转换成智能指针对象。

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; } }; int main() { auto_ptr<Date> ap1(new Date); //拷贝时,管理权限转移,被拷贝对象ap1悬空 auto_ptr<Date> ap2(ap1); //悬空指针,不能访问 //ap1->_day++; unique_ptr<Date> up1(new Date); //不支持拷贝,只支持移动,但移动后也会悬空 //unique_ptr<Date> up2(up1); unique_ptr<Date> up3(move(up1)); shared_ptr<Date> sp1(new Date); //支持拷贝 shared_ptr<Date> sp2(sp1); shared_ptr<Date> sp3(sp2); //sp1sp2sp3指向同一个内容 cout << sp1.use_count() << endl; sp1->_year++; cout << sp1->_year << endl; cout << sp2->_year << endl; cout << sp3->_year << endl; //2 2 2 //支持移动,但移动后也悬空,谨慎使用 shared_ptr<Date> sp4(move(sp1)); return 0; }
template<class T> void DeleteArrayFunc(T* ptr) { delete[] ptr; } template<class T> class DeleteArray { public: void operator()(T* ptr) { delete[] ptr; } }; class Fclose { public: void operator()(FILE* ptr) { cout << "fclose" << ptr << endl; fclose(ptr); } }; int main() { //这样程序会崩溃 //unique_ptr<Date> up1(new Date[10]); //shared_ptr<Date> sp1(new Date[10]); //解决方案一,模版特化 unique_ptr<Date[]> up1(new Date[10]); shared_ptr<Date[]> sp1(new Date[10]); //解决方法二 //仿函数对象做删除器 //unique_ptr<Date, DeleteArray<Date>> up2(new Date[5],DeleteArray<Date>()); //unique_ptr和shared_ptr支持的删除器的方式有所不同 //unique_ptr是在类模板参数支持的,shared_ptr是构造函数参数支持的 //使用仿函数unique_ptr可以不在构造函数传递,因为仿函数类型构造的对象直接就可以调用 //但是下面的函数指针和lambda的类型就不可以 unique_ptr<Date, DeleteArray<Date>> up2(new Date[5]); shared_ptr<Date> sp2(new Date[5], DeleteArray<Date>()); //函数指针做删除器 unique_ptr<Date, void(*)(Date*)> up3(new Date[5], DeleteArrayFunc<Date>); shared_ptr<Date> sp3(new Date[5], DeleteArrayFunc<Date>); //lambda表达式做删除器 auto delArrOBJ = [](Date* ptr) {delete[] ptr; }; unique_ptr<Date, decltype(delArrOBJ)> up4(new Date[5], delArrOBJ); shared_ptr<Date> sp4(new Date[5], delArrOBJ); //实现其他资源管理的删除器 shared_ptr<FILE> sp5(fopen("Test.cpp", "r"), Fclose()); shared_ptr<FILE> sp6(fopen("Test.cpp", "r"), [](FILE* ptr) { cout << "fclose:" << ptr << endl; fclose(ptr); }); }
int main() { shared_ptr<Date> sp1(new Date(2026, 1, 5)); shared_ptr<Date> sp2 = make_shared<Date>(2026, 1, 5); auto sp3= make_shared<Date>(2026, 1, 5); shared_ptr<Date> sp4;//默认构造不管理任何对象 if(sp1.operator bool()) cout << "sp1 is not nullptr" << endl; if (!sp4) cout << "sp1 is nullptr" << endl; // 报错 不能让普通指针隐式类型转化为智能指针 //shared_ptr<Date> sp5 = new Date(2024, 9, 11); //unique_ptr<Date> sp6 = new Date(2024, 9, 11); }

##注意unique_ptr和shared_ptr支持删除器的方式不同

unique_ptr是在类模版参数支持的,shared_ptr是构造函数参数支持的。

在仿函数上:unique_ptr只需要给类模版传仿函数类型就可以,因为仿函数会自动实例化

在函数指针上,unique_ptr既得给类模板传函数指针类型,也得给模版函数传函数指针,因为单单只给类模版传函数指针类型,并不会有一个对应指向实例。

在lambda上:unique_ptr同样既得给模版函数传lambda类型,也得给模版函数传lambda表达式。

四、智能指针的原理

        *下面我们模拟实现一下auto_ptr和unique_ptr的核心功能,这两个智能指针的实现比较简单,大家了解一下原理就可。auto_ptr的思路是拷贝的时候转移管理权给被拷贝对象,这种思路是不被认可的,也不建议使用。unique_ptr的思路是不支持拷贝、

        *大家重点要看shared_ptr是如何设计的,尤其是引用计数的设计,注意这里一份资源就需要一个引用计数,所以引用计数才能用静态成员的方式是无法实现的。要使用堆上动态开辟的方式,构造智能指针对象时来一份资源,就需要new一个引用计数出来。多个shared_ptr指向资源就++引用计数。要析构就--引用计数,引用计数减到0就代表当前析构的shared_ptr是最后一个管理资源的对象,就析构资源。

namespace bit { template<class T> class auto_ptr { public: explicit auto_ptr(T* ptr) :_ptr(ptr) { } explicit auto_ptr(auto_ptr<T>& t) :_ptr(t._ptr) { t._ptr = nullptr; } auto_ptr<T>& operator=(auto_ptr<T>& t) { if (&t != this) {//检测是否给自己赋值 if (_ptr) delete _ptr; //转移资源 _ptr = t._ptr; t._ptr = nullptr; } } T* operator->() { return _ptr; } T& operator[](int i) { return _ptr[i]; } T& operator*() { return *_ptr; } ~auto_ptr() { if (_ptr) { cout << "delete:" << _ptr << endl; delete _ptr; } } private: T* _ptr; }; template<class T> class unique_ptr { public: explicit unique_ptr(T* ptr) :_ptr(ptr) { } unique_ptr(unique_ptr<T>&& up) { if (_ptr) delete _ptr; _ptr = up._ptr; up._ptr = nullptr; } unique_ptr<T>& operator=(unique_ptr<T>&& up) { if (_ptr) delete _ptr; _ptr = up._ptr; up._ptr = nullptr; return *this; } unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete; unique_ptr(const unique_ptr<T>& up) = delete; T* operator->() { return _ptr; } T& operator[](int i) { return _ptr[i]; } T& operator*() { return *_ptr; } ~unique_ptr() { if (_ptr) { cout << "delete:" << _ptr << endl; delete _ptr; } } private: T* _ptr; }; template<class T> class shared_ptr { public: explicit shared_ptr(T* ptr=nullptr) :_ptr(ptr) , _count(new int(1)) { } template<class D> shared_ptr(T* ptr,D del) :_ptr(ptr) ,_del(del) ,_count(new int(1)) { } shared_ptr(const shared_ptr<T>& sp) :_ptr(sp._ptr) ,_count(sp._count) ,_del(sp._del) { (*_count)++; } void release() { if (--(*_count) == 0) { _del(_ptr); delete _count; _ptr = nullptr; _count = nullptr; } } shared_ptr<T>& operator=(const shared_ptr<T>& sp) { if (&sp != this) { release(); //只是更改指针 _ptr=sp._ptr; _count=sp._count; _del=sp._del; (*_count)++; } return *this; } T* operator->() { return _ptr; } T& operator[](int i) { return _ptr[i]; } T& operator*() { return *_ptr; } ~shared_ptr() { release(); } T* get() { return _ptr; } int use_count() { return *_count; } private: int* _count; T* _ptr; //function包装器可以包装函数指针,仿函数,lambda等。 function<void(T*)> _del = [](T* ptr) {delete ptr; }; }; template<class T> class weak_ptr { public: weak_ptr() {} weak_ptr(const shared_ptr<T>& sp) :_ptr(sp.get()) { } weak_ptr<T>& operator=(const shared_ptr<T>& sp) { _ptr = sp.get(); return *this; } private: T* _ptr = nullptr; }; } int main() { bit::auto_ptr<Date> ap1(new Date); //拷贝时,管理权限转移,ap1悬空 bit::auto_ptr<Date> ap2(ap1); bit::unique_ptr<Date> up1(new Date); //不支持拷贝 //bit::unique_ptr<Date> up2(up1); //允许移动,但移动后也会悬空 bit::unique_ptr<Date> up3(move(up1)); bit::shared_ptr<Date> sp1(new Date); bit::shared_ptr<Date> sp2(sp1); bit::shared_ptr<Date> sp3(sp2); bit::shared_ptr<Date> sp5(sp2); bit::shared_ptr<Date> sp4(new Date(2026,1,1)); sp1 = sp4; cout << sp1.use_count() << endl; cout << sp1.use_count() << endl; sp1->_year++; cout << sp1->_year << endl; cout << sp2->_year << endl; cout << sp3->_year << endl; }

五、shared_ptr和weak_ptr

        1.shared_ptr的循环引用问题

        *shared_ptr大多数情况下管理资源非常合适,支持RALL,也支持拷贝。但是在循环引用的场景下,会导致资源没得到释放内存泄漏,所以我们要认识循环引用的场景和资源没有释放的原因,并且学会使用weak_ptr解决问题。

        *如图,n1和n2析构后,管理两个节点的引用计数减到1.

1.右边结点什么时候释放呢,受到左边结点的_next管理,_next析构后,右边结点就释放了。

2._next什么时候析构呢,_next是左边结点的成员,左边结点释放,_next就释放了。

3.左边节点什么时候释放呢,左边节点由右边节点中的_prev管着呢,_prev析构后,左边的节点就释 放了。

4. _prev什么时候析构呢,_prev是右边节点的成员,右边节点释放,_prev就析构了。

*这样就会导致形成回旋镖式的循环引用,谁都不会释放就形成了循环引用,导致内存泄漏

*把ListNode结构体中的next和prev改成weak_ptr,weak_ptr绑定到shared_ptr不会增加引用计数,_next和_prev不参与资源管理释放逻辑,就将循环引用所打破,解决了问题。

struct ListNode { int data; //shared_ptr<ListNode> _prev; //shared_ptr<ListNode> _next; //要改为weak_ptr,当n1->_next=n2;绑定shared_ptr时 //不会增加引用计数,不参与资源释放的管理 std::weak_ptr<ListNode> _next; std::weak_ptr<ListNode> _prev; ~ListNode() { cout<< "~ListNode()" << endl; } }; int main() { std::shared_ptr<ListNode> n1(new ListNode); std::shared_ptr<ListNode> n2(new ListNode); cout << n1.use_count() << endl; cout << n2.use_count() << endl; n1->_next = n2; n2->_prev = n1; cout << n1.use_count() << endl; cout << n2.use_count() << endl; // weak_ptr不⽀持管理资源,不⽀持RAII // weak_ptr是专⻔绑定shared_ptr,不增加他的引⽤计数,作为⼀些场景的辅助管理 //std::weak_ptr<ListNode> wp(new ListNode); return 0; }

2.weak_ptr

        *weak_ptr不支持RALL,也不支持访问资源,所以我们看文档发现weak_ptr构造时不支持绑定到资源,只支持绑定到shared_ptr,绑定到shared_ptr时,不增加引用计数,那么就可以解决上述问题。

       *weak_ptr也没有重载->和[]等,它不参与资源管理,如果她绑定的shared_ptr已经释放了资源,那么他去访问资源是很危险的。weak_ptr支持expired检查指向的资源是否过期,use_count也可获取shared_ptr的引用计数,weak_ptr想访问资源的时候,可以调用lock返回一个管理资源的shared_ptr,如果资源已经释放,返回的是一个空shared_ptr,如果没有释放资源,返回的shared_ptr是安全的。

int main() { shared_ptr<string> sp1(new string("111111")); shared_ptr<string> sp2(sp1); weak_ptr<string> wp = sp1; cout << wp.expired() << endl; cout << wp.use_count() << endl; //sp1 and sp2 指向其他资源,weak_ptr就过期了 sp1 = make_shared<string>("222222"); cout << wp.expired() << endl; cout << wp.use_count() << endl; sp2 = make_shared<string>("333333"); cout << wp.expired() << endl; cout << wp.use_count() << endl; wp = sp1; //wp.lock()是重新产生的 auto sp3 = wp.lock();//返回一个shared_ptr cout << wp.expired() << endl; cout << wp.use_count() << endl; *sp3+= "***"; cout << *sp1 << endl; }

六、 shared_ptr的线程安全问题

        *shared_ptr的引用计数在堆上,如果多个shared_ptr对象在多个线程中进行shared_ptr的拷贝析构的时候就会访问修改引用计数,存在线程安全问题,所以shared_ptr引用计数是需要加锁或者原子操作保证线程安全的。

        *shared_ptr指向的对象也是有线程安全的问题的,但是这个对象的线程安全问题不归shared_ptr 管,它也管不了,应该有外层使用shared_ptr的人进行线程安全的控制。

七、C++11和boost中智能指针的关系

*Boost标准库是为C++语言标准库提供的一些C++程序库的总称,Boost社区建立的初衷之一就是为了C++的标准化工作中提供可供参考的实现,Boost社区发起人Dawes本人就是标准委员会的成员之一。

*C++98中产生了第一个智能指针auto_ptr。

*C++Boost给出了更实用的scoped_ptr/scoped_array和shared_ptr/shared_array和weak_ptr等.

*C++11,引⼊了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr对应boost的 scoped_ptr。并且这些智能指针的实现原理是参考boost中的实现的。

八、内存泄漏

        1.什么是内存泄漏,内存泄漏的危害

        什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不在使用的内存,一般是忘记释放或发生异常导致程序未能正常指向导致的。内存泄漏并不是指内存在物理上的消失,而是由于设计错误,失去了对该内存的控制,而造成内存的浪费

        内存泄漏的危害:普通程序运行一会就结束了出现内存泄漏的问题也不大,进程正常结束,页表的映射关系解除,物理内存也可释放。长期运行的程序出现内存泄漏,影响很大,如操作系统,后台服务,长时间运行的客户端等等,不断出现内存泄漏会导致可用内存不断变少,功能响应变慢,最后卡死。

        2.如何避免内存泄漏

工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理 想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下⼀条智能指针来管理 才有保证。

• 尽量使用智能指针来管理资源,如果自己场景比较特殊,采用RAII思想自己造个轮⼦管理。

 • 定期使用内存泄漏工具检测,尤其是每次项目快上线前,不过有些工具不够靠谱,或者是收费。

• 总结⼀下:内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错 型。如泄漏检测工具

Read more

JavaScript学习笔记:1.JavaScript简介

JavaScript学习笔记:1.JavaScript简介

JavaScript学习笔记:1.JavaScript简介 打开网页时,弹出的欢迎弹窗、滑动时的平滑动画、点击就刷新的投票按钮——这些让静态页面变得生动有趣的互动,背后都藏着同一个“幕后功臣”:JavaScript(简称JS)。这门诞生于网页的脚本语言,如今早已跳出浏览器的“舒适区”,在服务器、手机App、智能设备等多个领域发光发热。今天就来聊聊,这门既灵活又强大的语言,到底是什么来头? 一、什么是JavaScript?——不止于“网页互动” 简单说,JavaScript是一门跨平台、面向对象的脚本语言。先拆解这两个关键属性,你就懂了: * 「跨平台」:不管你用Chrome、Firefox还是Edge浏览器,甚至用Node.js跑在服务器上,JS代码都能正常工作——就像一部好电影,在电影院、电脑、手机上看都不影响体验。 * 「面向对象+脚本语言」:不用像Java、C++那样写复杂的类声明、编译步骤,直接写代码就能运行;同时它又能创建对象、实现继承,兼顾了灵活性和功能性。

By Ne0inhk
Java 大视界 -- Java 大数据分布式计算在基因测序数据分析与精准医疗中的应用(400)

Java 大视界 -- Java 大数据分布式计算在基因测序数据分析与精准医疗中的应用(400)

Java 大视界 -- Java 大数据分布式计算在基因测序数据分析与精准医疗中的应用(400) * 引言: * 正文: * 一、传统基因测序分析的 “三重困局”:慢、漏、贵 * 1.1 数据洪流压垮单机算力 * 1.1.1 测序数据量与算力的矛盾 * 1.1.2 数据存储与复用难题 * 1.2 突变检测漏检率高 * 1.2.1 单机分析的 “算力天花板” * 1.2.2 临床解读与数据脱节 * 1.3 成本高企制约普及 * 1.3.1 硬件与人力成本双高 * 1.3.2 基层医院 “用不起” * 二、

By Ne0inhk
从反射到方法句柄:深入探索Java动态编程的终极解决方案

从反射到方法句柄:深入探索Java动态编程的终极解决方案

🌟 你好,我是 励志成为糕手 ! 🌌 在代码的宇宙中,我是那个追逐优雅与性能的星际旅人。 ✨ 每一行代码都是我种下的星光,在逻辑的土壤里生长成璀璨的银河; 🛠️ 每一个算法都是我绘制的星图,指引着数据流动的最短路径; 🔍 每一次调试都是星际对话,用耐心和智慧解开宇宙的谜题。 🚀 准备好开始我们的星际编码之旅了吗? 目录 摘要  一、Java反射机制基础 1.1 什么是反射? 1.2 Java反射核心类关系图 1.3 反射的核心原理 二、反射核心操作详解 2.1 获取Class对象的三种方式 2.2 动态创建对象实例 2.3 动态调用方法 2.4 动态操作字段 三、反射的典型应用场景 3.1 框架开发(Spring IOC容器) 3.2 动态代理(JDK

By Ne0inhk
【Map vs Set】:Java数据存储的“双子星”对决

【Map vs Set】:Java数据存储的“双子星”对决

个人主页:♡喜欢做梦 欢迎  👍点赞  ➕关注  ❤️收藏  💬评论 目录 🍰一、搜索 🍮1.概念 🍮2.模型 🍰二、Map 🍨1.什么是Map? 🍨2.Map的实例化 🍨3.Map的常见方法 🍨4.Map方法的使用 🍰三、Set 🍯1.什么是Set? 🍯2.Set的常见方法 🍯3.Set方法的使用 🍰四、Map和Set的区别 🍰一、搜索 🍮1.概念 搜索:是指在数据集合过程中查找特定元素或满足特定条件元素的过程。如:在一组数组中查找特定的数字。常见的搜索有直接遍历和二分查找..... 直接遍历和二分查找比较适合静态类型的查找,即一般不会对区间进行插入和删除操作。 所以当需要动态查找时,即查找时要进行一些插入和删除,上述的方法并不适用 。如:在学生系统中,

By Ne0inhk