异常处理概述

程序终止

1.执行正常结束而终止

2.程序执行中发生错误或特殊事件而终止(异常终止)
• 可预测的错误
• 用户自己定义的错误
• 难以预测的错误

异常处理(exception handling)机制的基本思想

• 采用结构化方法对程序的运行时错误进行显式管理

1.处理的是可预料的错误或特殊事件 2.将程序中的正常处理代码与异常处理代码显式区别开来,提高程序的可
读性

C++语言中的异常处理

抛出异常与异常处理分离的实现

Try-Catch 执行机制

• 若程序有异常,则通过 throw 关键字创建一个异常对象,并抛出

• 将可能抛出异常的程序段嵌在 try 块保护段之中,控制通过正常的顺序执行到达 try 块,然后执行 try 子块内的保护段

• 如果在保护段执行期间没有引发异常,那么跟在 try 子块后的 catch 子句就不执行。
程序继续执行紧跟在 try 块中最后一个 catch 子句后面的语句

• 如果有异常,catch 子句按其在 try 块后出现的顺序被检查。类型匹配的 catch 子句将捕获并处理异常(或继续抛出异常)

• 如果找不到匹配的处理代码,则自动调用标准库函数 terminate,其默认功能是调用 abort( )终止程序

异常定义与抛出

throw 关键字

throw 表达式; (1
throw; (2)用于当前异常再抛出

• 表达式可以是任意类型的对象

• throw 表达式; 操作的基本行为:

  1. 复制被抛出的对象为临时对象,作为异常对象
  2. 如果在当前函数 try 块中,则中止 try 执行,执行 catch 语句捕获
  3. 如有异常对象未在当前函数被捕获处理,则中止函数执行,异常传播给调用者处理

例:

#include <cstdio>
#include <cerrno>
int main () {
FILE * pf;
try {
pf = fopen ("unexist.txt", "rb");
if (pf == NULL) throw errno;
// write something;
fclose (pf);
}
catch (int errnum) {
if (errnum!=2) fclose (pf);
else perror("unexist.txt");
}
return 0;
}

异常基类 exception

class exception {
public:
exception () noexcept;
exception (const exception&) noexcept;
exception& operator= (const exception&) noexcept;
virtual ~exception();
virtual const char* what() const noexcept;
}
// bad_alloc example
#include <iostream> // std::cout
#include <new> // std::bad_alloc
int main () {
try
{
int* myarray= new int[10000];
}
catch (std::bad_alloc& ba)
{
std::cerr << "bad_alloc caught: " << ba.what() << '\n';
}
return 0;
}
// out_of_range example
#include <iostream> // std::cerr
#include <stdexcept> // std::out_of_range
#include <vector> // std::vector
int main (void) {
std::vector<int> myvector(10);
try {
myvector.at(20)=100; // vector::at throws an out-of-range
}
catch (const std::out_of_range& oor) {
std::cerr << "Out of Range error: " << oor.what() << '\n';
}
return 0;
}

异常的捕获及处理

异常处理语法结构

try {
program-statements //程序的正常处理逻辑
throw exception-object
program-statements //程序的正常处理逻辑

}
catch (exception-declaration) {
handler-statements //异常处理代码
}
catch (exception-declaration) {
handler-statements //异常处理代码
}

catch (…) {
handler-statements //异常处理代码
}

异常捕获的匹配规则

1.try 期间发生异常; 2.按 catch 顺序与异常定义匹配
异常对象类型是定义类型或是定义类型的子类
• 匹配成功
• 异常接收变量是值,复制异常到接收变量
• 异常接收变量是引用或指针,创建引用和指针
• 清除当前异常,执行该异常处理块 3.直到匹配成功或结束
要点:
子类必须优先基类 catch,(…)必须是最后 catch
• 用基类引用类型捕捉异常,且虚函数才能产生多态

异常传播

• 从异常抛出到控制转移给合适的异常处理语句的过程就叫做异常传播。

• 异常传播过程(一句话:当前函数未处理异常,则交给调用函数):

1.由里层向外层的执行每一个包围抛出点(异常被抛出的最初位置)的 try 语句。 2.如果当前的成员函数调用中没能定位异常处理,则调用终止。并且在该成员函数调用点将该异常抛给调用者,重复执行上一步。 3.如果该异常终止了当前线程或进程的所有成员函数调用,则说明该线程或进程中不存在对异常的处理,它将自行终止。

限定函数异常

void f3(int x) throw(double,float) {
switch (x) {
case 1: throw 3.4; // 抛出double型异常
case 2: throw 2.5f; // 抛出float型异常
case 3: throw 1; // 抛出int型异常
case 5: throw exception(); //抛出标准异常
}
cout << "End of f3" << endl;
}

noexcept 关键字

void f2(int x) noexcept {
try {
f3(x);
}
catch (int) { //int型异常的处理代码
cout << "An int exception occurred!--from f2" << endl;
}
catch (float) { //float型异常的处理代码
cout << "A float exception occurred!--from f2" << endl;
}
cout << "End of f2" << endl;
}

仅需说明是否允许抛出异常,有异常未处理,则立即调用 std::terminate

异常重新抛出

catch (MyException &e) {
e.setMessage("new message");
throw;
}
//修改被捕获的异常对象;导致传递到上层函数的MyException类型异常对象的数据成员message中所包含的字符串将变成“new message”
catch (MyException e) {
e.setMessage("new message");
throw;
}
//修改被捕获异常对象的局部副本;传递到上层函数的MyException类型异常对象并不发生改变。

题目

1

#include <iostream>
#include <stdexcept>
using namespace std;

class DerivedException : public runtime_error {
public:
DerivedException() : runtime_error("派生类异常") {}
const char* what() const noexcept override {
return "我是派生类!";
}
};

int main() {
try {
throw DerivedException();
} catch (runtime_error e) {
cout << e.what() << endl;
}
return 0;
}

输出什么:->”派生类异常”

对象切片(Object Slicing): 1.当派生类对象被赋值给基类对象时(这里是按值捕获),会发生对象切片。即,只有基类部分被复制,派生类的部分被“切掉”。 2.因此,e 是一个 runtime_error 对象,而不是 DerivedException 对象。
3.e.what() 调用的是 runtime_error 的 what() 方法,而不是 DerivedException 的 what() 方法。

2

std::abort() 和 std::terminate() 都是用于异常终止程序的函数
简单粗暴 → abort()。
与异常系统集成 → terminate()

3

1.构造函数中的异常:

如果构造函数抛出异常,对象被视为未完全构造。

C++ 会自动调用已构造的成员变量和基类的析构函数(栈展开的一部分),但不会调用当前类的析构函数(因为对象未完全构造)。

2.析构函数的调用规则:

只有完全构造的对象才会在离开作用域时调用析构函数。

对于部分构造的对象,已成功构造的子对象(成员变量、基类)会被析构,但当前对象的析构函数不会执行。

3.动态内存的释放:

如果构造函数中动态分配了内存(如 new),但随后抛出异常,已分配的内存不会自动释放(除非使用智能指针或手动释放)。

需要手动管理或在异常处理中释放资源。