C++ —— 智能指针

C++ —— 智能指针


文章目录

  • C++ —— 智能指针
  • 一、为什么需要使用智能指针?
  • 二、内存泄漏
    • 什么是内存泄漏?
    • 内存泄漏的危害?
    • 内存泄漏分类
  • 三、智能指针的使用及原理
    • 1. RAII
    • 2. 智能指针的原理
  • 三、智能指针的缺陷及其发展
    • 3.1 std::auto_ptr
    • 3.2 std::unique_ptr
    • 3.3 std::shared_ptr
    • 3.4 std::weak_ptr
  • 四、C++11和boost中智能指针的关系
  • 五、总结


一、为什么需要使用智能指针?

我们观察如下代码,并思考代码中提到的三个问题

int div()
{int a, b;cin >> a >> b;if (b == 0)throw invalid_argument("除0错误");return a / b;
}
void Func()
{// 1、如果p1这里new 抛异常会如何?// 2、如果p2这里new 抛异常会如何?// 3、如果div调用这里又会抛异常会如何?int* p1 = new int;int* p2 = new int;cout << div() << endl;delete p1;delete p2;
}
int main()
{try{Func();}catch (exception& e){cout << e.what() << endl;}return 0;
}

通过分析代码我们可以发现,在一些情况下,由于抛异常与捕获异常的跳转情况,在抛异常前申请的内存空间存在没有回收的可能
例如上述代码中p1与p2指针都是我们先new出来的对象,在调用div函数时候,一但我们输入的除数为0,此时程序就会抛异常,程序就会直接跳转到catch语句,进行捕获异常的操作,而本应该进行的delete p1与delete p2语句则被跳过了,此时就出现了内存泄漏的情况,而智能指针的提出就是用来解决这个问题的

二、内存泄漏

什么是内存泄漏?

什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费

内存泄漏的危害?

内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死

内存泄漏分类

C/C++程序中一般我们关心两种方面的内存泄漏:

  1. 堆内存泄漏(Heap leak)
    堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak

  2. 系统资源泄漏
    指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定

三、智能指针的使用及原理

1. RAII

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象

这种做法有两大好处:

  1. 不需要显式地释放资源
  2. 采用这种方式对象所需的资源在其生命期内始终保持有效

2. 智能指针的原理

智能指针实际上是RAII思想的一种具体实现,简单来讲就是将我们自主开辟的内存空间交给一个类的对象来管理,利用类的特性,在对象构造时获取资源来管理,在对象析构的时候会自动调用析构函数,此时释放我们开辟的资源

template<class T>
class SmartPtr
{
public:// RAIISmartPtr(T* ptr):_ptr(ptr){}~SmartPtr(){delete[] _ptr;cout << "delete[] " << _ptr << endl;}
private:T* _ptr;
};

有了智能指针我们再运行上述内存泄漏的代码看看

double Division(int a, int b)
{// 当b == 0时抛出异常if (b == 0){throw invalid_argument("Division by zero condition!");}return (double)a / (double)b;
}void Func()
{// RAIISmartPtr<int> sp1(new int[10]);SmartPtr<double> sp2(new double[10]);int len, time;cin >> len >> time;cout << Division(len, time) << endl;
}int main()
{try{Func();}catch (const exception& e){cout << e.what() << endl;}return 0;
}

在这里插入图片描述
可以发现此时内存泄漏的问题解决了
但作为一个指针,还需要有 *、-> 等功能,才能真正称得上是一个指针

我们将其完善一下

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

总结一下智能指针的原理:

  1. RAII特性
  2. 重载operator*和opertaor->,具有像指针一样的行为

三、智能指针的缺陷及其发展

基本的智能指针框架我们都完成了,不仅能自动释放空间,还具备有指针的基本属性
但上文中我们实现的SmartPtr还是具有一定的缺陷,我们将智能指针拷贝赋值时,就存在了两个智能指针对象共同管理一片空间,这也意味着同一块申请的空间可能会被析构两次,此时BUG就出现了

而下面将逐步分析C++如何优化解决这一问题的

3.1 std::auto_ptr

C++98版本的库中就提供了auto_ptr的智能指针

auto_ptr的实现原理:管理权转移的思想,下面简化模拟实现了一份Tlzns::auto_ptr来了解它的原理

namespace Tlzns
{template<class T>class auto_ptr{public:// RAIIauto_ptr(T* ptr):_ptr(ptr){}auto_ptr(auto_ptr<T>& ap):_ptr(ap._ptr){ap._ptr = nullptr;}~auto_ptr(){delete[] _ptr;cout << "delete[] " << _ptr << endl;}T& operator* (){return *_ptr;}T* operator-> (){return _ptr;}//1.自己给自己赋值//2.自己原来有值改为其他的auto_ptr<T> operator= (auto_ptr<T>& ap){if (this != &ap){//释放原有管理空间delete _ptr;_ptr = ap->_ptr;ap->_ptr = nullptr;}return *this;}private:T* _ptr;};}

3.2 std::unique_ptr

C++11中开始提供更靠谱的unique_ptr
unique_ptr的实现原理:简单粗暴的防拷贝,下面简化模拟实现了一份UniquePtr来了解它的原理

简单粗暴,直接杜绝拷贝

namespace Tlzns
{template<class T>class unique_ptr{public:// RAIIunique_ptr(T* ptr):_ptr(ptr){}~unique_ptr(){delete[] _ptr;cout << "delete[] " << _ptr << endl;}T& operator* (){return *_ptr;}T* operator-> (){return _ptr;}//C++11unique_ptr(unique_ptr<T>& up) = delete;unique_ptr<T> operator= (unique_ptr<T>& up) = delete;//private://	//C++98//	//1、只声明不实现//	// 2、限定为私有//	unique_ptr(const unique_ptr<T>& up);//	unique_ptr<T>& operator=(const unique_ptr<T>& up);private:T* _ptr;};}

3.3 std::shared_ptr

C++11中开始提供更靠谱的并且支持拷贝的shared_ptr

shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源
例如:老板晚上在下班之前都会通知,让最后走的员工记得把门锁下

  1. shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享
  2. 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一
  3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源
  4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了
namespace Tlzns
{template<class T>class shared_ptr{public:// RAIIshared_ptr(T* ptr = nullptr):_ptr(ptr),_pcount(new int(1)){}shared_ptr(shared_ptr<T>& sp):_ptr(sp._ptr),_pcount(sp._pcount){(*_pcount)++;}void release(){if (--(*_pcount) == 0){cout << "delete->" << _ptr << endl;delete _ptr;delete _pcount;}}~shared_ptr(){release();}T& operator* (){return *_ptr;}T* operator-> (){return _ptr;}//自己给自己赋值的两种情况//1. sp1 = sp1//2. sp1 = sp2shared_ptr<T> operator= (shared_ptr<T>& sp){if (_ptr != sp._ptr){//若原有引用计数为0,释放原有空间release();_ptr = sp->_ptr;_pcount = sp->_pcount;(*_pcount)++;}return *this;}private:T* _ptr;int* _pcount;};}

这份自实现的shared_ptr用引用计数解决了重复析构的问题,但上述代码中并不支持delete一个数组或者容器,单单只支持delete一个对象 不支持delete[],为改进这个问题C++11中新增了一个构造函数,可以手动编写del的规则
在这里插入图片描述
我们用function接收析构规则,并提供默认的析构规则来解决问题

namespace Tlzns
{template<class T>class shared_ptr{public:// RAIIshared_ptr(T* ptr = nullptr):_ptr(ptr), _pcount(new int(1)){}template<class D>shared_ptr(T* ptr, D del): _ptr(ptr), _pcount(new int(1)), _del(del){}shared_ptr(shared_ptr<T>& sp):_ptr(sp._ptr), _pcount(sp._pcount){(*_pcount)++;}void release(){if (--(*_pcount) == 0){cout << "delete->" << _ptr << endl;//delete _ptr;_del(_ptr);delete _pcount;}}~shared_ptr(){release();}T& operator* (){return *_ptr;}T* operator-> (){return _ptr;}//自己给自己赋值的两种情况//1. sp1 = sp1//2. sp1 = sp2shared_ptr<T> operator= (shared_ptr<T>& sp){if (_ptr != sp._ptr){//若原有引用计数为0,释放原有空间release();_ptr = sp->_ptr;_pcount = sp->_pcount;(*_pcount)++;}return *this;}private:T* _ptr;int* _pcount;//用function接收析构规则,并提供默认的析构规则function<void(T*)> _del = [](T* ptr) {delete ptr; };};
}

我们可以进行测试

struct s
{~s(){cout << "delete" << endl;}
};int main()
{Tlzns::shared_ptr<s> ap1(new s[10], [](s* p) {delete[] p; });return 0;
}

在这里插入图片描述
至此shared_ptr已经趋近于完美,但仍然具有循环引用的缺陷

struct ListNode
{int _data;shared_ptr<ListNode> _prev;shared_ptr<ListNode> _next;~ListNode() { cout << "~ListNode()" << endl; }
};
int main()
{shared_ptr<ListNode> node1(new ListNode);shared_ptr<ListNode> node2(new ListNode);cout << node1.use_count() << endl;cout << node2.use_count() << endl;node1->_next = node2;node2->_prev = node1;cout << node1.use_count() << endl;cout << node2.use_count() << endl;return 0;
}

在这里插入图片描述
我们可以看到,由于循环引用的问题,使得node1与node2都没有调用析构函数

循环引用分析:
1.node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete
2.node1的_next指向node2,node2的_prev指向node1,引用计数变成2
3.node1和node2析构,引用计数减到1,但是_next还指向下一个节点,但是_prev还指向上一个节点
4.也就是说_next析构了,node2就释放了
5.也就是说_prev析构了,node1就释放了
6.但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev属于node2成员,所以这就叫循环引用,双方都在等对方释放,所以谁也不会释放

在这里插入图片描述

为了解决这个问题 C++提出了weak_ptr

3.4 std::weak_ptr

注意:weak_ptr其实已经脱离了RAII的思想,weak_ptr的提出只是为了解决shared_ptr循环引用的问题

解决方案:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr
原理:node1->_next = node2;与node2->_prev = node1;时,weak_ptr的_next和_prev不会增加node1和node2的引用计数

namespace Tlzns
{template<class T>class shared_ptr{public:// RAIIshared_ptr(T* ptr = nullptr):_ptr(ptr), _pcount(new int(1)){}template<class D>shared_ptr(T* ptr, D del): _ptr(ptr), _pcount(new int(1)), _del(del){}shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr), _pcount(sp._pcount){(*_pcount)++;}void release(){if (--(*_pcount) == 0){cout << "delete->" << _ptr << endl;//delete _ptr;_del(_ptr);delete _pcount;}}~shared_ptr(){release();}T& operator* (){return *_ptr;}T* operator-> (){return _ptr;}//自己给自己赋值的两种情况//1. sp1 = sp1//2. sp1 = sp2shared_ptr<T> operator= (const shared_ptr<T>& sp){if (_ptr != sp._ptr){//若原有引用计数为0,释放原有空间release();_ptr = sp->_ptr;_pcount = sp->_pcount;(*_pcount)++;}return *this;}int use_count() const{return *_pcount;}T* get() const{return _ptr;}private:T* _ptr;int* _pcount;//用function接收析构规则,并提供默认的析构规则function<void(T*)> _del = [](T* ptr) {delete ptr; };};template<class T>class weak_ptr{public:weak_ptr():_ptr(nullptr){}weak_ptr(const shared_ptr<T>& sp):_ptr(sp.get()){}weak_ptr<T>& operator=(const shared_ptr<T>& sp){_ptr = sp.get();return *this;}// 像指针一样T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};}
struct ListNode
{int _data;weak_ptr<ListNode> _prev;weak_ptr<ListNode> _next;~ListNode() { cout << "~ListNode()" << endl; }
};
int main()
{shared_ptr<ListNode> node1(new ListNode);shared_ptr<ListNode> node2(new ListNode);cout << node1.use_count() << endl;cout << node2.use_count() << endl;node1->_next = node2;node2->_prev = node1;cout << node1.use_count() << endl;cout << node2.use_count() << endl;return 0;
}

在这里插入图片描述

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

  1. C++ 98 中产生了第一个智能指针auto_ptr
  2. C++ boost给出了更实用的scoped_ptr和shared_ptr和weak_ptr.
  3. C++ TR1,引入了shared_ptr等。不过注意的是TR1并不是标准版。
  4. C++ 11,引入了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr对应boost的scoped_ptr。并且这些智能指针的实现原理是参考boost中的实现的

boost标准库就像是C++的先行版本,用于测试开发新的功能

五、总结

在这里插入图片描述


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/438262.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

如何使用Docker部署火狐浏览器并实现无公网ip远程访问

文章目录 1. 部署Firefox2. 本地访问Firefox3. Linux安装Cpolar4. 配置Firefox公网地址5. 远程访问Firefox6. 固定Firefox公网地址7. 固定地址访问Firefox Firefox是一款免费开源的网页浏览器&#xff0c;由Mozilla基金会开发和维护。它是第一个成功挑战微软Internet Explorer浏…

shell脚本登录dlut-lingshui并设置开机连网和断网重连

本文提供了一个用于无图形界面linux系统自动连接dlut-lingshui校园网的shell脚本&#xff0c;并提供了设置开机联网以及断网重连的详细操作步骤。本文的操作在ubuntu 22.04系统上验证有效&#xff0c;在其他版本的linux系统上操作时遇到问题可以自行百度。 1. 获取校园网认证界…

前端回显分类 回显有时显示键值(判断条件:看是否有值传入)

<div v-if"form.caseType 1"><p class"title-submit" style"margin-top: 0">案源提供人信息</p><el-row v-for"(item, index) in form.superviseList" :key"index"><el-col :span"8"…

拼接url - 华为OD统一考试

OD统一考试 分值&#xff1a; 100分 题解&#xff1a; Java / Python / C 题目描述 给定一个 url 前缀和 url 后缀, 通过 “,” 分割&#xff0c; 需要将其连接为一个完整的 url 。 如果前缀结尾和后缀开头都没有 /&#xff0c;需要自动补上 / 连接符&#xff1b; 如果前缀结…

大型电商系统商城源码_架构_订单系统_OctShop

中国的电商差不多发展到今天已经有20多年的历史啦&#xff0c;特别是过去的10年里其发展速度与竞争是相当的激烈&#xff0c;发展出了各种各样的模式如&#xff1a;B2B、B2C、B2B2C、O2O、社交电商等等。对于广大的企业或商家来说&#xff0c;电商是一个不可或缺的销售渠道&…

二分查找|详细讲解|两种写法

二分查找 目录 1 介绍2 例题引入3 “左闭右闭”写法4 “左闭右开”写法 1 介绍 二分查找适用于从一个递增或递减的有序数列中查找某一个值 因此&#xff0c;使用二分查找的条件是&#xff1a; 用于查找的内容从逻辑上来看是有序的查找的数量只能是一个而不是多个 在二分查…

【每日一题】5.LeetCode——环形链表

&#x1f4da;博客主页&#xff1a;爱敲代码的小杨. ✨专栏&#xff1a;《Java SE语法》 ❤️感谢大家点赞&#x1f44d;&#x1f3fb;收藏⭐评论✍&#x1f3fb;&#xff0c;您的三连就是我持续更新的动力❤️ &#x1f64f;小杨水平有限&#xff0c;欢迎各位大佬指点&…

c语言实战之贪吃蛇

文章目录 前言效果展示游戏用到的图片游戏思路一览游戏前准备一、贪吃蛇、食物、障碍物节点坐标的结构体二、枚举游戏状态、和贪吃蛇的方向三、维护运行的结构体 游戏开始前的初始化一、学习图形库相关知识二、设置背景三、欢迎界面四、初始化贪吃蛇五、生成障碍物六、生成食物…

【JaveWeb教程】(35)SpringBootWeb案例之《智能学习辅助系统》登录功能的详细实现步骤与代码示例(8)

目录 案例-登录和认证1. 登录功能1.1 需求1.2 接口文档1.3 思路分析1.4 功能开发1.5 测试 案例-登录和认证 在前面的课程中&#xff0c;我们已经实现了部门管理、员工管理的基本功能&#xff0c;但是大家会发现&#xff0c;我们并没有登录&#xff0c;就直接访问到了Tlias智能…

CMake 完整入门教程(一)

1 前言 每一次学习新东西都是很有乐趣的&#xff0c;虽然刚开始会花费时间用来学习&#xff0c;但是实践证明&#xff0c;虽然学习新东西可能会花费一些时间&#xff0c;但是它们带来的好处会远远超过这些花费的时间。学习新东西是值得的&#xff0c;也是很有乐趣的。 网络上…

如何搭建Nextcloud云存储网盘并实现无公网ip访问本地文件【内网穿透】

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

如何实现无公网ip远程SSH连接家中本地的树莓派

文章目录 如何通过 SSH 连接到树莓派步骤1. 在 Raspberry Pi 上启用 SSH步骤2. 查找树莓派的 IP 地址步骤3. SSH 到你的树莓派步骤 4. 在任何地点访问家中的树莓派4.1 安装 Cpolar4.2 cpolar进行token认证4.3 配置cpolar服务开机自启动4.4 查看映射到公网的隧道地址4.5 ssh公网…