初识C++ · 智能指针

目录

前言:

1 智能指针的发展历史

2 unique_ptr和shared_ptr的基本使用

3 shared_ptr的模拟实现

4 有关定制删除器


前言:

智能指针的引入,我们得先从异常开始说起,异常面临的一个窘境是new了多个对象,抛异常了会导致先new的对象没有析构,从而导致内存泄漏的问题,解决方法是使用RAII

RAII是一种技术,英文的全称是:Resource Acquisition Is Initialization,是利用对象的声明周期来解决内存泄漏的一种技术。

就像这样:

template<class T>
class Smart_ptr
{
public:Smart_ptr(T* ptr):_ptr(ptr){}~Smart_ptr(){cout << "~Smart_ptr" << endl;}private:T* _ptr;
};
double Division(int a, int b)
{if (b == 0){throw string("除数为0");}else{return (double)a / (double)b;}
}
void Func()
{Smart_ptr<int> p1 = new int[1000];Smart_ptr<int> p2 = new int[1000];int a = 0, b = 0;cin >> a >> b;Division(a, b);}
int main()
{try{Func();}catch (const exception& e){cout << e.what() << endl;}catch (...){cout << "捕获到未知异常" << endl;}return 0;
}

之前的忧虑来源于第二个抛异常之后前面的对象不会析构的问题,那么现在用一个智能指针对象来接受指针,不仅起到了管理空间的作用,还可以调用原本的行为,比如* -> ,这里还没有重载。

上文有些许未介绍的,比如exception,是C++异常体系里面的基类,所以捕捉到了异常我们就可以用exception来捕捉,打印是用的what函数,就和前面提到的示例代码是一样的。

现在就对智能指针有了一个简单的理解了,现在我们就深入了解一下。


1 智能指针的发展历史

提到智能指针,我们首先应该想到的是C++98里面的auto_ptr,早在C++98就引入了智能指针,但是使用确实让人一言难尽,它的本质是转移控制权,所以会导致一个悬空,像move左值一样,导致原来的自定义对象变为空了,这就不是将亡值的施舍了,是活生生的夺舍:

就像文档里面提到的,推荐使用unique_ptr而不是auto_ptr,因为是指针,所以需要重载函数使得该类有指针的对应行为,比如* -> ,这里我们先使用,所在的头文件是memory:

int main()
{auto_ptr<string> s1(new string("aaaa"));auto_ptr<string> s2(s1);return 0;
}

当我们通过监视窗口可以看到:

执行了拷贝构造之后,s1就悬空了,相应的,s2获得了控制权,但是,属实有点鸡肋。

比如之后还要访问s1,那就报错了,反正呢,很多公司是禁止使用auto_ptr的。

那么呢,在03年的时候,引入了一个库boost,这个库是由C++委员会的部分工作人员搞的,在C++11里面引入的很多新特性都是在boost里面借鉴的,其中就涉及到了智能指针,在boost里面有三种智能指针,分别是scoped_ptr,shared_ptr,weak_ptr,其中也有引入数组,但是C++11并没有采纳。

在C++11引入智能指针的时候就借鉴了boost的智能指针,但是有一个指针改了一个名,scoped_ptr改成了unique_str,为什么改呢,咱也不知道,学就完事儿了。

所以今天的重点就是unique_str和shared_ptr和weak_ptr,其中shared_ptr是人人都要会的。


2 unique_ptr和shared_ptr的基本使用

对于使用方面都是很简单,咱们先看一个文档:

模板参数有一个D = 什么什么的,这时定制删除器,本文后面会介绍,现在先不急。

基本的函数使用就是:

这就是所有的成员函数了,get函数是获取原生指针,release是显式的释放指针,但不是显式的析构这个类,同样的,既然是指针,就应该具备指针的行为,比如* ->等,有了stl的基本,这些我们应该是看一下文档就应该知道怎么使用的,这里再看看构造函数:

注意的是unique_ptr是不支持拷贝构造的,重载的第9个函数,拷贝函数被delete修饰了,所以不支持。其中支持auto_ptr 右值构造什么的,咱们先不管,主要了解前面几个就可以了。

class A
{
public:A(int a1 = 1, int a2 = 1):_a1(a1),_a2(a2){}~A(){cout << "~A()" << endl;}
//private:int _a1 = 1;int _a2 = 1;
};int main()
{unique_ptr<A> sp1(new A[10]);unique_ptr<A> sp2(sp1);A* p = sp1.get();cout << p << endl;sp1->_a1++;sp1->_a2++;return 0;
}

找找错误?

第一个 不能拷贝构造,第二析构会报错,因为开辟的是一个数组,基本类型是A,应该是A[],这就和定制删除器有关系了,所以这里的正确代码是:

class A
{
public:A(int a1 = 1, int a2 = 1):_a1(a1),_a2(a2){}~A(){cout << "~A()" << endl;}
//private:int _a1 = 1;int _a2 = 1;
};int main()
{unique_ptr<A[]> sp1(new A[10]);//unique_ptr<A> sp2(sp1);A* p = sp1.get();cout << p << endl;//sp1->_a1++;//sp1->_a2++;sp1[1]._a1++;sp1[1]._a2++;return 0;
}

这里的执行结果就是析构了十次:

为什么new了十个空间,基本类型也要是一个数组,这里的解决方案是定制删除器,先不管。

然后就是shared_ptr的基本使用:

相比之下shared_ptr就朴实无华很多了。

成员函数这么多,这里和pair有点像的是,有一个make_shared的东西,在构造的时候会用到:

因为它支持拷贝构造,欸~所以我们可以使用make_shared构造,产生临时对象拷贝一个就行。

简单介绍一下其中的成员函数,use_count是用来计算计数的,因为在智能指针要实现的一个事就是,管理资源更加简单,比如多个对象共同管理一块资源,新加了一个对象管理资源,引入计数就++,反之--,对比auto那里不能拷贝,就是因为如果拷贝了,造成了多次析构的问题,就会报错。

那么要验证计数很简单:

int main()
{//shared_ptr<int> sp1(new int);shared_ptr<int> sp1 = make_shared<int>(1);cout << sp1.use_count() << endl;{shared_ptr<int> sp2(sp1);cout << sp2.use_count() << endl;}cout << sp1.use_count() << endl;return 0;
}

既然是出了局部域就会销毁,那么我们创建一个局部域即可:

简单使用了解一下可以了,咱们这里简单模拟实现一下。


3 shared_ptr的模拟实现

目前对于shared_ptr我们的简单理解有了,那么现在简单捋一下,智能指针防止多次析构的方法是使用计数的方式,那么我们就需要一个计数了,问题是这个变量我们如何创建呢?

template<class T>
class shared_ptr
{
private:T* ptr;int count;
};

这样可行吗?显然是不可行的,因为我们创建一个对象,指向空间,就会让这个类多次实例化,每个对象都有一个count,实例化一次count++,然后呢就导致每个对象都有count,count的都是一样的,我们指向一个空间,创建了n个对象,预期是计数为n,但是创建的对象是独立的,析构只会析构某个对象的count,从而count的大小是n * n,所以结果是不可预估的。

那么我们应该用static吗?每创建一个对象就++一次,看起来好像可以,但是我们如果指向的空间不是一个呢?new了两个空间,就会导致两个空间公用一个计数,更不行了。

所以解决办法是创建一个指针,每创建一个对象,指针指向的空间,即计数空间就++:

template<class T>
class shared_ptr
{
public:shared_ptr(T* ptr):_ptr(ptr), _pcount(new int(1)){}shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr), _pcount(sp._pcount){(*_pcount)++;}int use_count(){return *_pcount;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;int* _pcount;
};

这是一般的函数,重点在于赋值重载 和 定制删除器。

对于赋值重载来说,指向的空间修正,指向的计数空间修正,那么原来指向的空间是否需要修正呢?这就要看计数是否到0了,所以需要判断是否到0,到了就析构:

那么析构函数,简单的,new了空间删除就完事了:

void release()
{if (--(*_pcount) == 0){delete _ptr;delete _pcount;}
}
~shared_ptr()
{release();
}

赋值重载判断+ 改定指向:

shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{if (--(sp._pcount) == 0){release();_ptr = sp._ptr;_pcount = sp._pcount;(*_pcount)++;}return *this;
}

但是,万一有人突发奇想,想自己给自己赋值呢?再万一,有人在指向空间是同一块的基础上相互赋值呢?

所以我们不妨判断一下指向空间的地址是不是一样的:

shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{if (_ptr != sp._ptr){release();_ptr = sp._ptr;_pcount = sp._pcount;(*_pcount)++;}return *this;
}

shared_ptr的基本实现就完成了。

现在引入一个线程安全的问题:
 

mutex mtx;
void Func(Ptr::shared_ptr<list<int>> sp, int n)
{cout << sp.use_count() << endl;for (int i = 0; i < n; i++){Ptr::shared_ptr<list<int>> copy1(sp);Ptr::shared_ptr<list<int>> copy2(sp);Ptr::shared_ptr<list<int>> copy3(sp);mtx.lock();sp->push_back(i);	mtx.unlock();}
}int main()
{Ptr::shared_ptr<list<int>> sp1(new list<int>);cout << sp1.use_count() << endl;thread t1(Func, sp1, 100);thread t2(Func, sp1, 200);t1.join();t2.join();cout << sp1->size() << endl;cout << sp1.use_count() << endl;return 0;
}

解决方法是引入atomic,原子操作:

shared_ptr(T* ptr):_ptr(ptr), _pcount(new std::atomic<int>(1))
{}private:T* _ptr;std::atomic<int>* _pcount;

还有一个问题就是,如果是交叉指向,就会导致无法析构:

struct Node
{//std::shared_ptr<Node> _next;//std::shared_ptr<Node> _prev;std::weak_ptr<Node> _next;std::weak_ptr<Node> _prev;int _val;~Node(){cout << "~Node()" << endl;}
};
int main()
{std::shared_ptr<Node> p1(new Node);std::shared_ptr<Node> p2(new Node);cout << p1.use_count() << endl;cout << p2.use_count() << endl;p1->_next = p2;p2->_prev = p1;cout << p1.use_count() << endl;cout << p2.use_count() << endl;cout << p1->_next.use_count() << endl;cout << p2->_prev.use_count() << endl;return 0;
}

 此时就需要弱指针,weak_ptr,只是指向空间,但是计数不++即可。


4 有关定制删除器

为什么会引入定制删除器呢?

int main()
{Ptr::shared_ptr<int> sp1((int*)malloc(sizeof(4)));Ptr::shared_ptr<FILE> sp2(fopen("test.txt", "w"));return 0;
}

在内存管理章节提及,内存管理的函数不能混合使用,何况是FILE呢,FILE*要使用fclose,所以我们应该定制一个删除器,但是如何传过去呢?传只能传到构造,传不到析构,所以我们不妨:

template<class D>
shared_ptr(T* ptr, D del): _ptr(ptr), _pcount(new std::atomic<int>(1)), _del(del)
{}private:T* _ptr;std::atomic<int>* _pcount;std::function<void(T*)> _del = [](T* ptr) {delete ptr; };

要删除的时候,传对象就可以了,比如:

template<class T>
struct FreeFunc {void operator()(T* ptr){cout << "free:" << ptr << endl;free(ptr);}
};
class A
{
public:A(int a1 = 1, int a2 = 1):_a1(a1),_a2(a2){}~A(){cout << "~A()" << endl;}
private:int _a1 = 1;int _a2 = 1;
};int main()
{Ptr::shared_ptr<A> sp1(new A[10], [](A* ptr) {delete[] ptr; });Ptr::shared_ptr<int> sp2((int*)malloc(4), FreeFunc<int>());Ptr::shared_ptr<FILE> sp3(fopen("test.txt", "w"), [](FILE* ptr) {fclose(ptr); });Ptr::shared_ptr<A> sp4(new A);return 0;
}

智能指针就介绍到这里。


感谢阅读!

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

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

相关文章

厂家揭秘:劳保鞋里的防砸黑科技,这些材料你了解多少?

在工业生产的前沿阵地&#xff0c;安全生产始终是企业发展的基石&#xff0c;也是每一位劳动者的头等大事。在繁忙的生产线上&#xff0c;一双看似普通的劳保鞋&#xff0c;实则蕴含着保护我们双脚免受意外伤害的重要科技——防砸材料。今天&#xff0c;百华小编就来和大家盘点…

GitLab-CI/CD指南

由于公司没有运维&#xff0c;写go服务时各个环境编译部署还是略显麻烦&#xff0c;由于代码管理使用的是 gitlab&#xff0c;所以决定使用 gitlab 自带的 CI/CD 来做自动编译和部署&#xff0c;这样每次提交代码以后就可以自动部署到服务器上了。 gitlab 本身只有 CI/CD 的接…

STM32第十二节(中级篇):串口通信(第一节)——功能框图讲解

前言 我们在51单片机中就已经学习过了串口通信的相关知识点&#xff0c;那么我们现在在32单片机上进一步学习通信的原理。我们主要讲解串口功能框图以及串口初始化结构体以及固件库讲解。 STM32第十二节&#xff08;中级篇&#xff09;&#xff1a;串口通信&#xff08;第一节…

Python绘图入门:使用Matplotlib绘制柱状图

Python绘图入门&#xff1a;使用Matplotlib绘制柱状图 柱状图是一种常见的数据可视化方式&#xff0c;能够直观地展示不同类别之间的数据差异。在Python中&#xff0c;Matplotlib是一个非常强大且灵活的绘图库&#xff0c;它不仅能绘制简单的图表&#xff0c;还能创建复杂的多…

远程命令行控制SSH

第一次接触SSH是ROS小车作为服务端&#xff0c;通过ubuntu电脑客户端访问。因为机器人接键盘和屏幕操作起来不方便&#xff0c;所以使用SSH进行连接&#xff0c;方便对小车的操作。 1.服务端安装 打开终端查看ssh是否安装 sudo service ssh status 如果未安装 sudo apt upd…

【网络】私有IP和公网IP的转换——NAT技术

目录 引言 NAT工作机制​编辑 NAT技术的优缺点 优点 缺点 个人主页&#xff1a;东洛的克莱斯韦克-CSDN博客 引言 公网被子网掩码划分为层状结构&#xff0c;一个公网IP的机器又可以用很多私有IP搭建内网。在日常生活场景中用的都是私有IP&#xff0c;例如手机&#xff0c;…

目标检测算法,Yolov7本地化部署使用(一)

安全帽检测、口罩检测、行为检测、目标物体检测&#xff0c;随着深度学习和计算机视觉技术的不断发展&#xff0c;目标检测成为了研究热点之一。YOLOv7作为YOLO系列的新成员&#xff0c;以其高效和准确的性能受到了广泛关注。本文将介绍如何在本地部署并利用YOLOv7完成目标检测…

OpenCV图像处理——按最小外接矩形剪切图像

引言 在图像处理过程中&#xff0c;提取感兴趣区域&#xff08;ROI&#xff09;并在其上进行处理后&#xff0c;往往需要将处理后的结果映射回原图像。这一步通常涉及以下几个步骤&#xff1a; 找到最小外接矩形&#xff1a;使用 cv::boundingRect 或 cv::minAreaRect 提取感兴…

计算机毕业设计 助农产品采购平台 Java+SpringBoot+Vue 前后端分离 文档报告 代码讲解 安装调试

&#x1f34a;作者&#xff1a;计算机编程-吉哥 &#x1f34a;简介&#xff1a;专业从事JavaWeb程序开发&#xff0c;微信小程序开发&#xff0c;定制化项目、 源码、代码讲解、文档撰写、ppt制作。做自己喜欢的事&#xff0c;生活就是快乐的。 &#x1f34a;心愿&#xff1a;点…

C# 中 Tuple 与 ValueTuples 之间的区别

在 C# 中&#xff0c;元组和值元组都用于在单个变量中存储多个值。但它们在语法、功能和性能方面存在一些关键差异。 一.Tuples(元组) 元组是一种引用类型&#xff0c;长期以来一直是 .NET 的一部分。它们是使用 System.Tuple 类创建的。 例子 using System; class Program…

07 STM32ADC

AD单通道和AD多通道 使用ADC可以对高电平和低电平之间的任意电压进行量化&#xff0c;最终用一个变量来表示&#xff0c;读取这个变量。所以ADC就是一个电压表&#xff0c;把引脚电压值测出来&#xff0c;放在一个变量里。 数字到模拟的桥梁&#xff0c;PWM来控制电机的速度&a…

springboot家校共育平台-计算机毕业设计源码54235

摘 要 采用高效的SpringBoot框架&#xff0c;家校共育平台为家长与教师提供了便捷的沟通渠道。该平台整合了丰富的教育资源&#xff0c;实现了家校之间的即时信息互通&#xff0c;从而助力协同教育。 为进一步方便用户访问和使用&#xff0c;平台与微信小程序进行了深度整合。家…

SPAW7000高精度功率分析记录仪,测试方案

测试目标 评估双电机四驱系统中前后电机的性能。 分析前后电机之间的实时联动情况。 测量并分析电机控制器的输入与输出功率。 计算功率转换效率和损耗。 验证电机系统的谐波特性。 测试设备 SPAW7000高精度功率分析记录仪&#xff1a;用于测量功率、电压、电流等参数&am…

【LeetCode Cookbook(C++ 描述)】一刷二叉树综合(上)

目录 LeetCode #226&#xff1a;Invert Binary Tree 翻转二叉树「遍历」「分而治之」广度优先搜索&#xff1a;层序遍历 LeetCode #101&#xff1a;Symmetric Tree 对称二叉树递归法迭代法 LeetCode #100&#xff1a;Same Tree 相同的树递归法迭代法 LeetCode #559&#xff1a;…

八种排序算法的复杂度(C语言)

归并排序(递归与非递归实现,C语言)-CSDN博客 快速排序(三种方法,非递归快排,C语言)-CSDN博客 堆排序(C语言)-CSDN博客 选择排序(C语言)以及选择排序优化-CSDN博客 冒泡排序(C语言)-CSDN博客 直接插入排序(C语言)-CSDN博客 希尔排序( 缩小增量排序 )(C语言)-CSDN博客 计数…

赋能基层,融合创新:EasyCVR视频汇聚平台构建平安城市视频共享系统

一、雪亮工程建设的意义 雪亮工程的核心在于通过高清视频监控、环境监测和智能预警等先进技术手段&#xff0c;构建一个高效、智能、安全、便捷的社会安全防控体系。这一工程的建设不仅代表了现代化科技手段在城市治安管理中的应用&#xff0c;更是提升社会安全保障能力、推动…

LeetCode.3152.特殊数组II

题目描述&#xff1a; 如果数组的每一对相邻元素都是两个奇偶性不同的数字&#xff0c;则该数组被认为是一个 特殊数组 。 你有一个整数数组 nums 和一个二维整数矩阵 queries&#xff0c;对于 queries[i] [fromi, toi]&#xff0c;请你帮助你检查 子数组 nums[fromi..toi…

纷享销客CRM AI产品架构概览、产品特色

一、纷享销客CRM AI产品架构概览 纷享AI平台架构分为三个主要层次&#xff1a;AI基础设施层、AI平台层和AI应用层。每个层次都由一系列功能模块组成&#xff0c;旨在为客户提供强大的技术支持和灵活的解决方案。 1.Al基础设施层 AI基础设施层是整个AI平台的底层支撑&#xff…

使用WooCommerce订阅续订进行货到付款:自定义订单状态

WooCommerce订阅插件允许商店设置周期性的订阅产品。客户购买订阅后&#xff0c;系统会自动根据设定周期进行续订。但对于货到付款的场景&#xff0c;自动续订就面临挑战。 自定义订单状态 为了实现货到付款的续订流程&#xff0c;我们需要创建一个自定义订单状态。以下是具体…

牛客刷题总结——Python入门07:内置函数

🤵‍♂️ 个人主页: @北极的三哈 个人主页 👨‍💻 作者简介:Python领域优质创作者。 📒 系列专栏:《牛客题库-Python篇》 🌐推荐《牛客网》——找工作神器|笔试题库|面试经验|实习经验内推,求职就业一站解决 👉 点击链接进行注册学习 文章目录 010 内置函…