(十六)C++进阶之异常

1.0、说明

本章很多概念来自 《轻松搞定C++语言.pdf》,这份文档,感兴趣的同学可以去搜该文档来学习,个人觉得很不错,我在基础章节最后一章已经将该文档分享出来了,想要的同学可以去下载。

1.1、C++的异常

1)异常是一种程序控制机制,与函数机制独立和互补。

函数是一种以栈结构展开的上下函数衔接的程序控制系统,异常是另一种控制结构,它依附于栈结构,却可以同时设置多个异常类型作为网捕条件,从而以类型匹配在栈机制中跳跃回馈。

2)异常设计目的:
栈机制是一种高度节律性控制机制,面向对象编程却要求对象之间有方向、有目的的控制传动,从一开始,异常就是冲着改变程序控制结构,以适应面向对象程序更有效地工作这个主题,而不是仅为了进行错误处理。异常设计出来之后,却发现在错误处理方面获得了最大的好处。

1.2、异常处理的思想

我们在C语言中经常使用最多的就是利用函数返回值来判断函数执行结果,然后进行相应的策略处理,但是这种方式如果嵌套的函数都存在返回值,那么你就应该嵌套的去判断,在程序逻辑上反而相对有些繁琐,使用起来可能相对更加严谨,否则漏掉某个判断,有可能出现程序奔溃的后果。

但是C++中的异常判断机制先对而言就友好很多,他和函数使用是分开的,可以进行多层传递(如果你不在传递过程中的函数中捕获。)

我们看一下下面这这个图:
在这里插入图片描述

只要触发异常,他可以一直传递异常到捕获那个调用者,在哪里进行处理,总结道四点:

1) C++的异常处理机制使得异常的引发和异常的处理不必在同一个函数中,这样底层的函数可以着重解决具体问题,而不必过多的考虑异常的处理。上层调用者可以再适当的位置设计对不同类型异常的处理。

2)异常是专门针对抽象编程中的一系列错误处理的, C++中不能借助函数机制,因为栈结构的本质是先进后出,依次访问,无法进行跳跃,但错误处理的特征却是遇到错误信息就想要转到若干级之上进行重新尝试,如图

在这里插入图片描述
3)异常超脱于函数机制,决定了其对函数的跨越式回跳。
4)异常跨越函数。

1.3、异常处理的实现

基本语法:
在这里插入图片描述

左边就是通过一个关键字throw 向外抛异常,右边则是接受抛出来的异常,获取进行处理。

我们先简单实现一个例子来看一下:

#define _CRT_SECURE_NO_WARNINGS

#include <iostream>

using namespace std;

void fun(void)
{
	int a = 3;
	throw a;
}

int main(void)
{
	try{
		fun();
	}
	catch (int e){
		cout << "异常:" << e << endl;
	}
	return 0;
}

这个代码很简单,就是在一个函数内直接抛除一个异常出来,然后通过特定语句进行异常捕获,因为我们抛出来的异常是int型的,所以在接受的时候我们也要定义一个int型变量进行获取。

看一下运行结果:
在这里插入图片描述

当然int型是一个具体例子,他也可以往外面抛字符串,对象,指针、引用都是可以的,只需要你在接受的时候定义对应的类型将异常接住,但是如果说你不需要处理返回来的数据,只需要这样定义即可:

	try{
		fun();
	}
	catch (...){
		cout << "触发异常了" << endl;
	}

运行结果:
在这里插入图片描述

当然对于异常,最重要的一个特点就是你可以在没有被接受之前,可以跨越函数传递,例如下面:
在这里插入图片描述

我把进行抛异常的函数放到另外一个函数里面执行,但是最终接受这个函数的异常在主函数里面,所以运行结果:
在这里插入图片描述
一样是可以行得通的。

但是如果程序中不接受处理异常会怎么样呢?我们做个测试:
在这里插入图片描述

最终运行结果:
在这里插入图片描述

最终宕机了!,所以一定要注意这点。

最后记住下面这六个点:

1) 若有异常则通过throw操作创建一个异常对象并抛掷。
2) 将可能抛出异常的程序段嵌在try块之中。控制通过正常的顺序执行到达try语句,然后执行try块内的保护段。
3) 如果在保护段执行期间没有引起异常,那么跟在try块后的catch子句就不执行。程序从try块后跟随的最后一个catch子句后面的语句继续执行下去。
4)catch子句按其在try块后出现的顺序被检查。匹配的catch子句将捕获并处理
异常(或继续抛掷异常)。
5) 如果匹配的处理器未找到,则运行函数terminate将被自动调用,其缺省功能是调用abort终止程序。
6)处理不了的异常,可以在catch的最后一个分支,使用throw语法,向上扔。

1.4、栈解旋(unwinding)

何为栈解旋?其实很简单,我们可以这么理解,加入之前根据类定义的几个对象,对象里面开辟了一些内存块,那么这个时候,你抛出异常了,开辟的内存块会怎么办?如果你不运行完,他是不是一直留在里面,因为你没法调用析构函数去处理,所以从try进入之后创建的对象,到异常抛除前,会被系统调用对象的析构函数进行处理,所以只需要你在析构函数进行释放即可,其实你可以换个理解,就是抛出异常那一刻,该程序执行变相结束了,所以会调用析构函数,析构的顺序和构建相反,这就是所谓的栈解旋。

我们来看一下这段代码:

#define _CRT_SECURE_NO_WARNINGS

#include <iostream>

using namespace std;

class Test
{
public:
	Test(char num)
	{
		this->num = num;
		cout << "构建函数:" << this->num << endl;
	}
	~Test()
	{
		cout << "构建函数:" << num << endl;
	}
private:
	char num;
};

void test(void)
{
	Test b('b');
	Test c('c');

	throw "测试";
}

int main(void)
{
	Test a('a');


	try{
		test();
	}
	catch(...){

	}

	system("pause"); //我们先把系统暂停
	return 0;
}

代码很简单,就是测试抛除异常后是否会进行自动调用析构函数,运行结果如下:
在这里插入图片描述

因为我们调用系统的暂停,所以不会析构a,但是b和c就是谁先构造就后析构!

1.5、异常接口声明

关于异常可以抛出来的类型,其实也是可以做限制的,之前我们写的时候并没有做限制,所以可以抛多重类型。

乳沟你想限制抛出来的异常类型,格式如下:
void  func()  throw  (A, B, C , D); 就是你在后面定义好接受的类型,方便你对于异常的判断。

当然,如果你括号内什么都不写的话,就意味着什么类型都不接受,也就是你抛出来的异常没法处理,不建议这样写。

当然你也可以不在函数后面写上throw这个限定类型,不写的话就代表多种类型都可以抛出来。

1.6、异常生命周期问题

既然可以抛出来多种类型,比如引用,指针之类的,那么就必须涉及到一个生命周期问题,就比如很简单的局部变量,当运行完后会被清掉,这时候你返回来他的指针,然后你在通过指针去访问的话就是异常访问了,因为那是非法区了。

我们测试一下关于这些的使用:

第一种情况:直接定义相同的对象:


#define _CRT_SECURE_NO_WARNINGS

#include <iostream>
#include <string>

using namespace std;

class Out_Range
{
public:
	Out_Range(string data)
	{
		cout << "正常构造函数" << endl;
		this->info = data;
	}

	//拷贝构造函数
	Out_Range(Out_Range &anther)
	{
		cout << "拷贝构造函数" << endl;
		this->info = anther.info;
	}

	void print(void)
	{
		cout << this->info << ":超出范围了" << endl;
	}
private:
	string info;
};


void age_test(int age)
{
	Out_Range a("年龄");
	if (age > 100){
		throw a;
	}
}

int main(void)
{
	try{
		age_test(200);
	}
	catch (Out_Range e){
		e.print();
	}
	system("pause"); //我们先把系统暂停
	return 0;
}


这个代码只是测试是否会产生匿名对象,我们看一下运行结果:
在这里插入图片描述

这个结果就有意思,我们知道当创建对象 a的时候是调用 “正常构造函数”,但是这里有两处 “拷贝构造函数”,那么只能这样说明 在语句 throw的时候创建了一个匿名对象,我暂时定位b,那么流程就是这样Out_Range b = a; 这样就产生了第一个拷贝构造,但是在异常接受那里也出现一个赋值语句也就是 Out_Range e = b;这时候产生另外一个匿名对象,所以再次调用拷贝构造函数。

所以对于这种方式,如果内部类定义大数据的,居然要拷贝两份,简直是浪费空间

第二种情况,使用引用 接收,代码如下:


#define _CRT_SECURE_NO_WARNINGS

#include <iostream>
#include <string>

using namespace std;

class Out_Range
{
public:
	Out_Range(string data)
	{
		cout << "正常构造函数" << endl;
		this->info = data;
	}

	//拷贝构造函数
	Out_Range(Out_Range &anther)
	{
		cout << "拷贝构造函数" << endl;
		this->info = anther.info;
	}

	void print(void)
	{
		cout << this->info << ":超出范围了" << endl;
	}



private:
	string info;
};


void age_test(int age)
{
	if (age > 100){
		throw Out_Range("年龄");
	}
}

int main(void)
{
	try{
		age_test(200);
	}
	catch (Out_Range &e){
		e.print();
	}
	system("pause"); //我们先把系统暂停
	return 0;
}

在之前的那一份那里修改两处地方:

在这里插入图片描述
在这里插入图片描述

运行结果:
在这里插入图片描述

因为我们调用的是引用,所以少了一个拷贝构造,接下来我们测试一下指针。


#define _CRT_SECURE_NO_WARNINGS

#include <iostream>
#include <string>

using namespace std;

class Out_Range
{
public:
	Out_Range(string data)
	{
		cout << "正常构造函数" << endl;
		this->info = data;
	}

	//拷贝构造函数
	Out_Range(Out_Range &anther)
	{
		cout << "拷贝构造函数" << endl;
		this->info = anther.info;
	}

	void print(void)
	{
		cout << this->info << ":超出范围了" << endl;
	}



private:
	string info;
};


void age_test(int age)
{
	if (age > 100){
		throw new Out_Range("年龄");
	}
}

int main(void)
{
	try{
		age_test(200);
	}
	catch (Out_Range *pe){
		pe->print();

		delete pe;
	}
	system("pause"); //我们先把系统暂停
	return 0;
}

因为异常接收中是数据类型严格匹配的,所以在返回的时候必须创建一个指针 new,然后接受,最后在处理的时候调用delete 将她删除,防止内存泄漏,我们看一下 运行结果:
在这里插入图片描述

因为这个是直接返回指针,所以并不存在创建匿名对象,所以没有调用拷贝构造函数,但是你必须在处理完后delete释放指针,所以也稍微有点麻烦。

总结一下这三种情况:

第一种直接创建对象接受:

会创建两次匿名对象,所以他的生命周期在返回的那一刻就已经结束了。

第二种使用引用接收:
会创建一次匿名对象,然后生命周期也在返回的那一刻结束了,后来使用的是匿名对象的值。

第三种使用指针来接受:
知道处理完后,调用delete才会结束,但是必须记得回收,否则造成内存泄漏。

首先不推荐直接第一种情况,太浪费空间了,创建两次匿名对象,第二种情况和第三种情况看情况使用,如果你的类中存在大数据,建议你是用第三种,毕竟不存在拷贝,但是要时刻记得释放内存,如果你类中没有大数据,那么强烈建议你是用第二种情况,因为防止第三种遗忘释放内存导致内存泄漏。

1.7、标准程序库异常

其实在之前都是我们自己定义自己的异常,但是这就存在一个问题,当多人合作开发的时候,因为是自己自定义异常,那么必定存在不兼容问题,大家各自定义各自的,所以C++中存在一个标准程序异常库。

在这里插入图片描述

在这里插入图片描述

异常都是标准异常里面定义,如果你想使用其中的一种,引入头文件后,即可使用。

但是如果还是想自定义自己的怎么办?
我们需要继承exception这个类,然后在里面实现两个方法,第一个是:
what()方法,返回const  char*  类型(C风格字符串)的值,描述异常信息。

第二个则是析构函数,为什么析构函数需要定义呢,因为防止在析构函数里面进行异常抛除,如果在类销毁之前的析构函数里面抛出异常,则会程序中止,具体缘由你们可以想一下。


#define _CRT_SECURE_NO_WARNINGS

#include <iostream>
#include <string>
#include <exception>

using namespace std;

class PersonErr :public exception
{
public:
	PersonErr()
	{
		this->info = "你不是人";
	}
	virtual const char* what() const throw () {
		return this->info.c_str();
	}

	~PersonErr() throw(){} //限制你在析构函数里面使用异常



private:
	string info;
};


void age_test(int age)
{
	if (age > 1000){
		throw PersonErr();
	}
}

int main(void)
{
	try{
		age_test(2000);
	}
	catch (exception &e){
		cout << e.what() << endl;
	}
	system("pause"); //我们先把系统暂停
	return 0;
}

先看一下运行结果:
在这里插入图片描述

这里需要注意点,下面这个代码我们是调用父类的引用,同时我们观察what函数的实现,我们可以知道what函数其实是虚函数来的,这就是类的多态。

catch (exception &e){
发布了29 篇原创文章 · 获赞 0 · 访问量 405

猜你喜欢

转载自blog.csdn.net/weixin_42547950/article/details/104552577