16.C++ 异常处理的基本与进阶用法:从基础到高级技巧

一、前言

异常是C++处理代码逻辑错误的一种方式,被广泛应用于各种高级语言中,比如java、python等等。

比异常更加原始的错误处理方式则是根据函数返回值判断,事实上这种方式如今在C/C++中依旧是用的最多的。

原因无它,使用异常会在一定程度上影响程序的性能,对于C/C++这种对性能要求高的语言来说,自然而来的会出现不受重视的情况。

但同样的,如果开发对性能要求不高的程序中,使用异常在一定程度上是可以让我们的代码变得更简洁的。

二、基本使用

C++中异常的使用非常简单,只有三个关键字:trycatchthrow

其中throw关键字被用来抛出异常,而trycatch这两个关键字则用来接收异常。

一个最简单的使用示例如下:

#include<iostream>
using namespace std;
int main() {
	try
	{
		throw "抛出异常";
	}
	catch (const char* err)
	{
		printf("处理异常:%s",err);
	}
}

执行结果:

image-20231218181744710

其中try后面跟着一个大括号,也被简称为try块,只要try块中执行到了throw关键字,那么程序就会立马从这里中断,并将throw后面紧跟的值抛出给catch块进行处理。

由于在try块中我抛出的是字符串,所以catch块的参数我就填写的const char*

注意两者必须要类型匹配,否则会导致抛出的异常没有catch块处理,那么程序就会直接挂掉。

为了更加深刻的理解上面这个过程,再来看一下下面这段代码:

image-20231218182337501

可以看到,一旦执行到了throw关键字,那么throw后面的代码就不会再被执行了,并将程序的执行流程直接跳到了匹配的catch块中去执行。

当然,上面这只是一种简单写法,为的是让大家稍微理解一下它的执行逻辑,一般我们不会这样写代码。

大多数时候,throw关键字是被嵌入到某个函数中的,就像下面这样:

image-20231218182733969

这是最常见的用法,比如上面的代码中,除数不能为0是最基本的要求,如果外面传入了0,那么就可以通过throw关键字来抛出一个异常交由外面的调用者处理。

三、使用进阶

上面的代码虽然已经能很好的运行了,但大多数时候我们可能只是想简单捕获一下错误,不让程序因为没有处理异常而直接挂掉而已。

最笨拙的一种方法就是写多个catch块:

image-20231218183221773

也就是将所有可能出现的异常结果都写一个catch块与之对应,不过显然,这是一件非常麻烦的事情。

所以我们可以使用下面的代码替换:

image-20231218183349039

我们可以直接使用...来捕获所有异常,此时无论其内部抛出什么内容,都会来到这个catch块中执行。

并且需要注意的是,catch块的执行顺序是从上到下依次匹配的,就和if语句中的else if一样,基于这个特性,我们就可以只处理自己感兴趣的异常:

image-20231218183617086

此时的效果就是,只有当try块中抛出了字符串类型的异常才会到第一个catch块中处理,其它异常则直接默认交给了最后一个catch块处理。

四、异常类

以上基本就是异常的常见用法了,但这里还是要稍微介绍一点其它我们在编程中可能会很常见的东西,那就是异常类。

从前面的学习中我们可以看到,我们可以抛出各种基本数据类型,但事实上不仅仅是基本数据类型,类对象也可以被抛出。

基于此,C++官方则推出了标准异常类,可以让我们直接使用:

image-20231218184203946

也就是exception类,在std命名空间中,使用方法其实和前面的基本数据类型是一样的。

只不过这里将基本数据类型换成了对象而已,比如throw关键字后面就是在构造一个异常类的对象,它构造函数可以接受一个字符串参数用来描述这个异常。

然后在外面捕获的时候,就可以通过它身上的what函数来获取这个字符串信息。

这样看起来它和直接使用字符串也没什么区别呀?那我们为什么还要有这么一个异常类呢?

原因很简单,那就是这样就可以利用类的继承特性:父类可以接受子类对象

因为...这种方式虽然能捕获所有异常,但同样的,这样做我们就没办法知道里面抛出的错误信息了。

而使用异常类的方式,却可以很好的解决这一问题,

比如C++标准库的异常类有如下关系图:

image-20231218184816143

也就是说,exception类是所有C++异常类的父类,这也就意味着,只要我们捕获一个exception类,就可以完成所有异常类的捕获,并且还能打印出它们的错误信息:

image-20231218185112387

比如这里,虽然我抛出了的是一个无效参数的类,但依旧能被它的父类所捕获到,并打印出来信息。

除此之外,我们也可以自定义自己的异常类:

image-20231218185432280

但要注意,由于exception类中的数据设置为了私有,所以只能在构造函数中通过初始化列表完成对父类数据的初始化操作。

只有这样,之后调用继承下来的what函数时,才能打印出来错误信息。

五、noexcept

C++11 引入了 noexcept 关键字,用于指定函数是否会抛出异常。如果一个函数声明为 noexcept,编译器可以进行一些优化,比如消除异常处理代码。

void test() noexcept {
	throw std::runtime_error("Error");
}
int main() {
	try
	{
		test();
	}
	catch (const std::exception&)
	{
		printf("111");
	}
}

一旦函数被设定为了noexcept,那么异常就无法被捕获了:

image.png

作者:余识
全部文章:0
会员文章:0
总阅读量:0
c/c++pythonrustJavaScriptwindowslinux