智能指针(C++11)

智能指针的使用

问题

我们在平时写程序的时候,有些情况下不可避免地会遇见内存泄露的情况。内存泄露是指因为疏忽或错误,造成程序未能释放已经不再使用的内存的情况。例如下面这个例子,内存泄漏不易被察觉。

int div()
{int a, b;cin >> a >> b;if(b == 0)throw invalid_argument("除0错误");return a / b;
}
void func()
{int* p = new int[10];cout << div() << endl;delete[] p;
}
int main()
{try{func();}catch (exception& e){cout << e.what() << endl;}return 0;
}

程序首先会调用func函数,当func函数中执行到“ cout << div() << endl ”这句代码时,就会跳转到div函数中依次执行,当我们把b设置为0时,这时就会抛异常。程序的执行流就直接跳转到主函数的catch块中执行,最终导致func函数中申请到的资源没有被释放,造成内存泄露。

解决方案一

我们可以利用异常的重新捕获来解决,在func函数中先对div函数中抛出的异常进行捕获,捕获后先将之前申请的内存资源释放,然后再将异常重新抛出。

int div()
{int a, b;cin >> a >> b;if (b == 0)throw invalid_argument("除0错误");return a / b;
}
void func()
{int* p = new int[10];try{cout << div() << endl;}catch (...){delete[] p;throw;}delete[] p;
}
int main()
{try{func();}catch (exception& e){cout << e.what() << endl;}return 0;
}

解决方案二

上述问题也可以使用智能指针进行解决

template<class T>
class SmartPtr
{
public:SmartPtr(T* ptr):_ptr(ptr){}~SmartPtr(){cout << "delete: " << _ptr << endl;delete _ptr;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}
private:T* _ptr;
};
int div()
{int a, b;cin >> a >> b;if (b == 0)throw invalid_argument("除0错误");return a / b;
}
void func()
{SmartPtr<int> sp(new int[10]);//...cout << div() << endl;//...
}
int main()
{try{func();}catch (exception& e){cout << e.what() << endl;}return 0;
}

代码中将申请到的内存空间交给了一个SmartPtr对象进行管理。

  • 在构造SmartPtr对象时,SmartPtr将传入的需要被管理的内存空间保存起来。
  • 在SmartPtr对象析构时,SmartPtr的析构函数中会自动将管理的内存空间进行释放。
  • 此外,为了让SmartPtr对象能够像原生指针一样使用,还需要对*->运算符进行重载。

这样一来,无论程序是正常执行完毕返回了,还是因为某些原因中途返回了,或是因为抛异常返回了,只要SmartPtr对象的生命周期结束就会调用其对应的析构函数,进而完成内存资源的释放。

智能指针的原理

实现智能指针时需要考虑以下三个方面的问题:

  1. 在对象构造时获取资源,在对象析构的时候释放资源,利用对象的生命周期来控制程序资源,即RAII特性。
  2. *->运算符进行重载,使得该对象具有像指针一样的行为。
  3. 智能指针对象的拷贝问题。

 RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、互斥量等等)的简单技术。 简而言之就是把资源交给对象管理,对象生命周期内,资源有效,对象生命周期到了,释放资源

但是我们对于当前实现的SmartPtr类,如果用一个SmartPtr对象来拷贝构造另一个SmartPtr对象,或是将一个SmartPtr对象赋值给另一个SmartPtr对象,都会导致程序崩溃。

int main()
{SmartPtr<int> sp1(new int);SmartPtr<int> sp2(sp1); //拷贝构造SmartPtr<int> sp3(new int);SmartPtr<int> sp4(new int);sp3 = sp4; //拷贝赋值return 0;
}

这是由于编译器默认生成的拷贝构造函数和赋值重载函数对内置类型完成的是浅拷贝,若将一个SmartPtr对象原封不动地拷贝给另外一个SmartPtr对象,这时两个SmartPtr对象就指向同一块内存空间。当SmartPtr对象销毁时就会调用析构函数,这样就导致了同一块空间被析构了两次,程序因而会崩溃。

需要注意的是,智能指针就是要模拟原生指针的行为,当我们将一个指针赋值给另一个指针时,目的就是让这两个指针指向同一块内存空间,所以这里本就应该进行浅拷贝,但单纯的浅拷贝又会导致空间被多次释放,因此根据解决智能指针拷贝问题方式的不同,从而衍生出了不同版本的智能指针。

C++中的智能指针

auto_ptr

auto_ptr是C++98中引入的智能指针,auto_ptr通过管理权转移的方式解决智能指针的拷贝问题,保证一个资源在任何时刻都只有一个对象在对其进行管理,这时同一个资源就不会被多次释放了。比如:

int main()
{// C++98 一般实践中,很多公司明确规定不要用这个auto_ptr<A> ap1(new A(1));auto_ptr<A> ap2(new A(2));auto_ptr<A> ap3(ap1);// 崩溃//ap1->_a++;ap3->_a++;return 0;
}

发生管理权转移后,再进行拷贝时就会把被拷贝对象的资源管理权转移给拷贝对象导致被拷贝对象悬空,这也就造成了很大的隐患,当访问被拷贝对象时程序就会造成程序崩溃。

auto_ptr的模拟实现

auto_ptr的实现步骤如下:

  • 在构造函数中获取资源,在析构函数中释放资源,利用对象的生命周期来控制资源。
  • 对*和->运算符进行重载,使auto_ptr对象具有像原生指针一样的行为。
  • 在拷贝构造函数中,通过传入对象管理的资源来构造当前对象,并将传入对象管理资源的指针置空。
template<class T>class auto_ptr{public:auto_ptr(T* ptr):_ptr(ptr){}~auto_ptr(){cout << "delete:" << _ptr << endl;delete _ptr;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}//管理权转移auto_ptr(auto_ptr<T>& a):_ptr(a._ptr){a._ptr = nullptr;}private:T* _ptr;};

unique_ptr

unique_ptr是C++11中引入的智能指针,unique_ptr通过禁止拷贝的方式解决智能指针的拷贝问题,它是个简单粗暴的办法,这样做也能保证资源不会被多次释放。但是禁止拷贝的办法也不是一个万全的办法,因为有些场景就是要用到拷贝。

int main()
{unique_ptr<int> up1(new int(0));//std::unique_ptr<int> up2(up1);  //会报错return 0;
}

unique_ptr的模拟实现

unique_ptr的实现步骤如下:

  • 在构造函数中获取资源,在析构函数中释放资源,利用对象的生命周期来控制资源。
  • 对*和->运算符进行重载,使unique_ptr对象具有像原生指针一样的行为。
  • 用C++11的方式在拷贝构造和赋值重载函数后面加上=delete。
template<class T>class unique_ptr{public:unique_ptr(T* ptr):_ptr(ptr){}~unique_ptr(){cout << "delete:" << _ptr << endl;delete _ptr;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}unique_ptr(unique_ptr<T>& ap) = delete;unique_ptr<T>& operator=(unique_ptr<T>& ap) = delete;private:T* _ptr;};

shared_ptr

shared_ptr是C++11中引入的智能指针,shared_ptr通过引用计数的方式解决智能指针的拷贝问题。

  • 每一个被管理的资源都有一个对应的引用计数,通过这个引用计数记录着当前有多少个对象在管理着这块资源。
  • 当新增一个对象管理这块资源时,则将该资源对应的引用计数进行++,当一个对象不再管理这块资源或该对象被析构时则将该资源对应的引用计数进行--。
  • 当一个资源的引用计数减为0时说明已经没有对象在管理这块资源了,这时就可以将该资源进行释放了。

通过这种引用计数的方式就能支持多个对象一起管理某一个资源,也就是支持了智能指针的拷贝,并且只有当一个资源对应的引用计数减为0时才会释放资源,因此保证了同一个资源不会被释放多次。

class A
{
public:A(int a = 0):_a(a){cout << "A(int a = 0)" << endl;}~A(){cout << this;cout << " ~A()" << endl;}//private:int _a;
};

注:A类的具体内部结构如上述代码所示。

int main()
{a::shared_ptr<A> sp1(new A(1));a::shared_ptr<A> sp2(new A(2));a::shared_ptr<A> sp3(sp1);sp1->_a++;sp3->_a++;a::shared_ptr<A> sp4(sp2);a::shared_ptr<A> sp5(sp4);return 0;
}

其关系图如下图所示,其中红色方框代表的是引用计数

shared_ptr的模拟实现

shared_ptr的实现步骤如下:

  • 首先要在shared_ptr类中增加一个成员变量_pcount,代表引用计数。
  • 在构造函数中获取资源,并将该资源对应的引用计数设置为1,表示当前只有一个对象在管理资源。
  • 在拷贝构造函数中,我们让传入的对象与之前管理资源的对象共同管理对应的资源,同时将该资源的引用计数加一。
  • 在赋值重载函数中,先将当前对象管理的资源对应的引用计数--(如果减为0则需要释放),然后再与传入对象一起管理它管理的资源,同时需要将该资源对应的引用计数++。

举例:例如将sp5赋值给sp1,那么sp1原来指向的那块资源的引用计数就要减一,否则的话就会内存泄漏,因为无论如何引用计数都减不到为0,那么这块空间也就不会释放。并且原先sp1指向的这块资源如果只有sp1在管理的话,那么就直接将其释放即可。还有一个细节问题,例如遇到“ sp1=sp1 ”自己给自己赋值这种情况的话,我们就直接返回*this即可。

  • 在析构函数中,将管理资源对应的引用计数--,如果减为0则需要将该资源释放。
  • 对*和->运算符进行重载,使shared_ptr对象具有像原生指针一样的行为。
template<class T>
class shared_ptr
{
public:// RAII// 像指针一样shared_ptr(T* ptr = nullptr):_ptr(ptr), _pcount(new int(1)){}~shared_ptr(){if (--(*_pcount) == 0){cout << "delete:" << _ptr << endl;delete _ptr;delete _pcount;}}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}// sp3(sp1)shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr), _pcount(sp._pcount){++(*_pcount);}// sp6 = sp6shared_ptr<T>& operator=(const shared_ptr<T>& sp){if (_ptr == sp._ptr)return *this;if (--(*_pcount) == 0){delete _ptr;delete _pcount;}_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;
};

这里有个问题,为什么引用计数要放在堆区呢? 

如果将引用计数设置为int类型时,那么每个对象都有自己独立的引用计数,而我们希望的是不同的对象管理同一份资源 。其次,shared_ptr中的引用计数count也不能定义成静态成员变量,因为静态成员变量是所有类型对象共享的,这会导致管理相同资源的对象和管理不同资源的对象用到的都是同一个引用计数。

weak_ptr

虽然shared_ptr非常好用,看似完美无缺,但是它在某个场景中却有致命的缺陷。

我们先定义一个节点类,并且将_next和_prev成员变量的类型改为shared_ptr类型。

struct Node
{A _val;a::shared_ptr<Node> _next;a::shared_ptr<Node> _prev;~Node(){cout<<"~Node"<<endl;}
};

 接着我们再将sp1和sp2相互链接,然后运行程序观察结果。

int main()
{// 循环引用shared_ptr<Node> sp1(new Node);shared_ptr<Node> sp2(new Node);cout << sp1.use_count() << endl;cout << sp2.use_count() << endl;sp1->_next = sp2;sp2->_prev = sp1;cout << sp1.use_count() << endl;cout << sp2.use_count() << endl;return 0;
}

我们发现这时程序运行结束后两个结点都没有被释放,但如果去掉链接节点时的两句代码中的任意一句,那么这两个节点就都能够正确释放,根本原因就是因为这两句链接节点的代码导致了循环引用

出现循环引用的原因

当以new的方式申请到两个Node节点并交给两个智能指针管理后,这两个资源对应的引用计数都为1。接着我们将这两个节点链接起来后,资源1当中的next成员与sp2一同管理资源2,资源2中的prev成员与sp1一起管理资源1,此时这两个资源对应的引用计数都被加到了2。

当出了main函数的作用域后,sp1和sp2的生命周期也就结束了,因此这两个资源对应的引用计数最终都减到了1

根据上图可知_prev管着左边的节点,_next管着右边的节点。当右边的节点析构时,_prev才会析构,而右边的节点是当_next析构它才会析构,而_next析构需要左边的节点析构才能析构。这时就形成了一个死循环,最终导致资源无法被释放。

而如果链接节点时只进行一个链接操作,那么当sp1和sp2的生命周期结束时,就会有一个资源对应的引用计数被减为0,此时这个资源就会被释放,当这个释放后另一个资源的引用计数也会被减为0,最终两个资源就都会被释放了。这就是为什么只进行一个连接操作时这两个节点就都能够正确释放的原因。

针对上述问题,C++11引入了weak_ptr。其中我们最需要注意的一点是weak_ptr不是用来管理资源的释放的,它主要是用来解决shared_ptr的循环引用问题的。

weak_ptr支持用shared_ptr对象来构造weak_ptr对象,构造出来的weak_ptr对象与shared_ptr对象管理同一个资源,但不会增加这块资源对应的引用计数

我们将_next和_prev的类型改为weak_ptr 

struct Node
{A _val;weak_ptr<Node> _next;weak_ptr<Node> _prev;~Node(){cout<<"~Node"<<endl;}
};

接着再执行上述代码就会发现两个节点能正确释放,并且根据use_count链接前后两次打印的内容发现weak_ptr不会增加对应资源的引用计数

weak_ptr的模拟实现

weak_ptr的实现步骤如下:

  • 提供一个无参的构造函数,比如new Node这句代码就会调用weak_ptr的无参的构造函数。
  • 支持用shared_ptr对象拷贝构造weak_ptr对象,构造时获取shared_ptr管理的资源。
  • 支持用shared_ptr对象赋值给weak_ptr对象,赋值时获取shared_ptr管理的资源。
  • 对*和->运算符进行重载,使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;
};

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

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

相关文章

【复习linux相关命令】

查看命令位置&#xff0c;查找命令 which命令 查看命令的位置 [rootVM-12-15-opencloudos ~]# which cd /usr/bin/cd [rootVM-12-15-opencloudos ~]# which java /usr/local/java/jdk1.8.0_261/bin/java [rootVM-12-15-opencloudos ~]# which pwd /usr/bin/pwdfind查找文件 …

element+Vue2,在一个页面跳转到另一个页面,并自动选中table的某一行

需求&#xff1a;点击A页面的某处&#xff0c;跳转到B页面并选中B页面表格的某一行&#xff08;点击B页面的搜索后需要清空默认选择的状态&#xff09;环境&#xff1a;vue2、element的table&#xff0c;table允许多选知识点&#xff1a;主要使用到table的这两属性&#xff1a;…

Diffusion添加噪声noise的方式有哪些?怎么向图像中添加噪声?

添加噪声的方式大致分为两种&#xff0c;一种是每张图像在任意timestep都加入一样的均匀噪声&#xff0c;另一种是按照timestep添加不同程度的噪声 一、在任意timestep都加入一样的noise batch_size 32x_start torch.rand(batch_size,3,256,256) noise torch.randn_like(x_…

亚信安全荣获2023年度5G创新应用评优活动两项大奖

近日&#xff0c;“关于2023 年度5G 创新应用评优活动评选结果”正式公布&#xff0c;亚信安全凭借在5G安全领域的深厚积累和创新实践&#xff0c;成功荣获“5G技术创新的优秀代表”和“5G应用创新的杰出实践”两项大奖。 面向异构安全能力的5G安全自动化响应系统 作为5G技术创…

【Mybatis 基础】增删改查(@Insert, @Delete, @Update, @Select)

Mybatis Insert Delete Update Select Mybatis用法基础操作 - 删除delete 传参SpringbootMybatisCrudApplicationTests 测试类删除预编译SQL 基础操作 - 插入Insert 插入SpringbootMybatisCrudApplicationTests 测试类插入对象主键返回 基础操作 - 更新UPDATE 更新SpringbootMy…

Python进阶编程 --- 1.类和对象

文章目录 第一章&#xff1a;1.初始对象1.1 使用对象组织数据1.2 类的成员方法1.2.1 类的定义和使用1.2.2 创建类对象1.2.3 成员变量和成员方法1.2.4 成员方法的定义语法1.2.5 注意事项 1.3 类和对象1.3.1 基于类创建对象 1.4 构造方法1.5 其他内置方法1.5.1 魔术方法str字符串…

(南京观海微电子)——DDIC显示触控芯片介绍

显示驱动芯片&#xff08;Display Driver Integrated Circuit&#xff0c;简称DDIC&#xff09;的主要功能是控制OLED显示面板。它需要配合OLED显示屏实现轻薄、弹性和可折叠&#xff0c;并提供广色域和高保真的显示信号。同时&#xff0c;OLED要求实现比LCD更低的功耗&#xf…

成绩管理系统|基于springboot成绩管理系统的设计与实现(附项目源码+论文)

基于springboot成绩管理系统的设计与实现 一、摘要 传统办法管理信息首先需要花费的时间比较多&#xff0c;其次数据出错率比较高&#xff0c;而且对错误的数据进行更改也比较困难&#xff0c;最后&#xff0c;检索数据费事费力。因此&#xff0c;在计算机上安装毕业设计成绩管…

平衡二叉树(AVL树)

文章目录 平衡二叉树&#xff08;AVL树&#xff09;1、平衡二叉树概念2、平衡二叉树的的实现2.1、平衡二叉树的结点定义2.2、平衡二叉树的插入2.3、平衡二叉树的旋转2.3.1、右单旋&#xff08;R旋转&#xff09;2.3.2、左单旋&#xff08;L旋转&#xff09;2.3.3、先右单旋再左…

leetcode 周赛 391场

2. 换水问题 给你两个整数 numBottles 和 numExchange 。 numBottles 代表你最初拥有的满水瓶数量。在一次操作中&#xff0c;你可以执行以下操作之一&#xff1a; 喝掉任意数量的满水瓶&#xff0c;使它们变成空水瓶。用 numExchange 个空水瓶交换一个满水瓶。然后&#xf…

Django安装及第一个项目

1、安装python C:\Users\leell>py --version Python 3.10.6 可以看出我的环境python的版本3.10.6&#xff0c;比较新 2、 Python 虚拟环境创建 2.1 官网教程 目前&#xff0c;有两种常用工具可用于创建 Python 虚拟环境&#xff1a; venv 在 Python 3.3 及更高版本中默…

Vue系列——数据对象

<!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>el:挂载点</title> </head> <body&g…

C++11入门手册第二节,学完直接上手Qt(共两节)

C++多线程 #include <thread>:C++多线程库 #include <mutex>:C++互斥量库 #include <future>:C++异步库 多线程介绍 线程的创建 void entry_1() { }以普通函数作为线程入口函数:void entry_2(int val) { }​std::thread my_thread_1(entry_1);std::thr…

【c++】类和对象(六)深入了解隐式类型转换

&#x1f525;个人主页&#xff1a;Quitecoder &#x1f525;专栏&#xff1a;c笔记仓 朋友们大家好&#xff0c;本篇文章我们来到初始化列表&#xff0c;隐式类型转换以及explicit的内容 目录 1.初始化列表1.1构造函数体赋值1.2初始化列表1.2.1隐式类型转换与复制初始化 1.3e…

C语言-写一个宏,可以将一个整数的二进制位的奇数位和偶数位交换。

0xaaaaaaaa...等是什么&#xff1f;-CSDN博客https://blog.csdn.net/Jason_from_China/article/details/137179252 #define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #define SWAP(num) (((num & 0xAAAAAAAA) >> 1) | ((num & 0x55555555) << …

WIFI驱动移植实验:配置 Linux 内核

一. 简介 前面文章删除了Linux内核源码&#xff08;NXP官方的kernel内核源码&#xff09;自带的 WIFI驱动。 WIFI驱动移植实验&#xff1a;删除Linux内核自带的 RTL8192CU 驱动-CSDN博客 将正点原子提供的 rtl8188EUS驱动源码添加到 kernel内核源码中。文章如下&#xff1a…

JavaScript基础语法–变量

文章目录 认识JavaScript变量程序中变量的数据&#xff08;记录&#xff09;–变量变量的命名格式在Java script中变量定义包含两部分1. 变量声明&#xff08;高级JS引擎接下来定义一个变量&#xff09;2. 其他的写法 变量命名的规范&#xff08;遵守&#xff09;变量的练习a. …

C语言每日一题

1.题目 二.分析 本题有两点需要注意的&#xff1a; do-while循环 &#xff1a;在判断while条件前先执行一次do循环static变量 &#xff1a;程序再次调用时static变量的值不会重新初始化&#xff0c;而是在上一次退出时的基础上继续执行。for( i 1; i < 3; i )将调用两次…

江协STM32:点亮第一个LED灯和流水灯

很多单片机都是高电平弱驱动&#xff0c;低电平强驱动&#xff0c;所以这里是低电平有效 点亮一个LED灯 操作STM32的GPIO需要三个操作&#xff1a; 第一个使用RCC开启GPIO的时钟 第二步使用GPIO_Init函数初始化GPIO 第三步使用输出或输入函数控制GPIO 1.使用RCC开启GPIO的时…

C++ | leetcode C++题解之第1题两数之和

题目&#xff1a; C 题解&#xff1a; class Solution { public:vector<int> twoSum(vector<int>& nums, int target) {unordered_map<int, int> hashtable;for (int i 0; i < nums.size(); i) {auto it hashtable.find(target - nums[i]);if (it …