C++ 基础系列——异常机制
一、异常处理入门
程序的错误大致分为三种:
- 语法错误,在编译和链接阶段就能发现;
- 逻辑错误,可以通过调试解决;
- 运行时错误,异常机制是为解决此类错误引入。
一个运行时错误的例子
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 才停止,调用链中剩下的未被执行代码都会跳过,没有执行机会。
二、异常类型及多级 catch 匹配
C++ 语言本身以及标准库中的函数抛出的异常,都是 exception 类或其子类的异常。也就是说,抛出异常时,会创建一个 exception 类或其子类的对象。
可以将 catch 看做一个没有返回值的函数,当异常发生后 catch 会被调用,并且会接收实参(异常数据)。
但 catch 和真正的函数调用相比又有区别,多了一个「在运行阶段将实参和形参匹配」的过程。
如果不希望 catch 处理异常数据,也可以将 variable 省略掉,也即写作:
try{
// 可能抛出异常的语句
}catch(exceptionType){
// 处理异常的语句
}
多级 catch
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 匹配过程中的类型转换
普通函数(非函数模板)实参和形参类型转换:
- 算数转换:例如 int 转换为 float,char 转换为 int,double 转换为 int 等。
- 向上转型:也就是派生类向基类的转换。
- const 转换:也即将非 const 类型转换为 const 类型,例如将 char * 转换为 const char *。
- 数组或函数指针转换:如果函数形参不是引用类型,那么数组名会转换为数组指针,函数名也会转换为函数指针。
- 用户自定的类型转换。
catch 异常匹配中的转换只包括:1)「向上转型」 2)「const 转换」 3)「数组或函数指针转换」。其他的都不能应用于 catch。
三、throw(抛出异常)
异常处理流程:
抛出(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 用作异常规范(C++11 后弃用)
throw 关键字除了可以用在函数体中抛出异常,还可以用在函数头和函数体之间,指明当前函数能够抛出的异常类型,这称为异常规范(Exception specification),也称为异常指示符或异常列表。
double func(char param) throw (int);
如果函数会抛出多种类型的异常,那么可以用逗号隔开:
double func (char param) throw (int, char, exception);
如果函数不会抛出任何异常,那么( )中什么也不写,这样函数不能抛出任何异常,即使抛出 try 也检测不到:
double func (char param) throw ();
1. 虚函数中的异常规范
派生类虚函数的异常规范必须与基类虚函数的异常规范一样严格,或者更严格。
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) 更严格
}
2. 异常规范与函数定义和函数声明
异常规范在函数声明和函数定义中必须同时指明,并且要严格保持一致,不能更加严格或者更加宽松。
四、C++异常的基类 exception
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 | 算术计算下溢。 |