C++|智能指针

目录

引入

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

1.1RAII

1.2智能指针原理

1.3智能指针发展

1.3.1std::auto_ptr

1.3.2std::unique_ptr

1.3.3std::shared_ptr 

二、循环引用问题及解决方法

2.1循环引用

2.2解决方法 

三、删除器

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


引入

回顾上一篇章,学习了异常机制,但面临了一种情况还没有解决,就是异常带来的内存泄漏,如下:

#include <iostream>
using namespace std;double Division(int a, int b)
{// 当b == 0时抛出异常if (b == 0)throw "Division by zero condition!";//抛出的异常是字符串elsereturn ((double)a / (double)b);
}
void Func() 
{int* p = new int[1];int len, time;cin >> len >> time;cout << Division(len, time) << endl;delete[] p;}int main() {try {Func();}catch (const char* errmsg) {cout << "Caught in main: " << errmsg << endl;}return 0;
}

调用Division函数时,若抛出异常,最终是被main函数中的catch所捕获,直接进入到main函数中的catch中了,但是,delete[] p不会被执行了 ,导致的问题就是内存泄漏。那么可以通过智能指针来解决这个问题

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

1.1RAII

RAII是一种利用对象生命周期来控制程序资源的简单技术。在对象构造时获取对象资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候由编译器顺带释放资源。实际上把控资源就是管理对象。

这种做法体现的优势是:

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

RAII是一种设计思想,而智能指针正是采用了这种思想。

RAII思想设计: 

#include <iostream>
using namespace std;// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr {
public:SmartPtr(T* ptr = nullptr):_ptr(ptr){}~SmartPtr(){if (_ptr)delete _ptr;}private:T* _ptr;
};
double Division(int a, int b)
{// 当b == 0时抛出异常if (b == 0)throw "Division by zero condition!";//抛出的异常是字符串elsereturn ((double)a / (double)b);
}
void Func()
{int* p = new int[1];int len, time;cin >> len >> time;cout << Division(len, time) << endl;SmartPtr<int> sp(p);//当Division抛异常时,会被main函数中的catch捕获,那么程序会直接跳到main函数所在的catch//对此了,Func函数的栈帧会自动销毁,对于内置类型会自动释放,对于自定义类型而言会去调用它的析构函数//所以sp会去调用析构函数,同时释放了p的资源}int main() {try{Func();}catch (const char* errmsg){cout << "Caught in main: " << errmsg << endl;}return 0;
}

输出结果:

1.2智能指针原理

上述的SmartPtr虽然是通过RAII思想设计的类,但还不能真正称作为智能指针,因为它还不有指针的行为。指针可以解引用,也可以通过->去访问所指空间的内容,因此还需要重载*和->。

#include <iostream>
using namespace std;
template<class T>
class SmartPtr {
public:SmartPtr(T* ptr = nullptr): _ptr(ptr){}~SmartPtr(){if (_ptr)delete _ptr;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}
private:T* _ptr;
};
struct Date
{int _year;int _month;int _day;
};
int main()
{SmartPtr<int> sp1(new int);*sp1 = 10;cout << *sp1 << endl;SmartPtr<Date> sparray(new Date);sparray->_year = 2024;sparray->_month = 7;sparray->_day = 7;cout << (*sparray)._year << ":" << (*sparray)._month << ":" << (*sparray)._day << endl;return 0;
}

输出结果:

总结一下,智能指针是通过RAII思想设计出的类,它重载operator*和operator->,具有像指针一样的行为。 

1.3智能指针发展

对于上述代码,却有一个弊端,如下:

int main()
{SmartPtr<int> sp1(new int);SmartPtr<int> sp2(sp1);return 0;
}

 带来的问题是,sp1拷贝给sp2是一个浅拷贝,所以sp2._ptr和sp1._ptr指向同一块空间,在析构时候,sp2._ptr所指向空间先释放,随后释放sp1._ptr所指向空间,但是该空间已被释放,所以会报错。那么为了解决这个问题,在历史上,智能指针有了一定的发展,来看。

1.3.1std::auto_ptr

C++98版本库中提供了auto_ptr的智能指针。他上述问题的实现原理是:通过管理权转移思想。

// C++98 管理权转移 auto_ptr
#include <iostream>
using namespace std;
namespace bit
{template<class T>class auto_ptr{public:auto_ptr(T* ptr):_ptr(ptr){}//ap2(ap1)auto_ptr(auto_ptr<T>& sp):_ptr(sp._ptr){// 管理权转移sp._ptr = nullptr;//相当于ap1._ptr不再管理资源了,被置空了}//ap2= ap1auto_ptr<T>& operator=(auto_ptr<T>& ap){// 检测是否为自己给自己赋值if (this != &ap){// 释放当前对象中资源if (_ptr)delete _ptr;// 转移ap中资源到当前对象中_ptr = ap._ptr;ap._ptr = NULL;}return *this;}~auto_ptr(){if (_ptr){cout << "delete:" << _ptr << endl;delete _ptr; }}// 像指针一样使用T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};
}int main()
{std::auto_ptr<int> sp1(new int);std::auto_ptr<int> sp2(sp1); // 管理权转移,sp1不再管理资源了,在析构的时候,确实没啥问题//但带来了另一个问题,sp1所管理资源,被置空了,即sp1._ptr1 ==  nullptr,但对于外人来说并不知道//若有人使用已被释放的资源,如下:*sp2 = 10;cout << *sp2 << endl;cout << *sp1 << endl;//对空指针解引用,报错return 0;
}

auto_ptr指针虽然解决了析构带来的问题,但是带来了新的问题,采用管理权转移思想,将资源由最后一个拷贝对象管理,而被拷贝对象都被置空了,导致有人使用被置空的拷贝对象时,会出现对空指针解引用,报错。所以该智能指针也可以算是一个"失败的man",很多公司也明确要求不能使用它。

1.3.2std::unique_ptr

随着C++的发展,C++11提供了更靠谱的智能指针unique_ptr,其实现原理:简单粗暴的防拷贝。

#include <iostream>
using namespace std;
namespace bit
{template<class T>class unique_ptr{public:unique_ptr(T* ptr):_ptr(ptr){}~unique_ptr(){if (_ptr){cout << "delete:" << _ptr << endl;delete _ptr;}}// 像指针一样使用T& operator*(){return *_ptr;}T* operator->(){return _ptr;}//只声明不实现,但是不能防止有人在类外实现,所以此方法不行//unique_ptr(unique_ptr<T>& sp);//c++11仿拷贝/*unique_ptr(const unique_ptr<T>& sp) = delete;unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;*/private:T* _ptr;//将拷贝私有化可以防拷贝,但这是c++98用法//unique_ptr(unique_ptr<T>& sp);};
}//类外实现拷贝
//template<class T>
//bit::unique_ptr<T>::unique_ptr(bit:: unique_ptr<T>& sp)
//{
//	//...
//}int main()
{bit::unique_ptr<int> sp1(new int);//bit::unique_ptr<int> sp2(sp1);//拷贝构造被删除了,编译器也不会默认生成了return 0;
}

 由于前面出现的问题都是因为拷贝带来的,那么unique_ptr了就直接把拷贝给禁了,不让你因为各种操作而导致额外的问题,这确实解决了。但是不能使用拷贝了,这也不是办法呀

1.3.3std::shared_ptr 

随着前面智能指针问题的暴露,C++11不断发展,最终也完善了该问题,提出了更靠谱且支持拷贝的shared_ptr。其原理:是通过引用计数方式来实现多个shared_ptr对象之间共享资源。

其满足以下规则:

1.每个shared_ptr对象,其都包含着一个指针,该指针所指向空间内容用来计数有多少个指针指向该空间,即有多少个对象管理同一份资源,当多个对象管理同一份资源,那么他们的指针就会指向同一份空间,空间内容记录着这些数量,每增加一个对象管理相同资源,则计数也会加1

2.当对象销毁时即调用析构函数,就说明该对象不再管理这份资源,其对应的指针不再指向计数的空间,引用计数就会减1

3.如果引用计数是0,就说明没有对象管理该资源了,则必须释放该资源,相反地,如果不是0,说明还有对象管理资源,则就不能释放该资源,否则,对于其他对象中的指针就成野指针了。

#include <iostream>
using namespace std;namespace bit
{template<class T>class shared_ptr{public:shared_ptr(T* ptr = nullptr):_ptr(ptr), _pcount(new int(1)){}shared_ptr(const shared_ptr<T>& sp){if (this != &sp){_ptr = sp._ptr;_pcount = sp._pcount;(*_pcount)++;}}shared_ptr<T>& operator=(const shared_ptr<T>& sp){if (this != &sp){if (--(*sp._pcount) == 0){delete _ptr;delete _pcount;}_ptr = sp._ptr;_pcount = sp._pcount;(*_pcount)++;}return *this;}~shared_ptr(){if (--(*_pcount) == 0){delete _ptr;delete _pcount;_ptr = nullptr;_pcount = nullptr;}}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;int* _pcount;};
}

调试结果: 

shared_ptr解决了拷贝带来的问题,不幸的是,又出来了新的问题,那就是在线程安全和循环引用问题,对于线程问题,先放放,那我们要讲的是循环引用问题。

二、循环引用问题及解决方法

2.1循环引用

根据上述shared_ptr的代码,在外头定义一个类,进行以下操作:

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

输出结果:

没有任何结果,而当屏蔽node1->_next = node2;或者node2->_prev = node1;中的任何一句,

其输出结果:

这是为何?如图:

两边的节点各自受到_next,_prev牵连,造成了一个循环,且引用计数一直维持在1,并没有减到0,只要屏蔽其中一条语句就不会受到限制,正常析构。虽然这个智能指针是模拟的,就算是库里面的也有一样的问题。

2.2解决方法 

那么为了解决该问题,专门引入了一个智能指针来处理--weak_ptr

#include <iostream>
using namespace std;namespace bit
{template<class T>class shared_ptr{public:shared_ptr(T* ptr = nullptr):_ptr(ptr), _pcount(new int(1)){}shared_ptr(const shared_ptr<T>& sp){if (this != &sp){_ptr = sp._ptr;_pcount = sp._pcount;++(*_pcount);}}shared_ptr<T>& operator=(const shared_ptr<T>& sp){if (this != &sp){if (--(*_pcount) == 0){delete _ptr;delete _pcount;}_ptr = sp._ptr;_pcount = sp._pcount;++(*_pcount);}return *this;}~shared_ptr(){if (--(*_pcount) == 0){delete _ptr;delete _pcount;_ptr = nullptr;_pcount = nullptr;}}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T* get() const{return _ptr;}private:T* _ptr;int* _pcount;};// 简化版本的weak_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
{bit::weak_ptr<ListNode> _next;bit::weak_ptr<ListNode> _prev;~ListNode(){cout << "~ListNode()" << endl;}
};int main()
{bit::shared_ptr<ListNode> node1(new ListNode);bit::shared_ptr<ListNode> node2(new ListNode);node1->_next = node2;node2->_prev = node1;return 0;
}

 输出结果:

那么weak_ptr是怎样解决问题的了,其实很简单,weak_ptr不在对引用计数进行管理了,所以shared_ptr对象初始化时的引用计数都是为1,在进行析构的时候,引用计数都会减到0,然后释放对应的_ptr,_pcount。

三、删除器

对于智能指针的析构还有一个问题,就是对于不是通过new出来的对象或者说new出来的对象是带有[]的如何通过智能指针进行管理,总不能每一种情况来一份代码。那么shared_ptr设计了一个删除器来解决这个问题。

// 仿函数的删除器#include <iostream>
using namespace std;template<class T>
struct FreeFunc {void operator()(T* ptr){cout << "free:" << ptr << endl;free(ptr);}
};
template<class T>
struct DeleteArrayFunc {void operator()(T* ptr){cout << "delete[]" << ptr << endl;delete[] ptr;}
};
int main()
{FreeFunc<int> freeFunc;shared_ptr<int> sp1((int*)malloc(4), freeFunc);DeleteArrayFunc<int> deleteArrayFunc;shared_ptr<int> sp2(new int[4], deleteArrayFunc);shared_ptr<FILE> sp5(fopen("test.txt", "w"), [](FILE* p){fclose(p); });return 0;
}

输出结果:

如上代码,删除器可以通过自己定义的方式来释放空间,这只是展示了仿函数删除器,仅仅是九牛一毛。

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

Boost库是为C++语言标准库提供扩展的一些C++程序库的总称,是一个"准"标准库,由Boost社区组织开发、维护。Boost库可以与C++标准库完美共同工作,并且为其提供扩展功能。

1.C++98 中产生了第一个智能指针auto ptr.

不好的设计,对象悬空(不建议使用)
2.C++ boost给出了更实用的scoped ptr和shared ptr和weak ptr.

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中的实现的。

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

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

相关文章

如何分析软件测试中发现的Bug!

假如你是一名软件测试工程师&#xff0c;每天面对的就是那些“刁钻”的Bug&#xff0c;它们像是隐藏在黑暗中的敌人&#xff0c;时不时跳出来给你一个“惊喜”。那么&#xff0c;如何才能有效地分析和处理这些Bug&#xff0c;让你的测试工作变得高效且有趣呢&#xff1f;今天我…

SpringBoot配置flyway

背景 目前我们的项目代码都会交由Git、SVN等版本管理工具进行管理&#xff0c;但是我们的sql脚本&#xff0c;尤其是各类ddl脚本并没有进行版本的管理&#xff08;python的web框架Django默认就提供了类似的工具&#xff0c;从一开始就鼓励开发者通过版本管理的方式进行数据库的…

Android Studio 的Gradle下载慢,Gradle切换下载源

看图 下面的文字地址因为转义符号的问题&#xff0c;https后面少了一个斜杠看图片进行补充&#xff0c;直接复制不知道能不能用 distributionUrlhttps://mirrors.cloud.tencent.com/gradle/gradle-8.7-bin.zip

第一关:Linux基础知识

Linux基础知识目录 前言LinuxInternStudio 关卡1. InternStudio开发机介绍2. SSH及端口映射2.1 什么是SSH&#xff1f;2.2 如何使用SSH远程连接开发机&#xff1f;2.2.1 使用密码进行SSH远程连接2.2.2 配置SSH密钥进行SSH远程连接2.2.3 使用VScode进行SSH远程连接 2.3. 端口映射…

进度条提示-在python程序中使用避免我误以为挂掉了

使用库tqdm 你还可以手写一点&#xff0c;反正只要是输出点什么东西都可以&#xff1b; Demo from chatgpt import time from tqdm import tqdm# 示例函数&#xff0c;模拟长时间运行的任务 def long_running_task():total_steps 100for step in tqdm(range(total_steps), …

手机容器化 安装docker

旧手机-基于Termux容器化 1、安装app 在手机上安装Termux或ZeroTermux&#xff08;Termux扩展&#xff09; 1.1 切换源 注&#xff1a;可以将termux进行换源&#xff0c;最好采用国内源&#xff0c;例如&#xff1a;清华源等 更新包列表和升级包&#xff08;可选&#xff0…

vue 画二维码及长按保存

需求 想要做如下图的二维码带文字&#xff0c;且能够长按保存 前期准备 一个canvas安装qrcode&#xff08;命令&#xff1a;npm i qrcode&#xff09; 画二维码及文字 初始化画布 <template><div><canvas ref"canvas" width"300" he…

8627 数独

为了判断数独解是否合法&#xff0c;我们需要遵循以下步骤&#xff1a; 1. **检查每一行**&#xff1a;确保1到9每个数字在每一行中只出现一次。 2. **检查每一列**&#xff1a;确保1到9每个数字在每一列中只出现一次。 3. **检查每个3x3的宫**&#xff1a;确保1到9每个数字在…

在pycharm中使用jupyter

在pycharm中使用jupyter 前置条件&#xff1a;你的环境中应该有juptyer &#xff0c;没有的话 pip install jupyter 点击项目目录&#xff0c;右键->new->jupyter notebook 打开file settings 找到 jupyter server &#xff08;按照默认的用代理服务器就行&#xff09; P…

东芝 TB5128FTG 强大性能的步进电机驱动器

TB5128FTG它以高精度和高效能为设计理念&#xff0c;采用 PWM 斩波方法&#xff0c;并内置时钟解码器。通过先进的 BiCD 工艺制造&#xff0c;这款驱动器提供高达 50V 和 5.0A 的输出额定值&#xff0c;成为广泛应用场景中的强劲解决方案。 主要特性 TB5128FTG 拥有众多确保高…

码云远程仓库, 回滚到指定版本号

1. 打开项目路径, 右击Git Bash Here 2. 查找历史版本 git reflog 3. 回退到指定版本 git reset --hard 版本号 4. 强制推送到远程 git push -f

SQL基础-DQL 小结

SQL基础-DQL 小结 学习目标&#xff1a;学习内容&#xff1a;SELECTFROMWHEREGROUP BYHAVINGORDER BY运算符ASC 和 DESC 总结 学习目标&#xff1a; 1.理解DQL&#xff08;Data Query Language&#xff09;的基本概念和作用。 2.掌握SQL查询的基本语法结构&#xff0c;包括SEL…

基于Android平台开发,购物商城

相关视频教程在某站上面(&#x1f50d;浩宇软件开发) 1. 项目功能思维导图 2. 项目涉及到的技术点 使用SQLite数据库实现数据存储使用CountDownTimer实现启动页倒计时使用SharedPreferences实现记住密码登录使用BottomNavigationView实现底部导航栏使用ActivityFragment实现底…

C++:红黑树

概念 红黑树是一种二叉搜索树&#xff0c;一般的二叉搜索会发生不平衡现象&#xff0c;导致搜索效率下降&#xff0c;于是学者们开始探索如何让二叉搜索树保持平衡&#xff0c;这种树叫做自平衡二叉搜索树。起初学者发明了AVL树&#xff0c;其通过一定算法保持了二叉搜索树的严…

【Linux】进程7——查看进程

1.为什么进程管理这么重要呢&#xff1f; 这是因为&#xff1a; 首先&#xff0c;我们在操作系统时的各项任务其实都是经过某个PID来完成的&#xff08;包括你的bash环境&#xff09;&#xff0c;因此&#xff0c;能不能执行某项任务&#xff0c;就与该进程的权限有关了。再来…

【Java数据结构】初识线性表之一:顺序表

使用Java简单实现一个顺序表 顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构&#xff0c;一般情况下采用数组存储。在数组上完成数据的增删查改。 线性表大致包含如下的一些方法&#xff1a; public class MyArrayList { private int[] array; pri…

SD卡讲解

SD 卡 (Secure Digital Memory Card) 在我们生活中已经非常普遍了&#xff0c;控制器对 SD 卡进行读写通信 操作一般有两种通信接口可选&#xff0c;一种是 SPI 接口&#xff0c;另外一种就是 SDIO 接口。SDIO 全称是安全数 字输入/输出接口&#xff0c;多媒体卡 (MMC)、SD 卡、…

【学习css2】grid布局-页面footer部分保持在网页底部

中间内容高度不够屏幕高度撑不开的页面时候&#xff0c;页面footer部分都能保持在网页页脚&#xff08;最底部&#xff09;的方法 1、首先上图看显示效果 2、奉上源码 2.1、html部分 <body><header>头部</header><main>主区域</main><foot…

centos9中mysql指令提示解决方案

CentOS 9 中没有 MySQL 的官方插件&#xff0c;因为 MySQL 不是 CentOS 的默认数据库&#xff0c;它是 MariaDB 的一部分。 如果想要一个命令行提示的 MySQL 客户端&#xff0c;可以使用第三方工具 &#xff0c;如mycli 首先&#xff0c;确保已经安装了 MySQL&#xff0c;且操…

【人工智能】-- 受限玻尔兹曼机

个人主页&#xff1a;欢迎来到 Papicatch的博客 课设专栏 &#xff1a;学生成绩管理系统 专业知识专栏&#xff1a; 专业知识 文章目录 &#x1f349;引言 &#x1f349;受限玻尔兹曼机 &#x1f348;RBM的结构 &#x1f34d;RBM的架构图 &#x1f34d;RBM的经典实现 &…