程序的错误大致分为三种:
- 语法错误,在编译和链接阶段就能发现;
- 逻辑错误,可以通过调试解决;
- 运行时错误,异常机制是为解决此类错误引入。
一个运行时错误的例子
int main(){ string str = "asdfa"; char ch1 = str[10]; // 越界 cout << ch1 << endl; // 程序崩溃 char ch2 = str.at(100); // 越界,抛出异常 cout << ch2 << endl; return 0; }
修改代码,使用异常机制捕获异常:
int main(){ string str = "asdfa"; try{ char ch1 = str[10]; // 越界 cout << ch1 << endl; // 程序崩溃 }catch(exception &e){ // 不会捕获,因为[]不会检查下标,不会抛出异常 cout << "[1]out of bound!" << endl; } try{ char ch2 = str.at(100); // 越界,抛出异常 cout << ch2 << endl; }catch(exception &e){ cout << "[2]out of bound!" << endl; } return 0; }
运行结果: [2]out of bound!
try-catch 语法:
try{
// 可能抛出异常的语句
} catch(exceptionType variable){
// 处理异常的语句
}
发生异常时必须将异常明确地抛出,try 才能检测到。
当异常点跳转到 catch 所在位置时,位于异常点之后,且在当前 try 语句块内的语句都不会再执行,即使 catch 成功处理了错误。
异常可以发生在当前 try 块中,也可以发生在 try 块中调用的某个函数,或者所调用函数调用的另外一个函数中。发生异常后,程序的执行流会沿着函数的调用链往前回退,直到遇到 try 才停止,调用链中剩下的未被执行代码都会跳过,没有执行机会。
C++ 语言本身以及标准库中的函数抛出的异常,都是 exception 类或其子类的异常。也就是说,抛出异常时,会创建一个 exception 类或其子类的对象。
可以将 catch 看做一个没有返回值的函数,当异常发生后 catch 会被调用,并且会接收实参(异常数据)。
但 catch 和真正的函数调用相比又有区别,多了一个「在运行阶段将实参和形参匹配」的过程。
如果不希望 catch 处理异常数据,也可以将 variable 省略掉,也即写作:
try{
// 可能抛出异常的语句
}catch(exceptionType){
// 处理异常的语句
}
try{ //可能抛出异常的语句 }catch (exception_type_1 e){ //处理异常的语句 }catch (exception_type_2 e){ //处理异常的语句 } //其他的 catch catch (exception_type_n e){ //处理异常的语句 }
异常发生时,程序会按照从上到下的顺序,将异常类型和 catch 所能接受的类型逐个匹配,一旦找到类型匹配的 catch 就停止,如果没有找到,会交给系统处理,终止程序。
class Base{}; class Derived : public Base{}; int main(){ try{ throw Derived(); // 抛出异常,实际上是创建一个 Derived 类型的匿名对象 cout << "此语句不会再执行" << endl; }catch(int){ cout<<"Exception type: int"<<endl; }catch(Base){ // 匹配成功,向上转型 cout<<"Exception type: Base"<<endl; }catch(Derived){ cout<<"Exception type: Derived"<<endl; } return 0; }
普通函数(非函数模板)实参和形参类型转换:
catch 异常匹配中的转换只包括:1)「向上转型」 2)「const 转换」 3)「数组或函数指针转换」。其他的都不能应用于 catch。
异常处理流程:
抛出(Throw)--> 检测(Try) --> 捕获(Catch)
通过 throw 关键字来显式抛出异常,语法为:
throw exceptionData;
exceptionData 是“异常数据”,可以是基本类型,也可以是聚合类型。
string str = "fasdf"; string *pstr = str; class Base{}; Base obj; throw 100; throw str; throw pstr; throw obj;
// 自定义异常类 class OutOfRange{ public: OutOfRange():m_flag(1){}; OutOfRange(int len, int index):m_len(len), m_index(index), m_flag(2){} void what() const; // 获取具体错误信息 private: int m_flag; // 错误类型标识 int m_len; // 当前数组长度 int m_index; // 当前使用数组下标 }; void OutOfRange::what() const { if(m_flag == 1){ cout<<"Error: empty array, no elements to pop."<<endl; }else if(m_flag == 2){ cout<<"Error: out of range( array length "<<m_len<<", access index "<<m_index<<" )"<<endl; }else{ cout<<"Unknown exception."<<endl; } } // 动态数组 class Array{ public: Array(); ~Array(){free(m_p);} int operator[](int i) const; //重载[] int push(int ele); // 末尾插入元素 int pop(); // 末尾删除元素 int length() const { return m_len; } // 获取数组长度 private: int m_len; // 数组长度 int m_capacity; // 当前内存还能容纳元素个数 int *m_p; // 内存指针 static const int m_stepSize = 50; // 每次扩容步长 }; Array::Array(){ m_p = (int*)malloc( sizeof(int) * m_stepSize ); m_capacity = m_stepSize; m_len = 0; } int Array::operator[](int index) const { if( index<0 || index>=m_len ) //判断是否越界 throw OutOfRange(m_len, index); //抛出异常(创建一个匿名对象) return *(m_p + index); } int Array::push(int ele){ if(m_len >= m_capacity){ //如果容量不足就扩容 m_capacity += m_stepSize; m_p = (int*)realloc( m_p, sizeof(int) * m_capacity ); //扩容 } *(m_p + m_len) = ele; m_len++; return m_len-1; } int Array::pop(){ if(m_len == 0) throw OutOfRange(); //抛出异常(创建一个匿名对象) m_len--; return *(m_p + m_len); } void printArray(Array &arr){ int len = arr.length(); if(len == 0){ //判断数组是否为空 cout<<"Empty array! No elements to print."<<endl; return; } for(int i=0; i<len; i++){ if(i == len-1) cout<<arr[i]<<endl; else cout<<arr[i]<<", "; } } int main(){ Array nums; for(int i=0; i<10; i++) // 向数组中添加十个元素 nums.push(i); printArray(nums); try{ //尝试访问第 20 个元素 cout<<nums[20]<<endl; }catch(OutOfRange &e){ e.what(); // Error: out of range( array length 10, access index 20 ) } try{ // 尝试弹出 20 个元素 for(int i=0; i<20; i++) nums.pop(); }catch(OutOfRange &e){ e.what(); // Error: empty array, no elements to pop. } printArray(nums); // Empty array! No elements to print. return 0; }
throw 关键字除了可以用在函数体中抛出异常,还可以用在函数头和函数体之间,指明当前函数能够抛出的异常类型,这称为异常规范(Exception specification),也称为异常指示符或异常列表。
double func(char param) throw (int);
如果函数会抛出多种类型的异常,那么可以用逗号隔开:
double func (char param) throw (int, char, exception);
如果函数不会抛出任何异常,那么( )中什么也不写,这样函数不能抛出任何异常,即使抛出 try 也检测不到:
double func (char param) throw ();
派生类虚函数的异常规范必须与基类虚函数的异常规范一样严格,或者更严格。
class Base{ public: virtual int fun1(int) throw(); virtual int fun2(int) throw(int); virtual string fun3() throw(int, string); }; class Derived:public Base{ public: int fun1(int) throw(int); //错!异常规范不如 throw() 严格 int fun2(int) throw(int); //对!有相同的异常规范 string fun3() throw(string); //对!异常规范比 throw(int,string) 更严格 }
异常规范在函数声明和函数定义中必须同时指明,并且要严格保持一致,不能更加严格或者更加宽松。
C++语言本身或者标准库抛出的异常都是 exception 的子类,称为标准异常(Standard Exception)。
try{ // ... }catch(exception &e){ // 使用引用是为了提高效率,不使用引用会执行一次对象拷贝 // ... }
class exception{ public: exception () throw(); //构造函数 exception (const exception&) throw(); //拷贝构造函数 exception& operator= (const exception&) throw(); //运算符重载 virtual ~exception() throw(); //虚析构函数 virtual const char* what() const throw(); //虚函数 };
下图展示了 exception 类的继承层次:
exception 类的直接派生类:
异常名称 | 说 明 |
---|---|
logic_error | 逻辑错误。 |
runtime_error | 运行时错误。 |
bad_alloc | 使用 new 或 new[ ] 分配内存失败时抛出的异常。 |
bad_typeid | 使用 typeid 操作一个 NULL 指针,而且该指针是带有虚函数的类,这时抛出 bad_typeid 异常。 |
bad_cast | 使用 dynamic_cast 转换失败时抛出的异常。 |
ios_base::failure | io 过程中出现的异常。 |
bad_exception | 这是个特殊的异常,如果函数的异常列表里声明了 bad_exception 异常,当函数内部抛出了异常列表中没有的异常时,如果调用的 unexpected() 函数中抛出了异常,不论什么类型,都会被替换为 bad_exception 类型。 |
logic_error 的派生类:
异常名称 | 说明 |
---|---|
length_error | 试图生成一个超出该类型最大长度的对象时抛出该异常,例如 vector 的 resize 操作。 |
domain_error | 参数的值域错误,主要用在数学函数中,例如使用一个负值调用只能操作非负数的函数。 |
out_of_range | 超出有效范围。 |
invalid_argument | 参数不合适。在标准库中,当利用 string 对象构造 bitset 时,而 string 中的字符不是 0 或 1 的时候,抛出该异常。 |
runtime_error 的派生类:
异常名称 | 说 明 |
---|---|
range_error | 计算结果超出了有意义的值域范围。 |
overflow_error | 算术计算上溢。 |
underflow_error | 算术计算下溢。 |