一.异常
程序运行时常会碰到一些错误,例如除数为 0、年龄为负数、数组下标越界等,这些错误如果不能发现并加以处理,很可能会导致程序崩溃。
C++ 异常处理机制就可以让我们捕获并处理这些错误,然后我们可以让程序沿着一条不会出错的路径继续执行,或者不得不结束程序,但在结束前可以做一些必要的工作,例如将内存中的数据写入文件、关闭打开的文件、释放分配的内存等。
程序的错误大致可以分为三种,分别是语法错误、逻辑错误和运行时错误:
-
语法错误在编译和链接阶段就能发现,只有 100% 符合语法规则的代码才能生成可执行程序。语法错误是最容易发现、最容易定位、最容易排除的错误,程序员最不需要担心的就是这种错误。
-
逻辑错误是说我们编写的代码思路有问题,不能够达到最终的目标,这种错误可以通过调试来解决。
-
运行时错误是指程序在运行期间发生的错误,例如除数为 0、内存分配失败、数组越界、文件不存在等。C++异常(Exception)机制就是为解决运行时错误而引入的。
1.运行时异常
一个发生运行时错误的程序:
#include <iostream>
#include <string>
using namespace std;
int main(){
string str = "http://c.biancheng.net";
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() 函数检测到下标越界会抛出一个异常,这个异常可以由程序员处理,但是我们在代码中并没有处理,所以系统只能执行默认的操作,也即终止程序执行。
2.捕获异常
我们可以借助 C++ 异常机制来捕获上面的异常,避免程序崩溃。捕获异常的语法为:
try{
// 可能抛出异常的语句
}catch(exceptionType variable){
// 处理异常的语句
}
try和catch都是 C++ 中的关键字,后跟语句块,不能省略{ }。try 中包含可能会抛出异常的语句,一旦有异常抛出就会被后面的 catch 捕获。从 try 的意思可以看出,它只是“检测”语句块有没有异常,如果没有发生异常,它就“检测”不到。catch 是“抓住”的意思,用来捕获并处理 try 检测到的异常;如果 try 语句块没有检测到异常(没有异常抛出),那么就不会执行 catch 中的语句。
说得直接一点,检测到异常后程序的执行流会发生跳转,从异常点跳转到 catch 所在的位置,位于异常点之后的、并且在当前 try 块内的语句就都不会再执行了;即使 catch 语句成功地处理了错误,程序的执行流也不会再回退到异常点,所以这些语句永远都没有执行的机会了。
3.抛出异常
#include <iostream>
#include <string>
#include <exception>
using namespace std;
int main(){
string str = "http://c.biancheng.net";
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){ //exception类位于<exception>头文件中
cout<<"[2]out of bound!"<<endl;
}
return 0;
}
第一个 try 没有捕获到异常,输出了一个没有意义的字符(垃圾值)。因为[ ]不会检查下标越界,不会抛出异常,所以即使有错误,try 也检测不到。换句话说,发生异常时必须将异常明确地抛出,try 才能检测到;如果不抛出来,即使有异常 try 也检测不到。所谓抛出异常,就是明确地告诉程序发生了什么错误。
异常的处理流程:
抛出(Throw)--> 检测(Try) --> 捕获(Catch)
所谓抛出异常==》告诉计算机这里有一个异常。
异常可以发生在当前的 try 块中,也可以发生在 try 块所调用的某个函数中,或者是所调用的函数又调用了另外的一个函数,这个另外的函数中发生了异常。这些异常,都可以被 try 检测到。
二.异常类型
1.异常
exceptionType是异常类型,它指明了当前的 catch 可以处理什么类型的异常;variable是一个变量,用来接收异常信息。当程序抛出异常时,会创建一份数据,这份数据包含了错误信息,程序员可以根据这些信息来判断到底出了什么问题,接下来怎么处理。
异常既然是一份数据,那么就应该有数据类型。C++ 规定,异常类型可以是
int、char、float、bool等基本类型,也可以是指针、数组、字符串、结构体、类等聚合类型。C++ 语言本身以及标准库中的函数抛出的异常,都是exception类或其子类的异常。也就是说,抛出异常时,会创建一个 exception 类或其子类的对象。
但是 catch 和真正的函数调用又有区别:
- 真正的函数调用,形参和实参的类型必须要匹配,或者可以自动转换,否则在编译阶段就报错了。
- 而对于 catch,异常是在运行阶段产生的,它可以是任何类型,没法提前预测,所以不能在编译阶段判断类型是否正确,只能等到程序运行后,真的抛出异常了,再将异常类型和 catch 能处理的类型进行匹配,匹配成功的话就“调用”当前的 catch,否则就忽略当前的 catch。
另外需要注意的是,如果不希望 catch 处理异常数据,也可以将variable省略掉,也即写作:
try{
// 可能抛出异常的语句
}catch(exceptionType){
// 处理异常的语句
}
这样只会将异常类型和 catch 所能处理的类型进行匹配,不会传递异常数据了。
2.多级catch
前面的例子中,一个 try 对应一个 catch,这只是最简单的形式。其实,一个 try 后面可以跟多个 catch:
try{
//可能抛出异常的语句
}catch (exception_type_1 e){
//处理异常的语句
}catch (exception_type_2 e){
//处理异常的语句
}
//其他的catch
catch (exception_type_n e){
//处理异常的语句
}
当异常发生时,程序会按照从上到下的顺序,将异常类型和 catch 所能接收的类型逐个匹配。一旦找到类型匹配的 catch 就停止检索,并将异常交给当前的 catch 处理(其他的 catch 不会被执行)。如果最终也没有找到匹配的 catch,就只能交给系统处理,即终止程序的运行。
三.异常类型
1.throw关键字
在 C++ 中,我们使用 throw 关键字来显式地抛出异常,它的用法为:
throw exceptionData;
exceptionData 是“异常数据”的意思,它可以包含任意的信息,完全有程序员决定。exceptionData 可以是int、float、bool等基本类型,也可以是指针、数组、字符串、结构体、类等聚合类型,请看下面的例子:
char str[] = "https://blog.csdn.net/weixin_51496226?spm=1000.2115.3001.5343";
char *pstr = str;
class Base{};
Base obj;
throw 100; //int 类型
throw str; //数组类型
throw pstr; //指针类型
throw obj; //对象类型
2.异常规范
throw 关键字除了可以用在函数体中抛出异常,还可以用在函数头和函数体之间,指明当前函数能够抛出的异常类型,这称为异常规范(Exception specification),有些教程也称为异常指示符或异常列表。请看下面的例子:
double func (char param) throw (int);
这条语句声明了一个名为 func 的函数,它的返回值类型为 double,有一个 char 类型的参数,并且只能抛出 int 类型的异常。如果抛出其他类型的异常,try 将无法捕获,只能终止程序。
如果函数会抛出多种类型的异常,那么可以用逗号隔开:
double func (char param) throw (int, char, exception);
如果函数不会抛出任何异常,那么( )中什么也不写:
double func (char param) throw ();
如此,func() 函数就不能抛出任何类型的异常了,即使抛出了,try 也检测不到。
C++ 规定,派生类虚函数的异常规范必须与基类虚函数的异常规范一样严格,或者更严格。只有这样,当通过基类指针(或者引用)调用派生类虚函数时,才能保证不违背基类成员函数的异常规范。
C++ 规定,异常规范在函数声明和函数定义中必须同时指明,并且要严格保持一致,不能更加严格或者更加宽松。
异常规范是 C++98 新增的一项功能,但是后来的 C++11 已经将它抛弃了,不再建议使用。
四.exception类
C++语言本身或者标准库抛出的异常都是 exception 的子类,称为标准异常。你可以通过下面的语句来捕获所有的标准异常:
try{
//可能抛出异常的语句
}catch(exception &e){
//处理异常的语句
}
之所以使用引用,是为了提高效率。如果不使用引用,就要经历一次对象拷贝(要调用拷贝构造函数)的过程。
1.继承层次

先来看一下 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 | 算术计算上溢。 |
| 的时候,抛出该异常。 |
runtime_error 的派生类:
| 异常名称 | 说 明 |
|---|---|
| range_error | 计算结果超出了有意义的值域范围。 |
| overflow_error | 算术计算上溢。 |
| underflow_error | 算术计算下溢。 |
357

被折叠的 条评论
为什么被折叠?



