C++ 异常

目录

异常的概念

异常的用法

异常的基础用法

异常与多态

异常重新抛出

异常的执行过程

异常的规范使用

函数的异常抛出声明

规范的异常体系

要点梳理


异常的概念

在C语言中有两种传统的错误处理机制:

  1. 强制终止程序,比如assert等,当发生诸如内存错误,除0错误时就会终止程序。缺陷:比较暴力,用户难以接受。
  2. 返回错误码,缺陷:需要程序员自己去查找对应的错误。如系统的很多库的接口函数都是通过把错误码放到errno中,表示错误。

也就是说,在C语言中基本都是使用返回错误码的方式处理错误,部分情况下使用终止程序处理非常严重的错误。

而在C++中通常采用"抛异常"的方式来处理错误。C++异常是一种处理错误的方式,当一个函数发现自己无法处理的错误时就可以抛出异常,让函数的直接或间接的调用者处理这个错误。

异常的用法

异常的基础用法

C++异常的基本用法就是抛出异常(throw)和尝试捕获异常(try - catch),即一个异常的基础写法通常包含throw、try、catch这三个部分。其中,throw用于抛出异常;try,用于尝试捕获代码块中的异常;catch,用于匹配捕获到的不同类型的异常,并作出不同的反馈。

写法格式如下:

cpp 复制代码
try
{
	// try a throw
}
catch (ExceptionName e1)
{
	// catch - e1
}
catch (ExceptionName e2)
{
	// catch - e2
}
catch (...)
{
	// catch - others
}

其中,catch(...)表示捕获其它任意类型的异常,通常用于捕获未知或者未设置的异常,以预防抛出异常而没捕获的情况发生。

而抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会生成一个拷贝对象。

用法示例如下:

cpp 复制代码
void test()
{
	// ...
	throw "const char*型的异常"; // 常量字符串是const char*型的
}


int main()
{
	try
	{
        // 其中,这里也可以直接throw抛出一个异常
		test();
	}
	catch (const int errint)
	{
		cout << "int型的异常" << endl; // 抛出的是const char* 的异常,所以不会匹配到这里
	}
	catch (const char* errmsg)
	{
		cout << errmsg << endl; // 类型匹配,所以最终打印结果就是:const char*型的异常
	}
	catch (...)
	{
		cout << "未知异常" << endl; // 用于防止抛出异常而没捕获的情况发生。
	}
	return 0;
}

异常与多态

当我们的异常体系很小时,catch的类型与抛出的类型完全匹配是完全行得通的,但如果我们的异常体系变得庞大时,如果还是有一个throw的类型就写一个catch语句就会显得十分冗杂。

所以我们可以利用C++的多态机制来灵活地处理这种情况。catch一个父类引用,这样就可以仅用一个catch语句块就可以处理多种相同体系的异常问题了。例如:

cpp 复制代码
// 基类异常
class baseException
{
public:
	virtual string what()
	{
		return "baseException.";
	}
};
// student子类异常
class studentException : public baseException
{
public:
	virtual string what()
	{
		return "student error!";
	}
};
// teacher子类异常
class teacherException : public baseException
{
public:
	virtual string what()
	{
		return "teacher error!";
	}
};

// 主函数,测试异常
int main()
{
	try
	{
		throw studentException(); // try中抛出子类异常
	}
	catch (baseException& e) // 父类引用遇到子类对象形成多态
	{
		cout << e.what() << endl;
	}

	return 0;
}

异常重新抛出

C++中还支持异常的重新抛出,即在catch语句中继续抛出异常。那么为什么要重新抛出异常呢?例如考虑如下场景:(内容参考:C++中异常处理中的异常重新抛出的一种用法

假设存在一个第三方库,我们需要使用自己的函数进行调用,有如下代码:

cpp 复制代码
#include<string>
#include <iostream>
 
using namespace std;
 
/*第三方库中函数 void func(int i)
	异常代码   -1:运行时错误
					 -2:数据超界异常
*/
void func(int i)
{
	if(i<0)
		throw -1;
	if(i>100)
		throw -2;
	
}
 
int main()
{
	try
	{
		func(199);
	}
catch (int i)
	{
		cout<<"Error Code: "<<i<<endl;
	}
	return 0;
}

//运行结果为  Error Code: -2

此时我们根本不知道-2代表什么意思,只能去查找函数的手册,不仅麻烦,而且不直观。所以我们可以考虑对异常重新抛出,那么改进后的代码如下:

cpp 复制代码
#include<string>
#include <iostream>
 
using namespace std;
 
/*第三方库中函数 void func(int i)
	异常代码   -1:运行时错误
			   -2:数据超界异常
*/
void func(int i)
{
	if(i<0)
		throw -1;
	if(i>100)
		throw -2;
	
}
//这是我们自己的库。调用第三方库的函数void func(int i)
void myFunc(int i)
{
	try 
	{
		func(i);
	}
	catch(int i)
	{
		switch(i)
		{
			case -1:
				throw "Runtime Error";
			break;
			case -2:
				throw "Data Error";
			break;
		}
	}
}

int main()
{
	try
	{
		myFunc(199);
	}
	catch (const char *s)
	{
		cout<<"Error Code: "<<s<<endl;
	}
	return 0;
}

//结果输出为  Error Code: Data Error

异常的执行过程

当执行到throw语句时,首先会检查当前的throw语句是否在try块内部,如果是,就在当前函数栈中查找匹配的catch语句。如果匹配到了则直接跳到catch的地方执行。如果没有相匹配的catch块,则退出当前函数栈,在上层函数栈帧中继续查找尝试匹配。如果到达main函数的栈,都没有匹配的catch,就会终止程序,有些编译器还会报错。例如:
图片出处: C++异常详细介绍-CSDN博客

上述沿着调用链查找匹配的catch块的过程叫栈展开或者栈解旋。也就是说异常被抛出后,从进入try块起,到异常被抛掷前(遇到throw之前),这期间在栈上构造的所有对象,都会被自动析构,其中析构的顺序与构造的顺序相反。其原因自然就与函数栈帧的开辟与释放分不开了,其具体细节就不再过多的阐述了。

需要注意的是,throw在一个函数中的效果和return有些类似。当执行到throw语句之后就会立即去寻找匹配的try语句块并跳到对应的catch语句块中,就不会再执行throw后续的代码了。

异常的规范使用

函数的异常抛出声明

一般来说,为了代码的可读性与规范型,抛出异常的函数通常需要在函数声明部分,以throw(...)的形式声明抛出异常的类型(声明和定义分开时,两个都可以写声明throw部分)示例如下:

cpp 复制代码
// 这里表示这个函数会抛出A、B、C、D中的某种类型的异常
void fun() throw(A,B,C,D);

// 这里表示这个函数只会抛出bad_alloc的异常
void* operator new (std::size_t size) throw (std::bad_alloc);

// 这里表示这个函数不会抛出异常
void* operator new (std::size_t size, void* ptr) throw();
void* operator new (std::size_t size, void* ptr) noexcept; // C++11支持

其中,早期用throw()来表示没有异常抛出,C++11之后可以用noexcept代替。

函数的异常抛出声明并不影响函数的任何功能,即它并没有实质性地影响函数的运行,只是一个便于代码阅读的声明。也就是说,如果实际抛出的异常和声明部分的类型不对应,并不会导致任何运行和编译错误。不过还是要保持统一的,因为如果不保持统一,那么这个异常抛出声明不但没有提高效率,反而还可能会造成很多麻烦。

规范的异常体系

实际中,并不是我们想抛什么异常就抛什么异常,这样会导致捕捉的时候不好捕捉。而是会建立一个异常体系,结合多态的性质,在抛出异常时,只需要用基类进行捕捉即可。

其中,在C++库中也建立了一个异常体系。也给我们提供了一些异常类。我们可以在程序中使用这些标准异常,它们就是以父子类的层次结构组织起来的(图片摘自:C++ 异常处理 | 菜鸟教程

说明如下:

要点梳理

  1. try和catch语句块不能省略后面的大括号,且try和catch之间不能有其它语句。
  2. try和catch必须匹配使用,即如果只有try没有catch就会报错。
  3. 捕获列表catch是按照抛出异常的类型进行捕获的。
  4. catch(...)表示捕获其它任意类型的异常,通常用于捕获未知或者未设置的异常,以预防抛出异常而没捕获的情况发生。
  5. 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会生成一个拷贝对象。
  6. 被选中的处理代码的调用链是,找到类型匹配且离抛出异常位置最近的catch语句块。
  7. 捕获是根据抛出的类型进行捕获的,捕获之后可以继续抛出新的异常。
  8. 如果抛出了异常但没捕获,程序会异常终止。而如果没有捕获到异常则会跳过整个异常捕获部分,包括catch(...)语句块
  9. throw与return类似,异常抛出后会立即结束try块与原函数,所以throw后面部分的代码不会被执行。
  10. 实际中抛出和捕获的类型不一定要类型完全匹配,可以抛出派生类对象,使用基类引用来捕获,这个在实际生活中很实用。
相关推荐
Theodore_10223 小时前
4 设计模式原则之接口隔离原则
java·开发语言·设计模式·java-ee·接口隔离原则·javaee
‘’林花谢了春红‘’4 小时前
C++ list (链表)容器
c++·链表·list
----云烟----5 小时前
QT中QString类的各种使用
开发语言·qt
lsx2024065 小时前
SQL SELECT 语句:基础与进阶应用
开发语言
开心工作室_kaic5 小时前
ssm161基于web的资源共享平台的共享与开发+jsp(论文+源码)_kaic
java·开发语言·前端
向宇it5 小时前
【unity小技巧】unity 什么是反射?反射的作用?反射的使用场景?反射的缺点?常用的反射操作?反射常见示例
开发语言·游戏·unity·c#·游戏引擎
武子康5 小时前
Java-06 深入浅出 MyBatis - 一对一模型 SqlMapConfig 与 Mapper 详细讲解测试
java·开发语言·数据仓库·sql·mybatis·springboot·springcloud
转世成为计算机大神6 小时前
易考八股文之Java中的设计模式?
java·开发语言·设计模式
机器视觉知识推荐、就业指导6 小时前
C++设计模式:建造者模式(Builder) 房屋建造案例
c++
宅小海6 小时前
scala String
大数据·开发语言·scala