【C++】C++中内存管理的利器“智能指针”

【C++】C++中内存管理的利器“智能指针”

各位大佬好,我是落羽!一个坚持不断学习进步的学生。
如果您觉得我的文章还不错,欢迎多多互三分享交流,一起学习进步!
也欢迎关注我的blog主页:
落羽的落羽

文章目录

  • 一、智能指针的场景需求
  • 二、智能指针的设计思路
  • 三、C++标准库中的智能指针
    • 1. auto_ptr
    • 2. unique_ptr
    • 3. shared_ptr
    • 4. weak_ptr
    • 5. 特殊说明
  • 四、智能指针的实现原理
  • 五、shared_ptr的循环引用问题与weak_ptr的使用

一、智能指针的场景需求

如果一个程序中手动new了对象,申请了空间资源,然后下面抛出了异常,就会导致申请的资源没有手动释放,造成内存泄露了。我们就需要在捕捉到异常后在catch语句里先delete资源。可是,new本身也可能抛异常的,导致我们处理起来就会很麻烦。智能指针在这样的场景下处理就十分轻松了。

doubledivide(int a,int b){if(b ==0){throwstring("Divide by zero condition!");}else{return(double)a /(double)b;}}voidFunc(){int* arr =newint[10];try{divide(1,0);}catch(string s){delete[] arr; cout << s << endl;}}

二、智能指针的设计思路

RAII是Resource Acquisition Is Initialization的缩写,是一种管理资源的类的设计思想,本质是一种利用对象生命周期来管理获取的动态资源,避免资源泄漏,这里的资源包括内存、文件指针、网络连接、互斥锁等。RAII在获取资源时把资源委托给一个对象,控制对资源的访问,资源在对象的生命周期内始终保持有效。在对象析构的同时释放资源,这样就保障了资源的正常释放,避免资源泄露。对象的生命周期结束后会自动调用析构函数,也就能使资源自动释放,无需我们再手动操作。

智能指针类除了满足上述RAII的思想,还需要方便资源的访问,所以一般还需要重载operator* -> []等运算符:

template<classT>classSmartPtr{public:SmartPtr(T* ptr):_ptr(ptr){}~SmartPtr(){delete[] _ptr; cout <<"资源已释放"<< endl;} T&operator*(){return*_ptr;} T*operator->(){return _ptr;} T&operator[](size_t i){return _ptr[i];}private: T* ptr;};

有了这样的智能指针类,在遇到手动申请资源时就可以用一个智能指针来接受了,当智能指针生命周期结束时,资源也会在调用的析构函数中释放。

在这里插入图片描述

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

C++标准库也有自己的智能指针,在<memory>这个头文件下。智能指针有很多种,有各自不同的特点。

1. auto_ptr

auto_ptr是C++98就有的智能指针,它的特点是拷贝时会把被拷贝对象的资源管理权转移给拷贝对象。这个设计很不好,因为它会使被拷贝对象悬空变成野指针,稍不注意就会访问报错。C++11后有了别的智能指针,就不要再使用auto_ptr了!

auto_ptr<string>ap1(newstring("xxx"));//拷贝后,资源管理权限转移,ap1悬空 auto_ptr<string>ap2(ap1);//此时若访问ap1,就会访问野指针,程序可能会挂掉//cout << *ap1 << endl;

2. unique_ptr

unique_ptr是C++11设计出的一种智能指针,它的特点是不支持拷贝,只支持move移动。如果是不需要拷贝的场景就很推荐使用它。

unique_ptr<string>up1(newstring("xxx"));//不支持拷贝,会报错//unique_ptr<string> up2(up1);//可以进行移动,但移动后up1也会悬空,移动需谨慎 unique_ptr<string>up3(move(up1));

unique_ptr还支持了operator bool的类型转换,如果智能指针对象是一个空对象(没有管理资源),则返回false,否则返回true。所以我们可以把智能指针给if等语句判断是否为空。

3. shared_ptr

shared_ptr是C++11设计出的一种智能指针,它的特点是支持拷贝,也支持移动。底层是用引用计数的方式实现的。
shared_ptr支持拷贝意味着,一份资源可以同时被多个智能指针管理,引用计数用于记录这份资源有几个“管理者”。有多个管理者时,其中一个智能指针对象进行析构,不会释放这份资源,而使引用计数减一。当引用计数为一时,意味着只有最后一个管理者了,这个智能指针进行析构时,才会真正释放资源。引用计数的方式避免了空间重复释放。share_ptr内也有一个接口use_count(),能返回这个智能指针指向的资源的管理者个数

shared_ptr<string>sp1(newstring("xxx")); shared_ptr<string>sp2(sp1); shared_ptr<string>sp3(sp1); cout << sp3.use_count()<< endl;
在这里插入图片描述
在这里插入图片描述

shared_ptr还支持了operator bool的类型转换,如果智能指针对象是一个空对象(没有管理资源),则返回false,否则返回true。所以我们可以把智能指针给if等语句判断是否为空。

4. weak_ptr

weak_ptr也是C++11设计出的一种智能指针,它和上面两种很不一样,它不支持RAII的设计思路,它不是用于直接管理资源的。weak_ptr的作用只在于解决shared_ptr的一个循环引用导致的内存泄漏问题。具体我们下文再讲

5. 特殊说明

库中的智能指针析构时默认使用delete进行资源释放,这意味着如果不是new出来的资源,智能指针析构时就会崩溃。智能指针其实支持在构造时提供一个删除器,本质是一个可调用对象,用于自定义我们想要的资源释放方式。给了定制的删除器,智能指针析构时就会调用删除器去释放资源。但是unique_ptr和shared_ptr的构造时提供删除器的格式还不太一样,也是设计上的一个小缺点了。

实际使用中,除了new,new[]也是经常使用的。所以unique_ptr和shared_ptr都特化了一种支持new[]的版本,使用如shared_ptr<int[]> sp(new int[10]);这样,就可以管理new[]的资源了

四、智能指针的实现原理

我们来模拟实现一下几种智能指针,其实思路很简单,只要遵循它们的特点就好。注意unique_ptr和shared_ptr的构造函数需要使用explicit修饰, 防止普通指针隐式类型转换成为智能指针对象。

namespace lydly {template<classT>classauto_ptr{public:auto_ptr(T& ptr):_ptr(ptr){}auto_ptr(auto_ptr<T>& ap):_ptr(ap._ptr){//管理权转移 ap._ptr =nullptr;}~auto_ptr(){if(_ptr){delete _ptr;}} auto_ptr<T>&operator=(auto_ptr<T>& ap){if(this!=&ap){if(_ptr){delete _ptr;} _ptr = ap._ptr; ap._ptr =nullptr;}return*this;} T&operator*(){return*_ptr;} T*operator->(){return _ptr;}private: T* _ptr;};template<classT>classunique_ptr{public:explicitunique_ptr(T& ptr):_ptr(ptr){}~unique_ptr(){if(_ptr){delete _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){delete _ptr; _ptr = up._ptr; up._ptr =nullptr;} T&operator*(){return*_ptr;} T*operator->(){return _ptr;}operatorbool(){return _ptr !=nullptr;}private: T* _ptr;};}

至于shared_ptr,需要注意设计它的引用计数机制,指向同一个资源的不同智能指针间需要共用一个引用计数,可以使用指针的方式:

namespace lydly {template<classT>classshared_ptr{public:explicitshared_ptr(T* ptr):_ptr(ptr),_pcount(newint(1)){}//自己提供删除器的构造函数template<classD>shared_ptr(T* ptr, D del):_ptr(ptr),_pcount(newint(1)),_del(del){}shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr),_pcount(sp._pcount),_del(sp._del){//拷贝后引用计数+1(*_pcount)++;}voidrelease(){//先使引用计数-1,再判断此时是否为0,为0则释放资源if(--(*_pcount)==0){_del(_ptr);delete _pcount; _ptr =nullptr; _pcount =nullptr;}}~shared_ptr(){release();} shared_ptr<T>&operator=(const shared_ptr<T>& sp){if(_ptr != sp._ptr){release(); _ptr = sp._ptr; _pcount = sp._pcount;(*_pcount)++; _del = sp._del;}return*this;} T&operator*(){return*_ptr;} T*operator->(){return _ptr;}intuse_count()const{return*_pcount;}operatorbool(){return _ptr !=nullptr;}private: T* _ptr;int* _pcount;//引用计数 function<void(T*)> _del =[](T* ptr){delete ptr;};//删除器,默认提供的是delete版本};}

五、shared_ptr的循环引用问题与weak_ptr的使用

shared_ptr在大部分情况下管理资源是非常合适的。但是有一种特殊的场景:

structListNode{int val; shared_ptr<ListNode> next =nullptr; shared_ptr<ListNode> prev =nullptr;}; shared_ptr<ListNode>n1(new ListNode); shared_ptr<ListNode>n2(new ListNode); n1->next = n2; n2->prev = n1;

经过这样的代码,n1、n2的prev指向一块资源,n2、n1的next指向一块资源。

在这里插入图片描述

这种场景中,n1和n2要怎么进行析构呢?
n1调用析构后,第一个资源的引用计数减为1,资源不释放。n2调用析构后,第二个资源的引用计数减为1,资源不释放。此时就进入了一个局面:第一个资源(的成员)管理着第二个资源,第二个资源(的成员)管理着第一个资源。
第一个资源想要释放,需要第二个资源先释放;第二个资源想要释放,需要第一个资源先释放。
逻辑上进入了死循环,谁都无法释放,这就是循环引用问题。

在这里插入图片描述

所以,weak_ptr出手了。把ListNode中的shared_ptr改为weak_ptr就能解决这种问题。weak_ptr不会增加引用计数,next和prev就不参与资源的管理了,成功打破循环引用

structListNode{int val; weak_ptr<ListNode> next =nullptr; weak_ptr<ListNode> prev =nullptr;}; shared_ptr<ListNode>n1(new ListNode); shared_ptr<ListNode>n2(new ListNode); n1->next = n2; n2->prev = n1;

weak_ptr不支持绑定到资源,只支持绑定到shared_ptr,且不增加shared_ptr的引用计数。weak_ptr也没有重载operator*operator->等,因为他不参与资源管理,如果他绑定的shared_ptr已经释放了资源,那么他去访问资源就是很危险的。

  • weak_ptr中有一个接口expired()检查它指向的资源是否过期。
  • weak_ptr的use_count()也可获取shared_ptr的引用计数。
  • weak_ptr想访问资源时,可以调用lock()返回⼀个管理资源的shared_ptr(也会使引用计数+1),如果资源已经被释放,则返回的shared_ptr是⼀个空对象,如果资源没有释放,则通过返回的shared_ptr访问资源是安全的。
在这里插入图片描述

本篇完,感谢阅读!

Read more

华为OD机试双机位C卷:采购订单 (Py/Java/C/C++/Js/Go)

华为OD机试双机位C卷:采购订单 (Py/Java/C/C++/Js/Go)

采购订单 华为OD机试双机位C卷 - 华为OD上机考试双机位C卷 100分题型 华为OD机试双机位c卷真题目录点击查看: 华为OD机试双机位C卷真题题库目录|机考题库 + 算法考点详解 题目描述 在一个采购系统中,采购申请(PR)需要经过审批后才能生成采购订单(PO)。每个PR包含商品的单价(假设相同商品的单价一定是一样的)及数量信息。系统要求对商品进行分类处理:单价高于100元的商品需要单独处理,单价低于或等于100元的相同商品可以合并到同一采购订单PO中。针对单价低于100的小额订单,如果量大可以打折购买。 具体规则如下: 如果PR状态为"审批通过",则将其商品加入到PO中。如果PR的状态为"审批拒绝"或"待审批",则忽略改PR。 对于单价高于100元的商品,每个商品单独生成一条PO记录。对于单价低于100元的商品,将相同商品的数量合并到一条PO记录中。 如果商品单价<100且商品数量>=100,则单价打9折。 输入描述 第一行包含整数N,

By Ne0inhk
【C++】C++异常

【C++】C++异常

🎬 个人主页:MSTcheng · ZEEKLOG 🌱 代码仓库 :MSTcheng · Gitee 🔥 精选专栏: 《C语言》 《数据结构》 《算法学习》 《C++由浅入深》 💬座右铭:路虽远行则将至,事虽难做则必成! 在前面的文章中,我们已经介绍了C++11的一些新特性。本文将和下一篇一起为大家讲解C++的最后两个重要主题:异常处理和智能指针。 文章目录 * 一、异常的概念及使用 * 1.1异常的概念 * 1.2异常的分类 * 1.3异常的抛出与捕获 * 1.4栈展开 * 1.5 查找匹配的处理代码 * 1.6异常重新抛出 * 1.7异常的安全问题 * 1.8异常规范 * 二、总结 一、异常的概念及使用 1.1异常的概念 异常(Exception)是指在程序执行过程中发生的意外或错误情况,

By Ne0inhk
面试官最爱问:C++ 多态底层到底是怎么实现的?

面试官最爱问:C++ 多态底层到底是怎么实现的?

欢迎来到 s a y − f a l l 的文章 欢迎来到say-fall的文章 欢迎来到say−fall的文章 🌈say-fall:个人主页🚀专栏:《手把手教你学会C++》 | 《C语言从零开始到精通》 | 《数据结构与算法》 | 《小游戏与项目》💪格言:做好你自己,才能吸引更多人,与他们共赢,这才是最好的成长方式。 前言: 关于上一篇文章的多态原理他来啦! 在上一篇《多态核心:虚函数、override、final、纯虚函数总结》中,我们已经初步认识了 C++ 多态的语法层面:虚函数、重写、纯虚函数等关键知识点,并提到了多态的底层依赖于 vptr 虚指针 与 vtable 虚函数表。但很多同学在学习时,仍然会有这些疑问: * 为什么带虚函数的类,sizeof 大小会多出

By Ne0inhk
C++ 运算符重载:自定义类型的运算扩展

C++ 运算符重载:自定义类型的运算扩展

C++ 运算符重载:自定义类型的运算扩展 💡 学习目标:掌握运算符重载的核心语法与规则,能够为自定义类型重载常用运算符,实现类对象的灵活运算。 💡 学习重点:运算符重载的基本形式、成员函数与全局函数重载的区别、常见运算符的重载实现、禁止重载的运算符。 一、运算符重载的概念与核心价值 ✅ 结论:运算符重载是 C++ 静态多态的重要体现,允许为自定义类型(如类、结构体)重新定义运算符的行为,让自定义对象可以像内置类型一样使用运算符。 运算符重载的核心价值: 1. 简化代码书写:用直观的运算符替代繁琐的成员函数调用,提升代码可读性 2. 统一操作风格:让自定义类型的运算逻辑与内置类型保持一致,降低学习和使用成本 3. 扩展类型功能:根据业务需求定制运算符的行为,满足自定义类型的运算需求 ⚠️ 注意事项:运算符重载不会改变运算符的优先级和结合性,也不会改变运算符的操作数个数。 二、运算符重载的基本语法 运算符重载的本质是函数重载,分为成员函数重载和全局函数重载两种形式。 2.1 成员函数重载语法 将运算符重载函数定义为类的成员函数,语法格式如下: class

By Ne0inhk