智能指针(C++)

目录

一、智能指针是什么

二、为什么需要智能指针

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

3.1、RALL

3.2 智能指针的原理

3.3、智能指针的分类

3.3.1、auto_ptr

3.3.2、unique_ptr

3.3.3、shared_ptr

3.2.4、weak_ptr


一、智能指针是什么

         在c++中,动态内存的管理式通过一对运算符来完成的:new,在动态内存中为对象分配空间并返回一个指向该对象的指针,我们可以选择对对象进行初始化;delete,接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。动态内存的使用很容易出现问题,因为确保在正确的时间释放内存是极其困难的。有时使用完对象后,忘记释放内存,造成内存泄漏的问题。

        所谓的智能指针本质就是一个类模板,它可以创建任意的类型的指针对象,当智能指针对象使用完后,对象就会自动调用析构函数去释放该指针所指向的空间。

        下面是智能指针的基本框架,所有的智能指针类模板中都需要包含一个指针对象构造函数析构函数

二、为什么需要智能指针

异常的重新抛出的场景:


void File()
{
	string filename;
	cin >> filename;
	FILE* fout = fopen(filename.c_str(), "r");
	if (fout == nullptr) {
		string errmsg = "打开文件失败:";
		errmsg += filename;
		errmsg += "->";
		errmsg += strerror(errno);
		Exception e(errno, errmsg);
		throw e;
	}
	char ch;
	while ((ch = fgetc(fout))!=EOF) {
		cout << ch;
	}
	fclose(fout);
}

double Division(int a, int b)
{
	if (b == 0) 
	{
		string errmsg = "Division by zero condition!";
		Exception e(100, errmsg);
		throw e;
	}
	else
	{
		return ((double)a / (double)b);
	}
}

void Func()
{
	int* p = new int[100];
	int len, time;
	cin >> len >> time;
	try {
		cout << Division(len,time) << endl;
		File();
	}
	catch (...)
	{
		//捕获之后,不是要处理异常,异常由最外层同一处理
		//这里捕获异常只是为了处理内存泄漏的问题
		delete[]p;
		throw; 
	}
	delete[]p;
}

int main()
{

	try {
		Func();
	}
	catch (const Exception& e) {
		cout << e.what() << endl;
	}
	catch (...)
	{
		cout << "未知异常" << endl;
	}
	return 0;
}

        在Func函数中,我们在堆上创建了开一个指针,为了防止函数抛出异常导致最后的析构函数不执行而产生野指针,我们使用了 异常的重新抛出策略。

        但是,终究不是个好的方法,如果这类资源较多,那么我们需要大量的 异常重抛 ,而且就算程序不涉及程序处理,大量的堆上空间需要人工释放,容易造成疏漏,这一问题在工程中比较常见。

        所以,这时候如果我们实用智能指针,就可以不用再操心内存是否会泄露的问题。

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

3.1、RALL

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

我们可以借助RALL思想来写一个简单的 智能指针:

#include<iostream>
using namespace std;

template<class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr =nullptr)
		:_ptr(ptr)
	{}
	~SmartPtr()
	{
		if (_ptr)delete _ptr;
		cout<<"~SmartPtr"<<endl;
	}
private:
	T* _ptr;
};

int main()
{
	int* a = new int(1);
	SmartPtr<int> sp(a); //将a 指针委托给sp对象管理

	SmartPtr<int>sp2(new int(2)); //直接船舰匿名对象给sp2管理
}

3.2 智能指针的原理

上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可
以通过->去访问所指空间中的内容,因此:AutoPtr模板类中还得需要将* 、->重载下,才可让其
像指针一样去使用。
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<int> sparray(new Date);
    // 需要注意的是这里应该是sparray.operator->()->_year = 2018;
    // 本来应该是sparray->->_year这里语法上为了可读性,省略了一个->
    sparray->_year = 2018;
    sparray->_month = 1;
    sparray->_day = 1;
}
总结一下智能指针的原理:
1. RAII特性
2. 重载operator*和opertaor->,具有像指针一样的行为。

3.3、智能指针的分类

        上面的SmartPtr 还不可以被称为智能指针,因为它还不具有指针的行为与性质。

        指针可以解引用,也可以通过->去访问所指向的空间中的内容,因此智能指针还需要将 *,->重载。

        除此之外,如果我们使用了 拷贝或者赋值操作,就会发生浅拷贝的问题,由于二者指向同一块空间,所以在析构的时候也会析构两次,造成错误。

所以,为了解决以上问题,C++提供了几种设计方案实现的智能指针。

C++中存在4种智能指针:auto_ptr,unquie_ptr,shared_ptr,weak_ptr,他们各有优缺点,以及对应的实用场景

3.3.1、auto_ptr

在C++98版本的库种,提供了 auto_ptr 的智能指针:

class Date
{
public:
	Date()
		:_year(0),_month(0),_day(0)
	{}
	~Date(){}

	int _year;
	int _month;
	int _day;

};

int main()
{
	auto_ptr<Date>ap(new Date);

	//拷贝构造
	auto_ptr<Date>copy(ap);
	
	ap->_year = 2022;
}

我们发现报错了,发生了非法访问。

这就是auto_ptr 的弊病,当我们使用对象拷贝或者赋值之后,之前的那个对象就被置空(如下图)

        在拷贝或者赋值的过程种,auto_ptr 会传递所有权,将资源全部从源指针转移给目标指针,源指针被置空。

        虽然这种方法确实解决了 浅拷贝的问题,但是十分局限性也很大,这也就导致了,我们使用auto_ptr的时候要注意,不要对源指针进行访问或者操作。

        由于C++98种提供的这个智能指针问题明显,所以在实际工作种哼多公司是明确规定了不能使用auto_ptr的。

auto_ptr具体实现:

template<class T>
	class auto_ptr
	{
	public:
		auto_ptr(T* ptr=nullptr)
			:_ptr(ptr)
		{}
		
		auto_ptr(auto_ptr<T>& ap)
			:_ptr(ap._ptr)
		{
			ap._ptr = nullptr; //管理权转移
		}

		auto_ptr<T>& operator = (auto_ptr<T>& ap)
		{
			if (this != *ap) {
				delete _ptr;
				_ptr = ap._ptr;
				ap._ptr = nullptr;
			}
			return *this;
		}

		~SmartPtr()
		{
			if (_ptr)delete _ptr;
		}
		T& operator *()
		{
			return *_ptr;
		}
		T* operator ->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
	};

3.3.2、unique_ptr

在C++11中,C++11y引入了unique_ptr.

unique_ptr的原理很简单,就是一个“得不到就毁掉”的理念,直接把拷贝和赋值禁止了。

对于用不上赋值拷贝的场景的时候,我们选择unique_ptr也是一个不错的选择。

template<class T>
	class unique_ptr
	{
	public:
		unique_ptr(T* ptr = nullptr)
			:_ptr(ptr)
		{}

		//防拷贝
		unique_ptr(unique_ptr<T>& ap) = delete;
		unique_ptr<T>& operator = (unique_ptr<T>& ap) = delete;

		~SmartPtr()
		{
			if (_ptr)delete _ptr;
		}

		T& operator *()
		{
			return *_ptr;
		}
		T* operator ->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
	};

3.3.3、shared_ptr

C++中还提供了shared_ptr。

shared_ptr 是当前最为广泛使用的智能指针,它可以安全的提供拷贝操作。

shared_ptr的原理:

        我们可以对一个资源添加一个计数器,让所有管理该资源的智能共用这个计数器,倘若发生拷贝,计数器加一,倘若有析构发生, 计数器减一,当计数器等于0的时候,就把对象析构掉。

  1. shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
  2. 在对象被销毁时(就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
  3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
  4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。

shared_ptr 的模拟实现

template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T*ptr =nullptr)
			:_ptr(ptr),_pcount(new int(1))
		{}
		//拷贝构造
		shared_ptr(const T& sp)
			_ptr(sp._ptr),_pcount(sp._pcount)
		{
			++(*_pcount);
		}
		//赋值拷贝
		shared_ptr<T>& operator = (shared_ptr<T>& sp)
		{
			if (_ptr != sp._ptr) {
				if (--(*_pcount) == 0){
					delete _pcount;
					delete _ptr;
				}
				_ptr = sp._ptr;
				_pcount = sp._pcount;
				++(*_pcount);
			}
			return *this;
		}
		T& operator *()
		{
			return *_ptr;
		}

		T* operator ->()
		{
			return _ptr;
		}


		~shared_ptr()
		{
			if (--(*_pcount) == 0 && _ptr) {
				delete _pcount;
				delete _ptr;
			}
		}
	private:
		T* _ptr;
		int* _pcount;
	};

我们把 这个 计数器 建在堆上,这样就可以保证各个对象之间保持同步同时计数正确。

        拷贝构造

         赋值拷贝

赋值拷贝需要注意两点:

  1. 在被赋值之前的对象需要将自己析构,也就是放弃当前资源的管理权,然后再去被赋值,取得新的管理权。
  2. 避免自己对自己赋值,按照1中的机制,如果自己对自己赋值,会造成无谓的操作,或者误析构资源。

另一种写法:

		shared_ptr<T>& operator=(shared_ptr<T> sp)
		{
			swap(_ptr, sp._ptr);
			swap(_pcount, sp._pcount);
			return *this;
		}

但是,此时我们的shared_ptr 还面临着 线程安全的问题。

这里我们需要保障的是对于 计数器的 ++ 和 – 造成的线程不安全。对于资源的线程安全问题,这不是智能指针保证的部分


	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr), _pcount(new int(1)), _pmtx(new mutex)
		{}

		void add_ref()
		{
			_pmtx->lock();
			++(*_pcount);
			_pmtx->unlock();
		}
		void release_ref()
		{
			bool flag = false;
			_pmtx->lock();
			if (--(*_pcount) == 0 && _ptr) {
				delete _pcount;
				delete _ptr;
				flag = true;
				cout << "释放资源:" << _ptr << endl;
			}
			_pmtx->unlock();
			if (flag)delete _pmtx;
		}

		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr),_pcount(sp._pcount),_pmtx(sp._pmtx)
		{
			add_ref();
		}
		shared_ptr<T>& operator = (const shared_ptr<T>& sp)
		{
			if (_ptr != sp._ptr) {
				if (--(*_pcount) == 0){
					delete _pcount;
					delete _ptr;
				}
				_ptr = sp._ptr;
				_pcount = sp._pcount;
				add_ref();
			}
			return *this;
		}
		T& operator *()
		{
			return *_ptr;
		}

		T* operator ->()
		{
			return _ptr;
		}

		T* get()
		{
			return _ptr;
		}
		int use_count()
		{
			return *_pcount;
		}


		~shared_ptr()
		{
			release_ref();
		}
	private:
		T* _ptr;
		int* _pcount;
		mutex* _pmtx;
	};

        定制删除器

        不管是我们自己实现的shared_ptr还是库中的shared_ptr,我们在析构的时候默认都是 delete _ptr,如果我们托管的类型是 new T[] ,或者 malloc出来的话,就导致类型不是匹配的,无法析构。

        为此,shared_ptr提供了 定制删除器,我们可以在构造的时候作为参数传入。如果我们不传参,就默认使用delete

这个自定义删除器可以是函数指针仿函数lamber,包装器。

        仿函数的删除器

shared_ptr中的析构函数会去调用DelArry仿函数去释放动态数组。

3.2.4、weak_ptr

虽然 shared_ptr 确实已经是一个不错的设计了,但是没有“十全十美”的东西,在一些特别的场景之下shared_ptr 也无能为力:

shared_ptr 的循环引用

我们看下面的场景,我们运行发现,两个节点n1.n2 都没有析构。

        在出了作用域之后,首先把 n1,n2 两个对象析构,此时两边计数器均减为1,那么左边节点资源什么时候析构呢, 当n2->prev析构,也就是当右边节点资源析构,那么右边节点资源什么时候析构呢,当n1->_next析构,也就是当左边节点资源析构…我们发现,此时形成了一个类似于“死锁”的情况。

此时我们就要使用 weak_ptr 来解决 循环引用

weak_ptr是一个弱引用,它是为了配合shared_ptr而引入的一种智能指针,是为了解决循环引用而生的,为什么这么说呢,我们可以看看它的构造函数:

我们只能使用 wek_ptr或者 shared_ptr 去初始化它。

        我们在会产生循环引用的位置,把shared_ptr换成weak_ptr。 weak_ptr 不是一个RALL智能指针,它不参与资源的管理,他是专门用来解决引用计数的,我们可以使用一个shared_ptr 来初始化一个weak_ptr,但是weak_ptr 不增加引用计数,不参与管理,但是也像指针一样访问修改资源。

        实现一个weak_ptr


	template<class T>
	class weak_ptr
	{
	public:
		weak_ptr()
			:_ptr(nullptr)
		{}
		weak_ptr(shared_ptr<T>& sp)
			:_ptr(sp.get()),_pcount(sp.use_count())
		{}
		weak_ptr(weak_ptr<T>& sp)
			:_ptr(sp._ptr), _pcount(sp._pcount)
		{}
		weak_ptr& operator = (shared_ptr<T>& sp)
		{
			_ptr = sp.get();
			_pcount = sp.use_count();
			return *this;
		}
		weak_ptr& operator = (weak_ptr<T>& sp)
		{
			_ptr = sp._ptr;
			_pcount = sp._pcount;
			return *this;
		}
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		int use_count()
		{
			return *_pcount;
		}
	private:
		T* _ptr;
		int* _pcount;

	};

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

VPX基于全国产飞腾FT-2000+/64核+复旦微FPGA的计算刀片

6U VPX计算板 产品简介 产品特点 飞腾计算平台&#xff0c;国产化率100% VPX-MPU6902是一款基于飞腾FT-2000/64核的计算刀片&#xff0c;主频2.2GHz&#xff0c;负责业务数据流的管控和调度。搭配自带独立显示芯片的飞腾X100芯片&#xff0c;可用于于各类终端及服务器类应用场…

Spring与SpringBoot入门

Spring入门 要使用Spring最起码需要引入两个依赖: <!-- Spring Core&#xff08;核心&#xff09; --><dependency><groupId>org.springframework</groupId><artifactId>spring-core</artifactId><version>5.3.20</version>…

小白水平理解面试经典题目LeetCode 655. Print Binary Tree【Tree】

655 打印二叉树 一、小白翻译 给定二叉树的 root &#xff0c;构造一个 0 索引的 m x n 字符串矩阵 res 来表示树的格式化布局。格式化布局矩阵应使用以下规则构建&#xff1a; 树的高度为 height &#xff0c;行数 m 应等于 height 1 。 列数 n 应等于​​xheight1​​ - …

爆火的1分钟声音克隆GPT-SoVITS项目 linux系统 ubuntu22.04安装2天踩坑教程

原项目地址&#xff1a;https://github.com/RVC-Boss/GPT-SoVITS 1分钟素材&#xff0c;最后出来的效果确实不错。 1. cuda环境安装 cuda环境准备 根据项目要求在cuda11.8和12.3都测试了通过。我这里是用cuda11.8 cuda11.8安装教程&#xff1a; ubuntu 22.04 cuda多版本和…

【软件测试】--功能测试4-html介绍

1.1 前端三大核心 html:超文本标记语言&#xff0c;由一套标记标签组成 标签&#xff1a; 单标签&#xff1a;<标签名 /> 双标签:<标签名></标签名> 属性&#xff1a;描述某一特征 示例:<a 属性名"属性值"> 1.2 html骨架标签 <!DOC…

蓝桥杯第十四届电子类单片机组决赛程序设计

目录 前言 单片机资源数据包_2023&#xff08;点击下载&#xff09; 一、第十四届比赛题目 1.比赛题目 2.题目解读 1&#xff09;任务要求 2&#xff09;注意事项 二、显示功能实现 1.关于高位为0时数码管熄灭功能的实现 2.关于显示小数位的处理 3.关于“校准值”的…

某查查首页瀑布流headers加密

目标网站&#xff1a; 某查查 对目标网站分析发现 红框内的参数和值都是加密的&#xff0c;是根据算法算出来的&#xff0c;故进行逆向分析。 由于没有固定参数名&#xff0c;只能通过搜索headers&#xff0c;在搜索的位置上打上断点&#xff0c;重新请求。 断点在此处断住&a…

【计算机】本科考研还是就业?

其实现在很多计算机专业的学生考研&#xff0c;也是无奈的选择 技术发展日新月异&#xff0c;而在本科阶段&#xff0c;大家学着落后的技术&#xff0c;出来找工作自然会碰壁。而且现在用人单位的门槛越来越高&#xff0c;学历默认研究生起步&#xff0c;面试一般都是三轮起步…

循序渐进丨MogDB / openGauss 如何实现自增主键

概述 自增主键是我们在设计数据库表结构时经常使用的主键生成策略&#xff0c;主键的生成可以完全依赖数据库&#xff0c;无需人为干预&#xff0c;在新增数据的时候&#xff0c;我们只需要将主键的值设置为default&#xff0c;数据库就会为我们自动生成一个主键值。 MySQL 主键…

基于沁恒微 ch643q 多通道采集 adc 驱动层实现

一、代码 #include "main.h"/********************************************************************** fn ADC_Function_Init** brief Initializes ADC collection.** return none*/ void ADC_Function_Init(void) {ADC_InitTypeDef ADC_InitStructure …

【Go 快速入门】协程 | 通道 | select 多路复用 | sync 包

文章目录 前言协程goroutine 调度使用 goroutine 通道无缓冲通道有缓冲通道单向通道 select 多路复用syncsync.WaitGroupsync.Mutexsync.RWMutexsync.Oncesync.Map 项目代码地址&#xff1a;05-GoroutineChannelSync 前言 Go 1.22 版本于不久前推出&#xff0c;更新的新特性可…

LoRa技术在智能气象监测中的应用与解决方案分享

LoRa技术在智能气象监测领域的应用具有广泛的前景&#xff0c;通过LoRa技术可以实现对气象数据的远程采集、传输和监测&#xff0c;为气象行业提供更加智能化和高效的解决方案。以下将探讨LoRa技术在智能气象监测中的应用与解决方案分享。 首先&#xff0c;LoRa技术可以用于连…

python|闲谈2048小游戏和数组的旋转及翻转和转置

目录 2048 生成数组 n阶方阵 方阵旋转 顺时针旋转 逆时针旋转 mxn矩阵 矩阵旋转 测试代码 测试结果 翻转和转置 2048 《2048》是一款比较流行​的数字游戏​&#xff0c;最早于2014年3月20日发行。原版2048由Gabriele Cirulli首先在GitHub上发布&#xff0c;后被移…

【C语言】数据存储篇,内存中的数据存储----C语言整型,浮点数的数据在内存中的存储以及大小端字节序【图文详解】

欢迎来CILMY23的博客喔&#xff0c;本篇为​【C语言】数据存储篇&#xff0c;内存中的数据存储----C语言整型&#xff0c;浮点数的数据在内存中的存储以及大小端字节序【图文详解】&#xff0c;感谢观看&#xff0c;支持的可以给个一键三连&#xff0c;点赞关注收藏。 前言 C语…

GIS之深度学习02:Anaconda2019版本安装(py38)

Anaconda是一个专注于数据科学和机器学习的开源发行版&#xff0c;内置了丰富的工具和库&#xff0c;包括Python解释器、NumPy、SciPy、Pandas、Scikit-learn、TensorFlow等&#xff0c;使用户能够轻松进行科学计算和数据分析。其强大的包管理器conda简化了软件包的安装和环境管…

Linux/Spectra

Enumeration nmap 第一次扫描发现系统对外开放了22&#xff0c;80和3306端口&#xff0c;端口详细信息如下 22端口运行着ssh&#xff0c;80端口还是http&#xff0c;不过不同的是打开了mysql的3306端口 TCP/80 进入首页&#xff0c;点击链接时&#xff0c;提示域名不能解析&…

4核8G服务器并发数多少?性能如何?

腾讯云4核8G服务器支持多少人在线访问&#xff1f;支持25人同时访问。实际上程序效率不同支持人数在线人数不同&#xff0c;公网带宽也是影响4核8G服务器并发数的一大因素&#xff0c;假设公网带宽太小&#xff0c;流量直接卡在入口&#xff0c;4核8G配置的CPU内存也会造成计算…

vue3 增加全局水印(显示登录信息)

一、纯文字水印 在main.ts页面里面 加入以下代码&#xff1a; // 导入 Vue 的 createApp 函数 import { createApp } from vue;// 导入全局样式文件 import ./style.css;// 导入根组件 App.vue import App from ./App.vue;// 导入路由配置 import router from ./router;// 使…

VUE从0到1创建项目及基本路由、页面配置

一、创建项目:(前提已经安装好vue和npm) 目录:E:\personal\project_pro\ windows下,win+R 输入cmd进入命令行: cd E:\personal\project_pro E:# 创建名为test的项目 vue create test# 用上下键选择vue2或vue3,回车确认创建本次选择VUE3 创建好项目后,使用…

【CSS-语法】

CSS-语法 ■ CSS简介■ CSS 实例■ CSS id 和 class选择器■ CSS 样式表■ 外部样式表(External style sheet)■ 内部样式表(Internal style sheet)■ 内联样式(Inline style)■ 多重样式 ■ CSS 文本■ CSS 文本颜色■ CSS 文本的对齐方式■ CSS 文本修饰■ CSS 文本转换■ CS…