菜鸟笔记
提升您的技术认知

谈谈异常处理

0. 为什么需要异常?

  • 异常机制的处理原理
    程序会出现错误,尤其是不易察觉的错误。需要了解并解决这些错误。通常,程序出现错误,都会强制退出,很难排除错误原因。

1. C语言如何表示错误

  • 函数返回值
    • 通常,成功返回0,返回值-1。
    • 返回值为指针类型,成功返回非NULL,失败返回值NULL。
      例如:malloc();例外shmat()失败返回值为MAP_INVALD(-1)
    • 其它另类的返回值
      fread()/fwrite()返回读写字符长度size_t,超出长度表示失败。
  • 全局变量errno

2. 异常处理特点

异常提供一个错误专用通道

优点:

  • 不干扰正常的返回值。
  • 必须处理异常。

3. 语法

异常分为两个部分:

  1. 抛出异常
  2. 捕获并处理异常。
  • 抛出异常
throw 表达式;
  • 捕获并处理异常
try {
    
        // 保护代码 包含可能抛出异常的语句;  
} catch (类型名 [形参名]) {
    
        // catch块 处理异常
}  

特点

  1. 只要抛出异常,异常后的代码不再执行
  2. 异常的所抛出与经过的都会销毁

常机制try-throw-catch的目标是问题检测问题解决分离

复杂一点地写法

try {
    
        // 保护代码 包含可能抛出异常的语句;  
} catch (类型名1 [形参名]) {
    
        // catch块 处理异常
} catch (类型名2 [形参名]) {
    
        // catch块 处理异常
} catch (类型名3 [形参名]) {
    
        // catch块 处理异常
} catch(...){
  
        // catch块 处理异常
}

注意

  • 异常捕获具有类型匹配,只有相同的或者父类类型才能匹配到。
  • 如果多个catch都能接受相同异常,只有最前面的一个可以接收到。
    catch(...)只能放在所有异常捕获的最后

4. 异常的接口声明/异常规范

返回值类型 函数() throw(异常列表);
  • 指定函数可以抛出何种异常,如果没有throw(异常列表)默认可以抛出所有异常。
  • 指定函数不抛出函数,异常列表为空throw()

当异常抛出后新对象如何释放?

异常处理机制保证:

  • 异常抛出的新对象并非创建在函数栈上,而是创建在专用的异常栈上,因此它才可以跨接多个函数传递到上层
  • 否则在栈清空的过程中就会被销毁。所有从trythrow语句之间构造起来的对象析构函数将被自动调用
  • 但如果一直上溯到main函数后还没有找到匹配的catch块,那么系统调用terminate()终止整个程序,这种情况下不能保证所有局部对象会被正确地销毁。

举例

  1. 捕获异常
#include <iostream>
using namespace std;

void test(){
  
    cout << "before throw." << endl;
    throw -1;
    cout << "after throw." << endl;
}
int main(){
  
    try{
  
        test();
    }catch(int a){
  
        cout << "exception:" << a << endl;
    }
}
  1. 异常与局部对象析构
#include <iostream>
using namespace std;

class Test{
  
public:
    Test(){
  
        cout << "Test Init" <<endl;
    }
    ~Test(){
  
        cout << "Test Destroy" <<endl;
    }
};

int main(){
  
    try{
  
        Test t;
        cout << "before throw." << endl;
        throw -1;
        cout << "after throw." << endl;
    }catch(int a){
  
        cout << "exception:" << a << endl;
    }
}

注意事项

  1. 如果抛出的异常一直没有函数捕获(catch),则会一直上传到c++运行系统那里,导致整个程序的终止
  2. 一般在异常抛出后资源可以正常被释放,但注意如果在类的构造函数中抛出异常,系统是不会调用它的析构函数的。
    处理方法是:如果在构造函数中要抛出异常,则在抛出前要记得删除申请的资源
  3. 异常处理仅仅通过类型而不是通过值来(switch-case)匹配的,所以catch块的参数可以没有参数名称,只需要参数类型
  4. 函数原型中的异常说明要与实现中的异常说明一致,否则容易引起异常冲突。
  5. 应该在throw语句后写上异常对象时throw先通过Copy构造函数构造一个新对象,再把该新对象传递catch.
  6. catch块的参数推荐采用地址传递而不是值传递,不仅可以提高效率,还可以利用对象的多态性。另外,派生类的异常扑获要放到父类异常扑获的前面,否则,派生类的异常无法被扑获。
  7. 编写异常说明时,要确保派生类成员函数的异常说明基类成员函数的异常说明一致,即派生类改写的虚函数的异常说明至少要和对应的基类虚函数的异常说明相同,甚至更加严格,更特殊

5. 标准异常类

exception派生

异常类 作用
logic_error 逻辑错误,在程序运行前可以检测出来
runtime_error 运行时错误,仅在程序运行中检测到

逻辑异常logic_error派生

异常类 作用
domain_error 违反了前置条件
invalid_argument 指出函数的一个无效参数
length_error 指出有一个超过类型size_t的最大可表现值长度的对象的企图
out_of_range 参数越界
bad_cast 在运行时类型识别中有一个无效的dynamic_cast表达式
bad_typeid 报告在表达试typeid(*p)中有一个空指针p

运行时runtime_error派生

异常类 作用
range_error 违反后置条件
bad_alloc 存储分配错误

尝试捕获逻辑异常和运行时异常

自定义异常类!!

  • 编码流程
    1. 继承异常类exception
    2. 实现接口what()
  • 代码结构
class 异常类:public exception {
      
public:    
   const char* what()const throw() {
  
        return 信息字符串;    
   }        
}; 

6. 构造函数、析构函数的异常处理

  • 构造函数可以抛出异常,此时不会调用析构函数,所以如果抛出异常前,申请了资源,需要自己释放
  • C++标准指明析构函数不能也不应该抛出异常
  • C++标准规定,构造函数失败,析构函数不会执行。就是说在构造函数抛出异常前分配的资源将无法释放

如果析构函数抛出异常

  1. 异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。
  2. 通常异常发生时,c++的机制会调用已经构造对象析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃的问题

7. 是否使用异常机制

为什么很多经典书籍鼓励使用异常,但是实际开发中很多C++编码规范却禁用异常?
C++异常机制在语法上是更加优雅的处理错误,但是实际上编译出来的程序会有一些性能损失,另外错误地使用异常处理代码会变得更加复杂。

编译器选项
g++特殊编译选项g++ -fno-exceptions

在不同的编码规范中,对是否使用异常存在争议。