1. 错误
    1. 语法错误:编译系统
    2. 逻辑错误:测试
  2. 异常 Exception
    1. 运行环境造成:内存不足、文件操作失败等
    2. 异常处理:错误提示信息等

异常处理

  1. 特征:

    1. 可以预见
    2. 无法避免
  2. 作用:提高程序鲁棒性(Bobustness)

    1
    2
    3
    4
    5
    6
    7
    8
    void f(char *str) {//str可能是用户的一个输入
    ifstream file(str);
    if (file.fail()) {
    // 异常处理
    }
    int x;
    file >> x;
    }
  3. 问题:发现异常之处与处理异常之处不一致,怎么处理?函数中的异常要告知调用者

  4. 常见处理方式:

    1. 函数参数:
      • 返回值(特殊的,0或者1)
      • 引用参数(存放一些特定的信息)
    2. 逐层返回
  5. 缺陷:

    1. 程序结构不清楚 什么时候是异常的返回值、什么时候是正常的返回值
    2. 相同的异常,不同的地方,需要编写相同的处理了逻辑是不合理的
    3. 希望对异常进行集中的处理
  6. 传统异常处理方式不能处理构造函数出现的异常 需要创建资源,会出现异常,但是没有返回值


异常处理机制

  1. C++异常处理机制是,一种专门、清晰描述异常处理过程的机制
  2. try:监控
  3. throw:抛掷异常对象,不处理
  4. catch:捕获并处理

关键点:
(1)throw是将抛出的表达式的值拷贝到“异常对象”中,catch则是根据异常对象进行参数匹配并处理异常;
(2)throw可一次性跳出多层函数调用,直到最近一层的try语句,称为“栈展开”
(3)catch捕获时是将异常对象与catch参数的进行** 类型比较,而不是值比较**,所以只要类型相同,就可以进入catch中处理。(例如throw抛出一个int类型的值,catch(int &i)就可以对其进行处理;或者throw抛出一个类对象,catch(Base& b)也可成功匹配)
所谓 “try”,就是 “尝试着执行一下”,如果有异常,则通过throw向外抛出,随后在外部通过catch捕获并处理异常。

1
2
3
4
5
6
7
8
9
10
11
12
try{
//<语句序列>
//监控
}throw <表达式>
// 异常可以是基本类型,有表达式计算得到
// throw 一个异常对象,会调用拷贝构造函数。所以需要能看到该对象的完成调用
// 可以是基本类型,拷贝构造函数用来拷贝类
catch(<类型>[<变量>]){
//变量不重要可以省略
//<语句序列> 捕获并处理
//依次退出,不要抛出指向局部变量的指针,解决:直接抛出对象,自动进行拷贝
}
  • 不要抛出指出局部对象的引用或者指针,直接抛出一个对象

image.png

  • catch试图精确匹配,允许从非常量到常量的转换、从派生类到基类的转换、从数组和函数到指针的转换,但不能匹配到**int****double**

throw的处理过程:(栈展开)

throw语句一般位于try语句块内,当throw抛出一个异常时,程序暂停当前函数的执行过程,并寻找与try语句块关联的catch语句(类似 switch…case…),
如果这一步没找到匹配的catch,且这一层的try语句外部又包含着另一层try,则在外层try中继续寻找匹配的catch,如果找不到,则退出当前函数,在当前函数的外层函数中继续寻找匹配的try与catch。
上述过程被称为“栈展开”(stack unwinding)过程。
栈展开 过程沿着嵌套函数的调用链不断查找,直到找到匹配的catch 子句为止;
或者一直没有找到匹配的catch,则退出主函数终止查找过程(调用标准库函数terminate)。
如果找到了一个匹配的catch子句,则程序进入该子句并执行其中的代码,执行完成后回到到这个 try…catch… 的最后一个catch之后的位置继续向下执行。

异常对象

在编译器的管理空间中,会维护一种“异常对象”,专门用于抛出异常时使用。
当发生异常时,编译器会用throw 抛出的表达式的值 对 “异常对象” 进行拷贝初始化,当异常处理完毕后,编译器会将“异常对象”销毁。
所以,基于 异常对象 的这种处理机制,对抛出异常的处理有几点限制:
① 如果throw抛出的表达式是类类型,则此类必须要有可访问的拷贝构造函数和析构函数;(因为对 异常对象 进行拷贝初始化 以及 释放 异常对象的时候需要调用)
② throw抛出的异常对象不能是指向局部对象的指针(因为throw退出作用域后,局部对象随之被释放掉,抛出指针到外层后将无法访问所指向的局部对象)
③ throw抛出的表达式为此表达式的静态编译类型,如果抛出的是一个指向类对象的基类指针,则派生类部分将被截断,只有基类部分被抛出。

析构函数与异常:

当异常发生调用throw,后面的语句将不会被执行,退出作用域时,作用域的局部对象都将会被释放,对于类对象,退出作用域时将自动调用它的析构函数。
因此,如果析构函数中有抛出异常的流程,应该要在析构函数内部try捕获,并在析构函数内部得到处理。

初始化列表与异常

1
2
3
4
Bob::Bob(string i1) try : data(i1) {

} catch(const bad_alloc &e) { handle_out_of_memory(e); }

函数参数与异常

通过对函数调用进行处理

构造函数与异常

参数部分:通过对new进行处理

异常处理嵌套

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
f(){
try{
g();
}catch (int)
{ … }
catch (char *) //捕获char*的对象
{ … }
}
g(){
try{
h();
}catch (int) // 不能捕获char*类型的对象
{ … }
}
h(){
throw 1; //由g捕获并处理
throw "abcd"; //由f捕获并处理
} // 最终被终止程序所捕获
// 如所抛掷的异常对象在调用链上未被捕获,则由系统的abort处理

catch块排列顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class FileErrors { };
class NonExist:public FileErrors { } ;
class WrongFormat:public FileErrors { } ;
class DiskSeekError:public FileErrors { };

int f(){
try{
WrongFormat wf;
throw wf;
}catch(NonExists&){...}
catch(DiskSeekError&){...}
catch(FileErrors){...}//最后一个可以接住,派生类像基类转换是允许的
}
int f(){
try{
WrongFormat wf;
throw wf;
}catch(FileErrors){...}//这样子底下都捕获不到
catch(NonExists&){...}
catch(DiskSeekError&){...}
}
//Catch exceptions by reference
//尝试多继承,而不是拷贝,避免冗余
  • Catch exceptions by reference不使用引用,会发生对象的拷贝。使用引用,也可以直接对该对象处理,而不用对临时对象进行操作

例题??

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class MyExceptionBase {};
class MyExceptionDerived: public MyExceptionBase { };
void f(MyExceptionBase& e) {
throw e;//调用拷贝构造函数。 由静态编译的类型确定
}
int main() {
MyExceptionDerived e;
try {
f(e);
}catch(MyExceptionDerived& e) {
cout << "MyExceptionDerived" << endl;
}catch(MyExceptionBase& e) {
cout << "MyExceptionBase" << endl;
}
}
//输出:MyExceptionBase,为什么?调用了拷贝构造函数,拷贝构造的结果是MyExceptionBase类型的对象

image.png
catch(...):捕获所有异常

多出口引发的处理碎片

一个语句块,可以以throwreturn作为出口,因此会导致有多个出口

  • Java:finally:用来处理多出口,最后都会执行,用来释放资源。
  • c++中没有finally
    • 异常处理器
    • 析构函数raii:将资源初始化为对象,由析构函数进行资源清理。即使是多出口,也可以通过析构函数进行处理。