首页 > 代码库 > C++学习39 异常处理入门(try和catch)

C++学习39 异常处理入门(try和catch)

编译器能够保证代码的语法是正确的,但是对逻辑错误和运行时错误却无能为力,例如除数为 0、内存分配失败、数组越界等。这些错误如果放任不管,系统就会执行默认的操作,终止程序运行,也就是我们常说的程序崩溃(Crash)。

优秀的程序员能够从故障中恢复,或者提示用户发生了什么;不负责任的程序员放任不管,让程序崩溃。C++提供了异常机制,让我们能够捕获逻辑错误和运行时错误,并作出进一步的处理。

一个程序崩溃的例子:

#include <iostream>using namespace std;int main(){    string str = "c plus plus";    char ch1 = str[100];  //下标越界,ch1为垃圾值    cout<<ch1<<endl;    char ch2 = str.at(100);  //下标越界,抛出异常    cout<<ch2<<endl;    return 0;}

运行代码,在控制台输出 ch1 的值后程序崩溃。下面我们来分析一下。

at() 是 string 类的一个成员函数,它会根据下标来返回字符串的一个字符。与“[ ]”不同,at() 会检查下标是否越界,如果越界就抛出一个异常(错误);而“[ ]”不做检查,不管下标是多少都会照常访问。

上面的代码中,下标 100 显然超出了字符串 str 的长度。由于第 6 行代码不会检查下标越界,虽然有逻辑错误,但是程序能够正常运行。而第 8 行代码则不同,at() 函数检测到下标越界会抛出一个异常(也就是报错),这个异常本应由程序员处理,但是我们在代码中并没有处理,所以系统只能执行默认的操作,终止程序执行。

捕获异常

在C++中,我们可以捕获上面的异常,避免程序崩溃。捕获异常的语法为:

try{    // 可能抛出异常的语句}catch(异常类型){    // 处理异常的语句}

try 和 catch 都是C++中的关键字,后跟语句块,不能省略“{ }”。try 中包含可能会抛出异常的语句,一旦有异常抛出就会被捕获。从“try”的意思可以看出,它只是“尝试”捕获异常,如果没有异常抛出,那就什么也不捕获。catch 用来处理 try 捕获到的异常;如果 try 没有捕获到异常,就不会执行 catch 中的语句。

修改上面的代码,加入捕获异常的语句:

#include <iostream>using namespace std;int main(){    string str = "c plus plus";       try{        char ch1 = str[100];        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;}

可以看出,第一个 try 没有捕获到异常,输出了一个垃圾值。因为“[ ]”不会检查下标越界,不会抛出异常,所以即使有逻辑错误,try 什么也捕获不到。

第二个 try 捕获到了异常,并跳转到 catch,执行 catch 中的语句。需要说明的是,异常一旦抛出,会立即被捕获,而且不会再执行异常点后面的语句。本例中抛出异常的位置是第 15 行的 at() 函数,它后面的 cout 语句不会再被执行,也就看不到输出。

异常类型

所谓抛出异常,实际上是创建一份数据,这份数据包含了错误信息,程序员可以根据这些信息来判断到底出了什么问题,接下来该怎么处理。

异常既然是一份数据,那么就应该有数据类型。C++规定,异常类型可以是基本类型,也可以是标准库中类的类型,还可以是自定义类的类型。C++语言本身以及标准库中的函数抛出的异常,都是 exception 类或其子类的类型。也就是说,抛出异常时,会创建一个 exception 类或其子类的对象。

异常被捕获后,会和 catch 所能处理的类型对比,如果正好和 catch 类型匹配,或者是它的子类,那么就交给当前 catch 块处理。catch 后面的括号中给出的类型就是它所能处理的异常类型。上面例子中,catch 所能处理的异常类型是 exception,at() 函数抛出的类型是 out_of_range,out_of_range 是 exception 的子类,所以就交给这个 catch 块处理。

catch 后面的exception e可以分为两部分:exception 为异常类型,e 为 exception 类的对象。异常抛出时,系统会创建 out_of_range 对象,然后将该对象作为“实参”,像函数一样传递给“形参”e,这样,在 catch 块中就可以使用 e 了。

其实,一个 try 后面可以跟多个 catch,形式为:

try{    //可能抛出异常的语句}catch (exception_type_1){    //处理异常的语句}catch (exception_type_2){    //处理异常的语句}// ……catch (exception_type_n){    //处理异常的语句}

异常被捕获时,先和 exception_type_1 作比较,如果异常类型是 exception_type_1 或其子类,那么执行当前 catch 中的代码;如果不是,再和 exception_type_2 作比较……依此类推,直到 exception_type_n。如果最终也没有找到匹配的类型,就只能交给系统处理,终止程序。

多个 catch 块的例子:

#include <iostream>#include <exception>#include <stdexcept>using namespace std;//自定义错误类型class myType: public out_of_range{public:    myType(const string& what_arg): out_of_range(what_arg){}};int main(){    string str = "c plus plus";    try{        char ch1 = str.at(100);        cout<<ch1<<endl;    }catch(myType){        cout<<"Error: myType!"<<endl;    }catch(out_of_range){        cout<<"Error: out_of_range!"<<endl;    }catch(exception){        cout<<"Error: exception!"<<endl;    }    return 0;}

try 捕获到异常后和第一个 catch 对比,由于此时异常类型为 out_of_range,myType 是 out_of_range 的子类,所以匹配失败;继续向下匹配,发现第二个 catch 合适,匹配结束;第三个 catch 不会被执行。

需要注意的是:catch 后面的括号中仅仅给出了异常类型,而没有所谓的“形参”,这是合法的。如果在 catch 中不需要使用错误信息,就可以省略“形参”。

C++学习39 异常处理入门(try和catch)