C++中的异常实例详解
时间:2022-08-23 10:22:57|栏目:C代码|点击: 次
1. 异常
异常: 异常是面向对象与法处理错误的一种方式
1.1 C语言中处理错误的方式
- 返回错误码 (很多API接口都会把错误码放到errno当中)
- 终止程序 (assert终止、除零错误、段错误) [产生信号去终止进程]
- C标准库中setjump和longjump组合
1.2 C语言处理错误方式的缺陷
- 使用错误码时,还需要查看错误码表,去找每个错误码的含义
- 当一个函数是通过返回值去输出数据,那么发生错误的时候很难处理 (不好区设置发生错误时返回什么)
- 如果调用的函数栈很深时,使用错误码去一层层返回时,处理起来很麻烦
//第2点 T& operator[](int index) { //问题: 当访问的下标index超出容器的范围,此时该返回什么值? //解决方案: 可以再传一个参数,用于获取真正的结果(当输出型参数), 返回值就用于判断是否正确的返回了. //实际上还是很麻烦 }
2. C++异常
2.1 异常相关关键字
- try: try块中放置可能会抛出异常的代码,这段代码被称为保护代码
- catch: catch用于捕获异常,catch块中写处理异常的代码。它跟在try的后面,1个try后面可以跟多个catch
- throw: throw用于抛出异常
try { //保护代码(可能会抛出异常的代码) }catch(ExceptionType e1) { //处理异常 }catch(ExceptionType e2) { //处理异常 }catch(ExceptionType eN) { //处理异常 }
2.2 异常的使用
2.2.1 异常的抛出和匹配原则
- 异常是通过抛出对象而引发的,该对象的类型决定了应该匹配到哪个catch块
- 异常的抛出会去匹配在整个调用链中与该对象类型相匹配且距离最近的那个
- 当异常抛出后,执行流会直接跳转到整个调用链当中能catch到异常的位置处,不能catch的地方就不会执行了,所以这可能导致内存泄漏、文件流没关闭、锁未释放**等情况。(free、fclose、unlock未执行)
- catch(…)可以用来捕获任意类型对象的异常,一般用于表示"未知异常"或被用作异常的重新抛出时的接收catch块
- 在抛出与捕获的类型匹配上并不全都是类型相同才能匹配。我们可以使用基类去捕获 —> 抛出的派生类的对象。 //会在下面具体讲解
· 原则2:函数调用链中的异常 - 栈展开的匹配原则
- 查看throw是否在try的内部,如果存在能匹配到的catch语句,就跳转到catch中进行异常的处理。
- 如果没有匹配的catch就退出当前栈,去查找调用链当中的能够匹配到的catch
- 如果在main函数中都没有找到匹配的catch,则终止程序。
栈展开:上述的这个沿着调用链去逐个栈中查找匹配的catch语句的过程就是栈展开。
//代码演示:f3()中抛出异常,该异常会被f1()捕捉,而在main函数中的catch是无法捕捉到的 void f3() { throw 123; } void f2() { f3(); } void f1() { try { f2(); cout << "f1() not catched Exception!" << endl; }catch(int& err) { cout << "f1()-err: " << err << endl; } } int main() { try { f1(); cout << "main not catched Exception!" << endl; }catch(int& err) { cout << "main-err: " << err << endl; } return 0; }
注意:
抛出异常对象后,会生成一个异常对象的拷贝(可能是一个临时对象),这个拷贝的临时对象在被catch后会被销毁。异常的执行流执行顺序:从throw抛出异常处跳转到调用链中能够匹配的catch语句中;然后执行catch块中的代码;catch执行完毕后在当前函数栈中顺序执行。(整个调用链中没有匹配的就终止程序)
2.3 异常的重新抛出
?可能会存在单个catch不能完全处理一个异常的情况,在经过一些校正处理后,我们希望将该异常交给外层调用链中的函数来处理,此时我们可以通过在catch中重新抛出异常的方式把异常传递给调用链的上层函数处理。
//在SecondThrowException()函数中我们要delete[]动态开辟(new出来)的空间, //如果不使用异常的重新抛出的话,就会造成内存泄漏问题 (也可以使用RAII) void FirstThrowException() { throw "First throw a exception!"; } void SecondThrowException() { int* arr = new int[10]; try { FirstThrowException(); }catch(...) { cout << "Delete[] arr Success!" << endl; delete[] arr; throw; } } void SoluteException() { try { SecondThrowException(); }catch(const char* err) { cout << err << endl; } } int main() { SoluteException(); return 0; }
2.4 自定义异常体系
自定义异常体系实际上就是自己定义的一套异常管理体系,很多公司当中都会自定义自己的异常体系以便于规范的进行异常管理。它主要用到了我们在上面所说的一条规则: 我们只需要抛出派生类对象,然后捕获基类对象就可以了。这样的抛出与捕获方式非常便于异常的处理。
class MyException { public: MyException(string errmsg, int id) :_errmsg(errmsg), _id(id) {} virtual string what() const = 0; //必须放到public下才能让类外定义的成员访问到 protected: int _id; //错误码 string _errmsg; //存放错误信息 //list<StackInfo> _traceStack; //存放调用链的信息 //... }; class CacheException : public MyException { public: CacheException(string errmsg, int id) :MyException(errmsg, id) {} virtual string what() const { return "CacheException!: " + _errmsg; } }; class NetworkException : public MyException { public: NetworkException(string errmsg, int id) :MyException(errmsg, id) {} virtual string what() const { return "NetworkException!: " + _errmsg; } }; class SqlException : public MyException { public: SqlException(string errmsg, int id) :MyException(errmsg, id) {} virtual string what() const { return "SqlException!: " + _errmsg; } }; int main() { try { //抛出任意的派生类对象 throw SqlException("sql open failed", 10); } catch (const MyException& e) //只需要捕获基类对象 { cout << e.what() << endl; //这里实际上完成了一个多态 } catch (...) //走到这里说明出现未知异常 { cout << "Unknown Exception!" << endl; } return 0; }
2.5 异常安全
- 最好不要在构造函数中抛异常,构造函数是完成对象的构造和初始化的,在里面抛异常可能会导致对象构造不完整或没有完全初始化。 (可能会造成内存泄漏)
- 最好不要在析构函数中抛异常,析构函数是完成资源的清理工作的,在里面抛异常可能导致资源没清完就结束了函数调用,从而导致内存泄漏、句柄未关闭等问题。
- 注意new、fopen、lock的使用
2.6 异常规范
异常规范的指定是为了让使用者知道函数可能抛出哪些异常,用法:
void func1() throw(); //表示该函数不会抛异常 void func2() noexcept; //等价于throw() 表示该函数不会抛异常 void func3() throw(std::bad_alloc); //表示该函数只会抛出bad_alloc的异常 void func4() throw(int, double, string); //表示该函数会抛出int/double/string类型中的某种异常
2.7 C++标准库的异常体系
C++提供了一系列标准的异常,定义在中,下面是这些异常的组织形式。
异常 | 描述 |
---|---|
std::exception | 该异常是所有标准 C++ 异常的父类。 |
std::bad_alloc | 该异常可以通过 new 抛出。 |
std::bad_cast | 该异常可以通过 dynamic_cast 抛出。 |
std::bad_exception | 这在处理 C++ 程序中无法预期的异常时非常有用。 |
std::bad_typeid | 该异常可以通过 typeid 抛出。 |
std::logic_error | 理论上可以通过读取代码来检测到的异常。 |
std::domain_error | 当使用了一个无效的数学域时,会抛出该异常。 |
std::invalid_argument | 当使用了无效的参数时,会抛出该异常。 |
std::length_error | 当创建了太长的 std::string 时,会抛出该异常。 |
std::out_of_range | 该异常可以通过方法抛出,例如 std::vector 和 std::bitset<>::operator。 |
std::runtime_error | 理论上不可以通过读取代码来检测到的异常。 |
std::overflow_error | 当发生数学上溢时,会抛出该异常。 |
std::range_error | 当尝试存储超出范围的值时,会抛出该异常。 |
std::underflow_error | 当发生数学下溢时,会抛出该异常。 |
int main() { try { vector<int> v(5); v.at(5) = 0; //v.at(下标) = v[下标] + 抛异常 } catch(const exception& e) { cout << e.what() << endl; } catch(...) { cout << "Unknown Exception!" << endl; } }
2.8 异常的优缺点
2.8.1 优点
- 清晰的显示出错误信息
- 可以很好地解决返回值需要返回有效数据的函数 //如T& operator[ ] (int index)
- 在多层函数调用时发生错误,可以用直接在外层进行捕获异常
- 异常在很多第三方库中也有使用 //boost、gtest、gmock
2.8.2 缺点
- 异常会导致执行流跳转,这会使得调试分析程序时更加困难
- C++没有GC (垃圾回收机制),使用异常时可能导致资源泄露等异常安全问题。
- C++标准库的异常体系不实用
- C++的允许抛出任意类型的异常,在项目中不进行规范管理的话,会十分混乱。 //一般需要定义一套继承体系的异常规范