34.异常 throw和try-catch
C++ 提供了一系列标准的异常,我们可以在程序中使用这些标准的异常。在定义中,它们是以父子类层次结构组织起来的
异常 | 描述 |
---|---|
std::exception | 该异常是所有标准C++异常的父类 |
std::bad_alloc | 该异常可以通过new抛出 |
std::bad_cast | 该异常可以通过dynamic_cast抛出。这在处理C++程序中无法预期的异常时非常有用 |
std::bad_exception | 该异常可以通过typeid抛出 |
std::bad_typeid | 理论上可以通过读取代码来检测到的异常 |
std::logic_error | 当使用了一个无效的数学域时,会抛出该异常 |
std::domain_error | 当使用了无效的参数时,会抛出该异常 |
std::invalid_argument | 当创建了太长的sta:string时,会抛出该异常 |
std::length_error | 该异常可以通过方法抛出,例如std:vector和std:bitset>:operatorl(0) |
std::out_of_range | 理论上不可以通过读取代码来检测到的异常 |
std::runtime_error | 当发生数学上溢时,会抛出该异常 |
std::overflow_error | 当尝试存储超出范围的值时,会抛出该异常 |
std::range_error | 当发生数学下溢时,会抛出该异常 |
std::underflow_error | 当发生数学下溢时,会抛出该异常 |
实际中我们可以可以去继承exception类实现自己的异常类。但是实际中很多公司像上面一样自己定义一套异常继承体系。因为C++标准库设计的不够好用。
以out_of_range为例:
#include using namespace std; #define DefaultArraySize 10 template class Array{ private: int size; elemType* ia; public: explicit Array(int sz = DefaultArraySize) { size = sz; ia = new elemType[size]; } ~Array(){ delete[] ia; } elemType& operator[](int ix) const { if (ix < 0 || ix >= size) { string eObj = "out_of_range error in Array::operator[]()"; throw out_of_range(eObj); //throw range_error(eObj); //throw bad_alloc(eObj); } return ia[ix]; } }; int main() { //int br[10]; br[12]; Array ar; for (int i = 1; i <= 10; ++i) ar[i - 1] = i; try { for (int i = 0; i <= 10; ++i) cout << ar[i] << " "; cout << endl; }catch (const out_of_range & e) { cout << endl; cout << e.what() << endl; return 0; } }
异常的使用:
异常的抛出和匹配原则
- 异常是通过抛出对象而引发的,该对象的类型决定了应该**哪个catch的处理代码。
- 被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。
- 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会生成
一个拷贝对象,这个拷贝的临时对象会在被catch以后销毁。(这里的处理类似于函数的传值返回) - catch(…)可以捕获任意类型的异常,问题是不知道异常错误是什么。
- 实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配,可以抛出的派生类对象,使用基类捕获,这个在实际中非常实用,我们后面会详细讲解这个。
在函数调用链中异常栈展开匹配原则
没有匹配的catch则退出当前函数栈,继续在调用函数的栈中进行查找匹配的catch。如果到达main函数的栈,依旧没有匹配的,则终止程序。
所以实际中我们最后都要加一个catch(…)捕获任意类型的异常,否则当有异常没捕获,程序就会直接终止。
异常重新抛出
有可能单个的catch不能完全处理一个异常,在进行一些校正处理以后,通过函数调用栈再交给更外层的调用链函数来处理,catch则可以通过重新抛出将异常传递给更上层的函数进行处理。
#include using namespace std; double Division(int a, int b) { // 当b == 0时抛出异常 if (b == 0) { throw "Division by zero condition!"; } return (double)a / (double)b; } void Func() { // 这里可以看到如果发生除0错误抛出异常,另外下面的array没有得到释放。 // 所以这里捕获异常后并不处理异常,异常还是交给外面处理,这里捕获了再 // 重新抛出去。 int* array = new int[10]; try { int len, time; cin >> len >> time; cout << Division(len, time) << endl; } catch (...) { cout << "delete []" << array << endl; delete[] array; throw; } // ... cout << "delete []" << array << endl; delete[] array; } int main() { try { Func(); }catch (const char* errmsg) { cout << errmsg << endl; } return 0; }
异常安全
(1).构造函数完成对象的构造和初始化,最好不要在构造函数中抛出异常,否则可能导致对象不完整或没有完全初始化
(2).析构函数主要完成资源的清理,最好不要在析构函数内抛出异常,否则可能导致资源泄漏(内存泄漏、句柄未关闭等)
(3).C++中异常经常会导致资源泄漏的问题,比如在new和delete中抛出了异常,导致内存泄漏,在lock和unlock之间抛出了异常导致死锁
C++经常使用RAII来解决以上问题。
异常规范
- 异常规格说明的目的是为了让函数使用者知道该函数可能抛出的异常有哪些。 可以在函数的后面接
throw(类型),列出这个函数可能抛掷的所有异常类型。 - 函数的后面接throw(),表示函数不抛异常。
- 若无异常接口声明,则此函数可以抛掷任何类型的异常。
自定义异常体系
实际使用中很多公司都会自定义自己的异常体系进行规范的异常管理,因为一个项目中如果大家随意抛异
常,那么外层的调用者基本就没办法玩了,所以实际中都会定义一套继承的规范体系。这样大家抛出的都是
继承的派生类对象,捕获一个基类就可以了
#include using namespace std; //栈满异常声明 template class pushOnFull { private: T _value; public: pushOnFull(T i) { _value = i; } T value()const { return _value; } void print() { cerr << "栈满," << value() << "未压入栈" << endl; } }; //栈空异常声明 template class popOnEmpty { public: void print() { cerr << "栈已空,无法出栈" << endl; } }; template class Stack { private: int top; //栈顶指针(下标) T* elements; //动态建立的数值 int maxSize; //栈最大允纳的元素个数 public: Stack(int default_size = 20) { maxSize = default_size; elements = new T[maxSize]; top = -1; } ~Stack() { delete[] elements; } //////////////////////////////////////////////////////////////// void Push(const T& data) throw(pushOnFull) { if (IsFull()) throw pushOnFull(data); elements[++top] = data; } T Pop() throw(popOnEmpty) { if (IsEmpty()) throw popOnEmpty(); T data = elements[top]; top--; return data; } ///////////////////////////////////////////////////////////////// bool IsEmpty() const { return top == -1; } bool IsFull() const { return top == maxSize - 1; } void PrintStack() { for (int i = top; i >= 0; --i) cout << elements[i] << " "; cout << endl; } }; int main() { Stack st(10); try { for (int i = 1; i <= 10; ++i) { st.Push(i); } } catch (pushOnFull& e) { e.print(); return 0; } st.PrintStack(); try { for (int i = 0; i <= 10; ++i) st.Pop(); } catch (popOnEmpty& e) { e.print(); return 0; } cout << "Main End." << endl; return 0; }