C++相关概念和易错语法(30)(异常、智能指针)

news2024/12/23 15:54:46

1.异常

在C语言这样的面向过程编程的语言来说,处理错误一般有两种方式:终止程序,如assert;返回错误码,比如打开文件失败,errno就会改变,perror就会打印错误码对应的错误信息

面向对象的语言中,异常是更常见的处理错误的方式,如连接服务器我们不会一次就成功,需要多尝试几次,不能一失败就终止程序。errno也过于局限,错误码的分配有限,不能自定义,同时错误码还必须及时手动处理,否则会被覆盖

(1)处理方式

先执行try内部语句,内部可能有throw抛异常,catch捕获异常并处理

我们可以看到,先执行try里面的语句,在fun()里遇到了throw语句抛出异常,抛出异常后终止后续所有代码的执行,之后catch捕获后进行处理。只有抛了异常才会走catch,如果不抛异常就不会走catch。抛异常是传值返回,返回的是拷贝的对象,返回右值会调用移动构造简化。

下面是一个更直观的例子,可以更好理解使用规则

(2)详细规则

在try语句内部(try中调用函数也算作在try代码块中,如上面的例子)抛出异常后,会直接跳转到catch语句,跳转规则是会沿着函数栈帧(调用函数的顺序)层层往回找,先看抛异常的语句在不在try代码块中,再看抛出的异常的类型有没有匹配当前catch,如果匹配了就会执行当前catch语句以及后面的语句,返回上层栈帧后如果本身还在try内部,那么不会进入任何catch语句,就算有匹配的,可以理解为抛出的异常用一次就销毁了。

#include <iostream>
using namespace std;

void fun2()
{
	throw "fun2()";
}

void fun1()
{
	try
	{
		fun2();
	}
	catch(const char* msg)
	{
		cout << "fun1()" << endl;
	}

	cout << "fun1()" << endl;
}

int main()
{
	try
	{
		fun1();
	}
	catch (const char* msg)
	{
		cout << "main()" << endl;
	}

	return 0;
}

结果是

我们可以看到fun2抛出异常后,会层层往上找,fun2是在fun1的try语句内,所以会被fun1的catch捕获,catch后的语句正常执行,再往上走发现fun1和fun2其实都是在main函数的try语句内,但是总共就抛出一次异常并被解决了,所以后面就不会执行catch语句

注意层层向上匹配时需要严格匹配,抛出的常量字符串不会被char*捕获,也不能用string捕获,如果抛出int异常自然也不会转为size_t。所以fun1中的catch语句及之后的代码不会执行,而会匹配main函数中的catch语句。抛异常后的下一句执行代码一定是向上找第一次完美匹配的catch语句的第一行代码

如果到了main函数还是找不到对应的catch,即抛出的异常没有被处理,就会直接终止程序,编译器认为异常没有被处理一定是存在问题的。

所以我们要保证所有的异常能得到处理,我们可以使用catch(...)兜底,catch(...)能在其它catch语句匹配不上时派上用场,至少不会让程序直接终止

有个小细节,即catch(...)只能放在最后作为兜底,不过也几乎没人这么做,这里提一下

(3)子类异常用父类捕获

先看看下面的代码,顺便复习复习继承和多态


#include <iostream>
using namespace std;

class B;

class A
{
public:
	virtual B& CreateMessage(int id = 1, const string& errmsg = "error") = 0
	{}

	int _id;
	string _errmsg;
};

class B : public A 
{
public:
	B& CreateMessage(int id, const string& errmsg)
	{
		_id = id;
		_errmsg = errmsg;
		return *this;
	}
};

int main()
{
	try
	{
		A* throwmsg = new B;

		throw throwmsg->CreateMessage();
	}
	catch (A& msg)
	{
		cout << msg._id << ":" << msg._errmsg << endl;
	}

	return 0;
}

结果是

首先使用父类指针指向子类空间构成多态,虚函数表存的CreateMessage()是B中的,当以A指针调用函数时匹配的是B的内容,但是多态中,都是以父类声明+子类定义调用函数,所以不需要传参,用纯虚函数的缺省值就可以了,实际走的代码还是B中的。

抛出的B可以被A&捕获,这其实也和赋值兼容转换结合起来了,同时也再次强调赋值兼容转换不是类型转换,因为catch是严格匹配的,不允许赋值兼容转换,因此这在逻辑上是合理的。

(4)异常规范

确定不会抛异常的在函数后面加noexcept,这样能很好规范异常的使用

写了noexcept后就算再抛异常也不会编译报错,但是noexcept会影响编译器逻辑,运行时不会捕获抛出的异常,就算看上去能匹配catch也会报错,这也相当于另一种规范

(5)标准库异常体系

当我们调用库中的函数出了问题时,就会抛出异常,我们可以用const exception& e接收异常,exception是一个类,我们可以用成员函数e.what()得到具体错误信息

下面是常见的用法

(6)C++异常缺点

异常使用频繁会导致代码执行位置乱跳,标准库的异常体系并不是太好用,一般来说都是自定义异常体系,同时noexcept也不是硬性规定。

但是最大的问题还是安全问题,即抛出异常后后续代码都会终止,这可能会导致已开辟的空间没有办法delete,出现内存泄漏,这极难控制,需要引入更复杂的解决办法,后续会讲到。

我为这个问题举个例子

如果arr1开辟失败要抛异常,如果arr2开辟失败也要开辟异常,arr3同理,而且还可能出现arr1和arr2都开辟好了,但arr3开辟失败,这个时候还要处理arr1和arr2的释放。我们发现要处理的异常极多,根本没有办法涵盖所有情况,所以我们用常规思维解决不了内存泄漏的问题。

2.智能指针(RAII思想)

RAII思想是实现智能指针的核心,它利用了C++局部对象自动销毁的特性(类的对象自动调用析构函数)来控制资源的生命周期,就能很好地防止内存泄漏,下面举个简单的例子

当调用函数时,创建了Ptr类的对象,资源获得立即初始化,这个对象掌管着堆区开辟的数组,当返回函数栈帧时,就会自动调用析构函数,把资源释放掉,因此我们堆区开辟的空间就不会泄露了

这本质上是借助对象的生命周期来控制程序资源,使用模板就可以管理任意类型的资源,智能指针就是按照这个思路来实现的,相当于在原本管理数据的int*外再包一层,这样就能解决异常乱跳导致的问题。

(1)unique_ptr

用法:unique_ptr<int> up(new int)

就和我们前面的使用一样,unique_ptr本质也是利用RAII思想实现的类,靠着类的生命周期来防止开辟的空间无法被delete,就算我们不知道unique_ptr具体实现,但是也能很快弄清具体的功能,直接用unique_ptr开辟空间也要安全得多

智能指针还能模仿原生指针的相关操作,如operator*和operator->(和迭代器的实现一样)

因此,堆区动态开辟空间应尽量交给智能指针管理。

智能指针难度在于拷贝,我们所需的拷贝是浅拷贝,而不是深拷贝,因为它是要模仿指针的拷贝,不同的智能指针可以指向同一块空间,但问题在于析构多次会导致越界访问。unique_ptr的特点就是禁止任何拷贝和赋值,一片空间只能交给一个unique_ptr管理。

拷贝构造和赋值重载都被delete掉,或是使用了private修饰,不能显式调用,也就实现了禁止拷贝赋值的操作,进而也就不会出现同一块空间用多个unique_ptr管理的情况。

在不需要同一块空间用多个unique_ptr管理时,可以多使用unique_ptr。

unique_ptr还需要处理自定义类型,如数组的开辟

第一种解决办法就是在模板参数类型后面加[ ]

第二种办法就是手动实现仿函数(删除器)

先看一下下面的代码,想一想是怎样使用的。


#include <iostream>
#include <vector> 
using namespace std;

template<class T>
class DeleteArray
{
public:
	void operator()(T* t)
	{
		cout << "DeleteArray" << endl;
		delete[] t;
	}
};

class DeleteFile
{
public:
	void operator()(FILE* f)
	{
		cout << "DeleteFile" << endl;
		if (f)
			fclose(f);
	}
};

class A
{
public:
	~A()
	{
		cout << "~A()" << endl;
	}
	int _a = 10;
};

int main()
{

	unique_ptr<FILE, DeleteFile> up1(fopen("test.txt", "r"));
	unique_ptr<A, DeleteArray<A>> up2(new A[10]);

	return 0;
}

结果是

第二个模板参数删除器是一个仿函数,unique_ptr会将里面存放的指针以仿函数的形式传过去,以此来进行自定义处理。

unique_ptr删除器对象不能作为参数传递

通过删除器和类型 + [ ]的使用,我们能处理任何指针类型了,只不过这种使用形式要多记忆一下,容易混淆。

(2)shared_ptr基本使用

unique_ptr的功能几乎完美,唯独缺失了拷贝和赋值的操作。

shared_ptr支持拷贝,采用了引用计数,每当新增一个shared_ptr管理一块空间,就为它计数++,每析构一次就计数--,最后一个析构的释放空间。这类似于最后一个人关灯的操作。

我们可以看见,shared_ptr能够精准避免越界访问的情况。

下面看一看自定义类型如何处理,用法几乎一致,但有区别


#include <iostream>
#include <memory>
using namespace std;

template<class T>
class DeleteArray
{
public:
	void operator()(T* t)
	{
		cout << "DeleteArray" << endl;
		delete[] t;
	}
};

class DeleteFile
{
public:
	void operator()(FILE* f)
	{
		cout << "DeleteFile" << endl;
		if (f)
			fclose(f);
	}
};


class A
{
public:
	~A()
	{
		cout << "~A()" << endl;
	}
};


int main()
{
	shared_ptr<A> sp1(new A[10], DeleteArray<A>());//开辟了一块新的空间
	shared_ptr<A> sp2(sp1);	
	
	shared_ptr<FILE> sp3(fopen("test.txt", "r"), DeleteFile());//开辟了一块新的空间
	shared_ptr<FILE> sp4(sp3);

	shared_ptr<A[]> sp5(new A[5]);//开辟了一块新的空间


	return 0;
}

结果是

shared_ptr删除器对象只能作为函数参数传递

(3)shared_ptr模拟实现

shared_ptr很重要,下面通过其具体实现来加深印象,并且找出shared_ptr的漏洞

这是模拟实现shared_ptr的成员变量,_ptr用于存储开辟的空间,_del用于接收删除器对象,_pcount是用于计数。

在这个模板类中,构造函数的第一个参数用于接收开辟空间的指针对象,如new int返回的int*;第二个参数利用了包装器,用lambda表达式做缺省值(匿名函数对象拷贝出临时对象,被包装器接收),默认以delete来释放空间,我们也可以手动传匹配的函数形式的对象(如仿函数,函数指针,仿函数),它们都能被包装器接收,这也体现包装器的优势。

通过构造和拷贝构造我们就能知道,shared_ptr再调用构造时(开辟新空间)就开辟一个存计数的空间,如果后续调用拷贝构造就会++计数,同理调用析构会--,如果为计数为0就delete开辟的空间。

这里需要理解为什么要用这种方式计数,为什么不用static?

static变量的特点就是一个类就只有一份,这就意味着当我们用同一个类实例化出多个对象时,计数就完全不可控了。假设同一个类有2个对象,4个指针管理,其中3个指针指向其一对象,另一个指针指向另一个对象。但static的计数始终为4,static完全没办法区分这两个对象,所以不可用。

最后还剩下赋值重载,需要注意两点,第一点即瞻前顾后,第二点则是处理自己给自己赋值

下面是所有代码实现,应该很快就能理解了

	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr = nullptr, const function<void(T*)>& del = [](T* t)
			{
				cout << "默认删除器" << endl;
				delete t;
				return;
			})
			:_ptr(ptr)
			, _del(del)
			, _pcount(new int(1))
		{}

		shared_ptr(const shared_ptr& sp)
		{
			(*(sp._pcount))++;
			_ptr = sp._ptr;
			_del = sp._del;
			_pcount = sp._pcount;
		}

		~shared_ptr()
		{
			release();
		}

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

		T& operator*() const
		{
			return *_ptr;
		}

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

		shared_ptr& operator=(const shared_ptr& sp)
		{
			if (_ptr == sp._ptr)
				return *this;

			release();

			_ptr = sp._ptr;
			_del = sp._del;
			_pcount = sp._pcount;
			(*_pcount)++;

			return *this;
		}

		size_t use_count() const
		{
			return *_pcount;
		}

		T* get() const
		{
			return _ptr;
		}

	private:
		T* _ptr;
		function<void(T*)> _del;
		int* _pcount;
	};

注意const修饰函数可以防止使用该函数时发生权限放大的情况

下面对拷贝操作进行讲解

一定要体会右值的处理,这样才能理解为什么不需要写移动构造。

移动构造的底层就是将有用的指针和无用的指针做交换,将无用的指针析构。如果实现移动构造当然也没问题,不实现是因为析构时有一个条件判断阻止了delete,总体上和值拷贝没任何区别。

(4)循环引用、weak_ptr

看一下下面这段代码,为什么会出现这种状况

class A类似于双向链表,A* _next和A* _prev指向后一个和前一个A。但是由于异常的处理麻烦,我们使用智能指针来包装指针,实现同样的功能并且能自动析构。像上述代码如果觉得难以理解的话,先将它们看作A*,再替换成智能指针,结合operator*和operator->仔细体会。

看懂代码后,我们就能明白sp1和sp2互指

我们可以很好理解sp2和sp1的计数都是2,当析构时,它们都只会计数--,不会delete。最后变成use_count() == 1,这个时候析构操作已经完成了,但空间没有释放,造成了内存泄漏。这就叫循环引用。

处理循环引用,我们需要使用另一种智能指针weak_ptr

下面是一个粗略的实现,帮我们简单看看weak_ptr的结构

weak_ptr不同于其它智能指针,它不支持直接管理资源,它配合解决shared_ptr的一个缺陷,即循环引用导致的泄漏。

weak_ptr不支持RAII,不支持管理资源,也不能用operator*和operator->访问。

在用法上,weak_ptr<int> wp(new int) 这种操作是不可行的(不支持RAII),但是weak_ptr可以用shared_ptr构造和赋值,而shared_ptr可以用weak_ptr构造

当在循环引用出现时使用weak_ptr避免时,使用sp1->_next = sp2就调用了weak_ptr<T>& operator=(const shared_ptr<T>& sp)这个赋值重载。

我们还可以借助make_shared<T>(new T)来让weak_ptr指向数据空间

wp只有在构造的那行才有用,过了之后shared_ptr就析构了(临时对象),这个时候wp就失效了,也叫悬空。这又如何处理?

weak_ptr中use_count()记录了管理的数据有多少次计数,当计数为0时就标记为已失效。expired()就是这个功能,为真时就表示失效

我们还可以在悬空前将数据进行转移,lock()就能实现这项功能

转移前后count计数会++(weak_ptr不会增加计数,转移到shared_ptr,shared_ptr会增加计数),只不过上面的代码是先++,后析构--,整体不变

下面的代码可以说明这一点

我们要把weak_ptr理解为一个单独存储数据的类,不会增加计数。存储的数据很完备,有_ptr、_pcount、_del,但是由于weak_ptr不会--计数,即不会析构,所以当原数据被释放后,就有可能出现悬空的情况。我们可以用expired()检查是否悬空,也可以在悬空前用lock()把数据转移出去。

(5)智能指针总结

C++98推出了auto_ptr(失败的设计,拷贝是管理权转移,拷贝后原来的自动指针为空,调用原来的指针会直接报错),在C++11又推出了unique_ptr、shared_ptr、weak_ptr,用于解决绝大多数内存泄露的场景。当不涉及拷贝时可用unique_ptr,涉及拷贝、传值返回用shared_ptr,weak_ptr用于解决循环引用。

(6)内存泄漏

智能指针要处理的就是内存泄漏问题,即占据着内存却不使用,内存不断被消耗,会导致最终程序被卡死。一般来说,短期快速出现的内存泄漏更容易被发现,而长期运行的慢速内存泄漏的程序影响很大,如服务器几乎不停服,就算每次内存泄漏一点,但时间一长,就会造成服务器崩溃。当今的windows、linux都有各自的内存泄漏检测工具。我们写代码时,如果管理好资源,就能防止内存泄漏。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

云原生存储Rook部署Ceph

Rook 是一款云原生存储编排服务工具&#xff0c;Ceph 是一种广泛使用的开源分布式存储方案&#xff0c;通过Rook 可以大大简化 ceph 在 Kubernetes 集群中的部署和维护工作。 Rook 由云原生计算基金会( CNCF )孵化&#xff0c;且于 2020 年 10 月正式进入毕业阶段。Roo…

【python因果推断库3】使用 CausalPy 进行贝叶斯geolift 分析

目录 导入数据 丹麦的销售额是否有地理提升&#xff08;GeoLift&#xff09;&#xff1f; 结果 本笔记本介绍如何使用 CausalPy 的贝叶斯{术语}合成控制功能来评估“地理提升”&#xff08;GeoLift&#xff09;。我们的假设情景如下&#xff1a; 你是一家在欧洲运营的公司的…

集成电路学习:什么是ISP系统编程

一、ISP&#xff1a;系统编程 ISP&#xff08;In-System Programming&#xff09;即系统编程&#xff0c;是一种在系统内部进行的编程方法&#xff0c;主要用于对闪存&#xff08;FLASH&#xff09;、EEPROM等非易失性存储器的编程。ISP编程提供了巨大的灵活性&#xff0c;允许…

SaaS用户增长:提升转化率的实践路径

在SaaS&#xff08;软件即服务&#xff09;行业这片竞争激烈的蓝海中&#xff0c;企业要实现稳健的用户增长&#xff0c;必须聚焦于优化用户获取与转化策略&#xff0c;以提升用户转化率。用户转化率&#xff0c;作为衡量SaaS产品市场吸引力和用户接纳度的核心指标&#xff0c;…

图文解析保姆级教程: IDEA里面创建SpringBoot工程、SpringBoot项目的运行和测试、实现浏览器返回字符串

文章目录 一、创建SpringBoot工程&#xff08;需要联网&#xff09;二、 定义请求处理类三、运行测试 此教程摘选自我的笔记&#xff1a;黑马JavaWeb开发笔记13——Springboot入门&#xff08;创建、运行&测试项目&#xff09;、Http协议&#xff08;请求&响应协议&…

Unity实战案例 2D小游戏HappyGlass(模拟水珠)

本案例素材和教程都来自Siki学院&#xff0c;十分感谢教程中的老师 本文仅作学习笔记分享交流&#xff0c;不作任何商业用途 预制体 在这个小案例中&#xff0c;水可以做成圆形但是带碰撞体&#xff0c;碰撞体比图形小一圈&#xff0c;顺便加上Trail renderer组件 材质 将碰撞…

SVN介绍和使用

一、SVN&#xff08;Subversion&#xff09; SVN 是一种版本控制系统&#xff0c;可以用于管理和控制文件的变更。以下是SVN的基本使用步骤&#xff1a; 安装SVN&#xff1a;首先&#xff0c;您需要在计算机上安装SVN客户端。您可以从Subversion官方网站下载安装程序&#xff…

sql-labs61-65关通关攻略

第61关 一&#xff1a;查看数据库 ?id1)) and updatexml(1,concat(1,(select database())),1)-- 二&#xff1a;查看表名 ?id1)) and updatexml(1,concat(1,(select group_concat(table_name) from information_schema.tables where table_schemasecurity)),1)-- 三&#…

MATLAB/Simulink 汽车ABS仿真模型 防抱死刹车 教程 资料 程序 模型 论文 视频

项目概述 防抱死制动系统&#xff08;ABS&#xff09;是现代车辆中的一项重要安全技术&#xff0c;它能够在紧急制动时防止车轮锁死&#xff0c;从而提高车辆的稳定性和操控性。本项目旨在使用MATLAB/Simulink建立一个完整的ABS仿真模型&#xff0c;帮助学习者理解ABS的工作原理…

WebRTC协议下的视频汇聚融合技术:EasyCVR构建高效视频交互体验

视频汇聚融合技术是指将来自不同源、不同格式、不同网络环境的视频流进行集中处理、整合和展示的技术。随着视频监控、远程会议、在线教育、直播娱乐等领域的快速发展&#xff0c;视频数据的规模急剧增长&#xff0c;对视频处理能力和效率提出了更高要求。视频汇聚融合技术通过…

【杭州】目前就业情况-自述

博主在今年6月份&#xff0c;被自己领导下达了裁员通知&#xff0c;所以近期一直都没有更新博文。那么接下来简单介绍下杭州2024年就业情况吧&#xff01; 目录 一、行情 二、薪资 三、外包 四、如果你真快吃不上饭了 五、博主被问的面试题 一、行情 今年应该是有史以…

低代码技术助力移动端开发:简化开发流程,实现快速创新

在移动互联网快速发展的今天&#xff0c;企业和开发者面临着越来越高的需求&#xff0c;要求开发高质量、功能强大的移动应用&#xff0c;以满足用户的期待和市场的变化。然而&#xff0c;传统的移动端开发流程通常复杂且耗时&#xff0c;需要投入大量的资源和开发人员。为了应…

Unity坐标系计算3D中两直线的最短距离及最近点的几何原理

方法1&#xff1a; 已知空间中两直线AB, CD&#xff0c;判断它们是否相交 问题的关键是求出这两条直线之间的最短距离&#xff0c;以及在这个距离上最接近两线的点坐标&#xff0c;判断该点是否在直线AB和直线CD上。 首先将直线方程化为对称式&#xff0c;分别得到两直线方向向…

VUE 实现三级权限选中与全选

功能&#xff1a;点击全选时所有子级选中&#xff0c;点击子级时对应的所有父级要选中。 实现思路&#xff1a;通过递归将所有子级转化为一级&#xff0c;选中时将选中的ID存为一个二级数组。循环时判断当前项在选中的数组中存在时即为勾选状态。 1、所有子级选中&#xff1a…

【盖世汽车-注册安全分析报告】

前言 由于网站注册入口容易被黑客攻击&#xff0c;存在如下安全问题&#xff1a; 暴力破解密码&#xff0c;造成用户信息泄露短信盗刷的安全问题&#xff0c;影响业务及导致用户投诉带来经济损失&#xff0c;尤其是后付费客户&#xff0c;风险巨大&#xff0c;造成亏损无底洞…

python django 使用教程

前言 python django使用起来简单方便&#xff0c;大大节省开发时间&#xff0c;提高开发效率&#xff0c;现在介绍下如何使用 一、创建 Django 项目 首先&#xff0c;建立虚拟环境&#xff0c;&#xff08;最好养成一个项目一个环境的习惯&#xff0c;防止多个项目pip包混乱问…

Web3与AI的融合:开启去中心化应用的新纪元

在数字科技不断发展的今天&#xff0c;Web3与人工智能&#xff08;AI&#xff09;的融合正引领去中心化应用&#xff08;DApps&#xff09;的新纪元。这种结合不仅扩展了去中心化技术的应用场景&#xff0c;还为智能应用提供了更加高效和创新的解决方案。本文将深入探讨Web3与A…

深入理解HTTP连接池及其在Java中的应用

更多内容前往个人网站&#xff1a;孔乙己大叔 在现代的Web开发中&#xff0c;HTTP请求已经成为应用程序与外部服务交互的主要方式。随着微服务架构的流行&#xff0c;一个应用可能需要同时与多个外部服务进行通信&#xff0c;这导致HTTP请求的数量显著增加。为了提升性能和资源…

微信小程序垃圾回收的前景方向

在当今这个环保意识日渐增强的时代&#xff0c;如何有效处理日常生活产生的垃圾已成为亟待解决的社会问题。微信小程序凭借其便捷性和广泛的用户基础&#xff0c;在推广垃圾分类与回收方面展现出巨大潜力。作为一款集智能化分类指导、在线预约回收、环保知识普及于一体的微信小…

使用JavaScript读取手机联系人列表:从理论到实践

更多内容前往个人网站&#xff1a;孔乙己大叔 在现代Web开发中&#xff0c;随着技术的不断进步&#xff0c;以前看似不可能的任务现在变得可行。例如&#xff0c;使用JavaScript读取手机联系人列表这一功能&#xff0c;在几年前几乎是不可想象的&#xff0c;但现在随着Web API的…