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

【Java ArrayList】从入门到精通:从概念,使用到扩容机制,一篇带你掌握Java动态数组核心

【Java ArrayList】从入门到精通:从概念,使用到扩容机制,一篇带你掌握Java动态数组核心

🎁个人主页:User_芊芊君子 🎉欢迎大家点赞👍评论📝收藏⭐文章 🔍系列专栏:Java.数据结构 【前言】 ArrayList 是 Java 集合框架(Java Collections Framework)中最常用的动态数组实现,它提供了灵活的容量管理、便捷的增删改查操作,广泛应用于日常开发中。本文将深入剖析 ArrayList 的底层结构、核心方法源码、性能特点及最佳实践,帮助读者彻底掌握这一基础数据结构。 文章目录: * 一、ArrayList的概念 * 二、ArrayList的使用 * 1.构造ArrayList * 1.1 无参构造方法 * 1. 2 带有初始容量的构造方法 * 1.3 用其他集合类构造ArrayList * 2.操作方法 * 2.1 boolean add(

By Ne0inhk
【2026 最新】Java 与 IntelliJ IDEA 详细下载安装教程 带图演示(Windows 版)

【2026 最新】Java 与 IntelliJ IDEA 详细下载安装教程 带图演示(Windows 版)

前言 Java 是全球最广泛使用的编程语言之一,适用于企业级应用、Android 开发、大数据处理和后端服务。而 IntelliJ IDEA(简称 IDEA)作为 JetBrains 公司推出的旗舰级 Java IDE,凭借卓越的智能代码补全、深度框架集成、强大的调试器和流畅的用户体验,被广大开发者誉为“最强 Java 开发工具”。 本教程专为 Windows 系统用户 编写,将手把手指导你完成 Java Development Kit (JDK) 和 IntelliJ IDEA 的下载、安装与基础配置,助你快速搭建专业的 Java 开发环境。 一、Java(JDK)下载与安装 💡 注意:开发 Java 程序需要安装 JDK(

By Ne0inhk
【JAVA 进阶】SpringBoot自动配置机制:从原理到实践的深度解析

【JAVA 进阶】SpringBoot自动配置机制:从原理到实践的深度解析

文章目录 * 前言 * 第一章 初识SpringBoot自动配置 * 1.1 自动配置的定义 * 1.2 自动配置的核心价值 * 1.2.1 降低开发门槛 * 1.2.2 提高开发效率 * 1.2.3 保证配置一致性 * 1.3 自动配置与传统Spring配置的对比 * 1.3.1 传统Spring Web配置(Spring 4.x及之前) * 1.3.2 SpringBoot自动配置实现 * 第二章 深入原理:SpringBoot自动配置是如何实现的 * 2.1 核心注解:@SpringBootApplication的“三位一体” * 2.1.1 @SpringBootConfiguration:标识配置类

By Ne0inhk
JAVA | 聚焦 OutOfMemoryError 异常

JAVA | 聚焦 OutOfMemoryError 异常

个人主页 文章专栏 在正文开始前,我想多说几句,也就是吐苦水吧…最近这段时间一直想写点东西,停下来反思思考一下。 心中万言,真正执笔时又不知先写些什么。通常这个时候,我都会随便写写,文风极像散文,形散意不散吧! 先说一下近况,最近参加了Mathorlab数学建模,作为一个大一的学生,第一次参加那么高强度的竞赛。深深的意识到自己的不足,天之大,不过蚍蜉撼树。我不过是渺小的沧海一粟。竟欲与苍天比高,不自量力、痴人说梦。梦醒了,还是得加油干…高考后,天真的以为自己不用学习了,后来发现,自己一辈子都要学习。害,挺难过的,也挺无助的。 我,出现在这里。可能你们以为我是理科生,工科女…哦不!我江苏高考纯文出身,这像极了案底。高二下之前,我的确是根正苗红的物化生。可惜,尔辈不能究物理。我无比后悔当时的决定,可是人是不会满足的,那时候不管学啥,我都会后悔。用现在的眼光,去埋怨当时的自己。

By Ne0inhk