c语言处理错误的方式
c语言传统的错误处理机制是这样子的
- 终止程序,如assert。缺陷:用户难以接受。如发生内存错误,除0错误时就会终止程序。
- 返回错误码。缺陷:需要程序员自己去查找对应的错误。如系统的很多库的接口函数都是通过把错误码放到errno中,表示错误。
- C标准库中setjmp和longjmp组合。(不常用)
我们在实际写c语言的过程中基本都是用错误码在报错 在发生很严重的错误时使用断言报错
C++异常概念
异常是面向对象语言常用的一种处理错误的方式 当一个函数发现自己无法处理的错误时就可以抛出异常 让函数直接或间接的调用者处理这个错误
这里有三个关键字
- throw:当程序出现问题时 可以通过throw关键字抛出一个异常
- try:try块中放置的是可能抛出异常的代码 该代码块在执行时将进行异常错误检测 ry块后面通常跟着一个或多个catch块
- catch:如果try块中发生错误 则可以在catch块中定义对应要执行的代码块
语法示例如下
try { //被保护的代码 } catch (ExceptionName e1) { //catch块 } catch (ExceptionName e2) { //catch块 } catch (ExceptionName eN) { //catch块 }
异常的用法
异常的抛出和捕获
异常的抛出和捕获的匹配原则:
- 异常是通过抛出对象而引发的 该对象的类型决定了应该激活哪个catch的处理代码 如果抛出的异常对象没有捕获 或是没有匹配类型的捕获 那么程序会终止报错
- 被选中的处理代码(catch块)是调用链中与该对象类型匹配且离抛出异常位置最近的那一个
- 抛出异常对象后 会生成一个异常对象的拷贝 因为抛出的异常对象可能是一个临时对象 所以会生成一个拷贝对象 这个拷贝的临时对象会在被catch捕获以后销毁(类似于函数的传值返回)
- catch(…)可以捕获任意类型的异常 但捕获后无法知道异常错误是什么
- 实际异常的抛出和捕获的匹配原则有个例外 捕获和抛出的异常类型并不一定要完全匹配 可以抛出派生类对象 使用基类进行捕获 这个在实际中非常有用
在函数调用链中异常栈展开的匹配原则:
- 当异常被抛出后 首先检查throw本身是否在try块内部 如果在则查找匹配的catch语句 如果有匹配的则跳到catch的地方进行处理
- 如果当前函数栈没有匹配的catch则退出当前函数栈 继续在上一个调用函数栈中进行查找匹配的catch 找到匹配的catch子句并处理以后 会沿着catch子句后面继续执行 而不会跳回到原来抛异常的地方
- 如果到达main函数的栈 依旧没有找到匹配的catch 则终止程序
现在我们使用一段代码来解释上面的匹配原则
void func1() { throw string("这是一个异常"); } void func2() { func1(); } void func3() { func2(); } int main() { try { func3(); } catch (const string& s) { cout << "错误描述:" << s << endl; } catch (...) { cout << "未知异常" << endl; } return 0; }
上面的代码中有四个函数 分别是main函数 func1 func2 func3
其中main函数调用了func3 func3调用了func2 func2调用了func1 再然后func1抛出了一个异常
针对这段代码我们来分析下函数调用链中的匹配问题
当func1中的异常被抛出之后
- 当func1中的异常被抛出之后 首先它会判断自己是否再try的内部 判断出不在之后他会返回上一层的函数栈帧
- 在func2中它仍然不在try的内部 继续返回上一层的函数栈帧
- 在func3中它仍然不在try的内部 继续返回上一层的函数栈帧
- 在main函数中它判断出自己在try的内部了 并且开始找符合要求的catch语句 捕获异常成功
演示效果图如下
代码实际运行结果如下
大家也许注意到了 我们在最后加上了一个catch(…) 这是因为有可能出现未知类型的异常 而此时如果该异常未被捕获 则有可能导致程序崩溃
下面是示例
而如果加上了最后捕获未知异常的代码就不会出现这种问题了
异常的重新抛出
有时候单个的catch可能不能完全处理一个异常 在进行一些校正处理以后 希望再交给更外层的调用链函数来处理 比如最外层可能需要拿到异常进行日志信息的记录 这时就需要通过重新抛出将异常传递给更上层的函数进行处理
如果直接让最外层捕获异常进行处理可能会引发一些问题 比如
void func1() { throw string("这是一个异常"); } void func2() { int* array = new int[10]; func1(); //do something... delete[] array; } int main() { try { func2(); } catch (const string& s) { cout << s << endl; } catch (...) { cout << "未知异常" << endl; } return 0; }
- 上面这段代码中 有三个函数 分别是func1 func2 main
- 其中main函数调用func2 func2开辟了一块空间之后调用func1
- func1抛出了一个异常开始往前找匹配的catch
- func1 和 func2的函数栈帧里面都没有找到 于是找到了main函数中并且异常被捕获
- 但是再销毁func2函数栈帧的过程中我们并没有销毁func2开辟出来的内存 于是乎造成了内存泄漏
为了解决内存泄漏问题 我们可以这样子重构代码
我们再func2中捕获func1中抛出的异常 捕获后将申请的内存释放 并且重新抛出
代码如下
void func2() { int* array = new int[10]; try { func1(); //do something... } catch (...) { delete[] array; throw; //将捕获到的异常再次重新抛出 } delete[] array; }
这里有两点需要注意的
- 因为我们不确定抛出异常的是什么类型 所以我们要使用 catch (…) 捕获
- 此外重新抛出的时候我们直接throw就好 不用指定类型
异常安全
对于异常安全问题这里有三点建议
- 构造函数完成对象的构造和初始化 最好不要在构造函数中抛出异常 否则可能导致对象不完整或没有完全初始化
- 析构函数主要完成对象资源的清理 最好不要在析构函数中抛出异常 否则可能导致资源泄露(内存泄露、句柄未关闭等)
- C++中异常经常会导致资源泄露的问题 比如在new和delete中抛出异常 导致内存泄露 在lock和unlock之间抛出异常导致死锁 C++经常使用RAII的方式来解决以上问题。
(RAII其实就是利用对象来管理资源的一种方式)
异常规范
为了让函数使用者知道某个函数可能抛出哪些类型的异常 C++标准规定:
- 在函数的后面接throw(type1, type2, …) 列出这个函数可能抛掷的所有异常类型
- 在函数的后面接throw()或noexcept(C++11)表示该函数不抛异常。
- 若无异常接口声明 则此函数可以抛掷任何类型的异常(异常接口声明不是强制的)
代码示例如下
//表示func函数可能会抛出A/B/C/D类型的异常 void func() 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(); // C++11 中新增的noexcept,表示不会抛异常 thread() noexcept; thread (thread&&x) noexcept;
但是由于这个规范并不是强制性的 所以说其实对于现实写代码的影响并不是那么大 也很少有人能够按照这个规范标准来写