异常
一、C语言传统的错误处理机制
- 终止程序:使用assert函数或者程序错误(内存错误,除0错误等等),当其发生时就会终止程序,但这种方式用户难以接受。
- 返回错误码:当程序出现错误时,返回错误编号(错误码)。例如,系统的很多库的接口函数都是通过把错误码放到errno中表示错误。但这种方式需要程序员根据错误码去查找对应的错误。
- 在实际中,C语言基本都是使用返回错误码的方式处理错误,部分情况下使用终止程序处理非常严重的错误。
二、异常
1、概念
- C++的异常处理机制通过try、catch和throw三个关键字,为开发者构建了一个结构清晰、易于理解的异常处理框架。
- 异常是一种处理错误的方式,当一个函数发现自己无法处理的错误时就可以抛出异常,让函数的直接或间接的调用者处理这个错误。
- 当程序执行到可能抛出异常的代码段时,可以使用try块将其包围起来。然后通过一个或多个catch块来捕获并处理可能发生的特定类型的异常。而throw关键字则用于在程序中显式地抛出异常,通知上层调用者当前代码遇到了无法继续执行的情况。
- 异常机制不仅使得异常处理代码与正常业务逻辑代码分离,提高了代码的可读性和可维护性,还通过异常的传播机制,使得开发者能够在更高层次上统一处理异常,从而避免了错误处理的代码在程序中到处蔓延,导致代码结构混乱。
2、关键字
- throw:抛出异常。当程序出现错误且有需要的异常抛出时,可以使用throw关键字抛出对应的异常。
- catch:捕获异常。在想要处理异常的地方,通过(一个或多个)catch关键字捕获对应类型的异常。如果捕获到异常,则在对应的catch函数体内进行对应异常的处理程序或者操作。
- try:try块(函数体)中的代码标识将被激活的特定异常,它(函数体)后面通常跟着一个或多个catch块捕获对应的异常并处理。
- 如果有抛出异常,则需要使用try和catch关键字捕获异常。try 块中放置可能抛出异常的代码,其中的代码被称为保护代码。
3、示例
try
{
// 保护的标识代码
}catch( ExceptionName e1 )
{
// catch块
}catch( ExceptionName e2 )
{
// catch块
}catch( ExceptionName eN )
{
// catch块
}
三、异常的使用
1、异常的抛出和匹配原则
- 异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个catch的处理代码。而被选中的catch块是调用链中与该抛出对象类型匹配且离抛出异常位置最近的那一个。
- 因为抛出的异常对象可能是一个临时对象。所以,抛出异常对象后,会生成一个异常对象的拷贝。这个拷贝的对象会在被catch以后销毁,它的处理方式类似于函数的传值返回。
- 当不知道异常的错误,即catch的类型不确定。可以使用参数包…(catch(…))的方式捕获任意类型的异常。
- 抛出和捕获的匹配原则有个例外,即抛出和捕获的类型并不都是完全匹配的。可以抛出派生类对象,使用基类捕获。
2、在函数调用链中异常栈展开匹配原则
- 栈展开为沿着调用链查找匹配的catch子句的过程。
- 首先检查throw操作本身是否被try包含,即是否在try函数体内。如果是再查找匹配的catch语句,如果有匹配的,则跳到对应的catch块处处理对应的异常;如果没有匹配的,则退出当前函数栈,继续在调用函数的栈中查找匹配的catch。如果到达main函数的函数栈依旧没有匹配的catch,则终止程序。
- 在实际中,最后都要加一个catch(…)捕获任意类型的异常,否则当抛出的异常没被捕获时,程序会直接终止。
- 找到匹配的catch子句并处理异常后,会继续沿着catch子句后面的程序(代码)继续执行。即异常被处理后,程序还会继续正常执行,只不过异常出现到异常被捕获和处理之间的程序(代码)不会执行。
3、栈展开示意图
4、示例代码
class Test
{
public:
Test()
{
cout << "Test()" << endl;
}
~Test()
{
cout << "~Test()" << endl;
}
};
double Division(int len, int time)
{
if (time == 0)
{
throw "除零错误";
}
return (double)len / (double)time;
}
void F1()
{
throw 1;
}
void Func()
{
Test t;
try
{
int len, time;
cin >> len >> time;
cout << Division(len, time) << endl;
}
catch (const char* s)
{
cout << s << endl;
}
F1();
cout << "snowdragon" << endl;
}
int main()
{
try
{
Func();
}
catch (const char* s)
{
cout << s << endl;
}
catch (...)
{
cout << "未知异常" << endl;
}
return 0;
}
5、运行结果
四、异常的重新抛出
1、作用
- 当单个的catch不能完全处理一个异常,在进行一些校正处理以后,希望再交给更外层的调用链函数来处理时,catch可以通过重新抛出,将异常传递给更上层的函数进行处理。
2、示例代码
double Division(int len, int time)
{
if (time == 0)
{
throw "除零错误";
}
return (double)len / (double)time;
}
void Func2()
{
int* array1 = new int[10];
cout << "new[] array1" << endl;
int* array2 = new int[20];
cout << "new[] array2" << endl;
try
{
int len, time;
cin >> len >> time;
cout << Division(len, time) << endl;
}
catch (...)
{
delete[] array1;
cout << "delete[] array1, catch (...)" << endl;
delete[] array2;
cout << "delete[] array2, catch (...)" << endl;
throw;
}
delete[] array1;
cout << "delete[] array1, Func2()" << endl;
delete[] array2;
cout << "delete[] array2, Func2()" << endl;
}
int main()
{
try
{
Func2();
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
return 0;
}
3、运行结果
五、异常安全
- 构造函数的功能为完成对象的构造和初始化。所以,最好不要在构造函数中抛出异常,否则可能导致对象不完整或没有完全初始化。
- 析构函数的功能为完成资源的清理。所以,最好不要在析构函数内抛出异常,否则可能导致资源泄漏,如内存泄漏、句柄未关闭等等。
- C++中异常经常会导致资源泄漏的问题,比如在new和delete中抛出了异常,导致内存泄漏,在lock和unlock之间抛出了异常导致死锁。以上问题在C++中经常使用RAII来解决,RAII相关内容参见智能指针(RAII)。
六、异常规范
1、概念
- 异常规格说明的目的是为了让函数使用者知道该函数可能抛出的异常有哪些。 可以在函数的后面接throw(类型),列出这个函数可能抛掷的所有异常类型。列出的异常类型可以不抛出,没有列出的异常类型可以抛出。
- 函数的后面接throw(),表示函数不抛异常。但函数体内还是可以抛异常且不会报错。
- 若无异常接口声明,则此函数可以抛掷任何类型的异常。
- C++11中的noexcept表示不会抛异常,如果抛出异常则会报错。
2、示例代码
void func1() throw(const char*, int*)
{
throw 1.2;
}
void func2() throw()
{
throw "snowdragon,func3()";
}
void func3() noexcept
{
//throw "snowdragon,fun2()";
}
int main()
{
try {
func1();
}
catch (...)
{
cout << "func1(),异常" << endl;
}
try {
func2();
}
catch (...)
{
cout << "func2(),异常" << endl;
}
try {
func3();
}
catch (...)
{
cout << "func3(),异常" << endl;
}
return 0;
}
3、运行结果
七、异常体系
1、C++标准库
(1)概念
- 标准异常的基类为exception。标准库的组件抛出的所有对象都来自此类,即基类exception和异常之间是以父子类层次结构组织起来的。因此,通过引用捕获基类exception,可以捕获所有标准异常。
(2)示意图
2、自定义
(1)作用
- 虽然可以继承C++标准库中exception类实现自己的异常类,但是C++标准库设计的不够好用。
- 在实际中可以定义一套继承的异常规范体系。当抛出的都是继承同一个基类的派生类对象时,捕获该基类就可以达到使用目的,即可以捕获对应的所有异常。
(2)示意图
八、异常的优缺点
1、优点
- 异常对象定义好了,相比错误码的方式可以清晰准确的展示出错误的各种信息,甚至可以包含堆栈调用的信息,这样可以帮助使用者更好地定位程序的bug。
- 返回错误码的传统方式在函数调用链中比较麻烦,当深层的函数返回错误码时,需要层层返回错误码,这样在最外层才能拿到对应的错误码。在这一过程中,如果不加以判断,则程序会继续运行,错误码也不会正常返回。而异常使用起来就相对来说比较简单,当它出现时,会直接跳转到对应的catch块中对对应的异常进行处理,使用者不需要对异常跳转途中的代码和函数调用链的处理做判断。
- 很多的第三方库都包含异常,比如boost、gtest、gmock等等常用的库,使用它们也需要使用异常。
- 部分函数使用异常更好处理,比如构造函数没有返回值,不方便使用错误码的方式处理;在重载[]函数(T& operator[](size_t pos))中,即在数组中查找元素并返回的函数中,查找的变量pos越界访问了只能使用异常或者终止程序处理,没办法通过返回值表示错误。
- 异常总体而言利大于弊,OO(面向对象)的语言基本都是用异常处理错误的。
2、缺点
- 运行时出错抛异常会导致程序的执行流乱跳且非常的混乱,导致在跟踪调试以及分析程序时比较困难。
- 异常会有一些性能的开销,但在现代硬件速度很快的情况下,这个影响基本忽略不计。
- C++没有垃圾回收机制,资源需要自己管理。而异常非常容易导致内存泄漏、死锁等异常安全问题。这个需要使用RAII来处理资源的管理问题,即学习成本较高。
- C++标准库的异常体系定义得不好,导致大家各自定义各自的异常体系,非常的混乱。
- 异常尽量规范使用,如果随意抛异常,外层捕获异常和处理的程序就会非常复杂和难。所以,异常规范有两点,其一为抛出异常的类型都继承同一个基类;其二为函数是否抛异常、抛什么类型的异常,都使用 func() throw(异常类型); 的方式规范化。
本文到这里就结束了,如有错误或者不清楚的地方欢迎评论或者私信
创作不易,如果觉得博主写得不错,请点赞、收藏加关注支持一下💕💕💕