第十六章:其他工具与技术(一)

异常处理

异常处理用于处理程序在调用过程中的非正常行为:

  • 传统的处理方法:传返回值表示函数调用是否正常结束
  • C++中的处理方法:通过关键字try/catch/throw引入异常处理机制
    void f1(){
        throw 1;
    }
    
    void f2(){
        f1();
    }
    
    void f3(){
        f2();
    }
    
    int main(){
        try{
            f3();
        }
        catch(int){
            std::cout << "exception is occured!" << "\n";
        }
    
    }
    

异常触发时的系统行为由栈展开实现

  • 抛出异常后续的代码不会被执行
  • 局部对象会按照构造相反的顺序自动销毁
  • 系统尝试匹配相应的 catch 代码段
    1. 如果匹配则执行其中的逻辑,之后执行 catch 后续的代码
    2. 如果不匹配则继续进行栈展开,直到 跳出 “ ” main 函数,触发 terminate 结束运行

在异常处理的过程中有一个非常重要的概念-异常对象

  • 系统会使用抛出的异常拷贝初始化一个临时对象,称为异常对象
  • 异常对象会在栈展开过程中被保留,并最终传递给匹配的catch语句
    void f1(){
        throw 1;
    }
    
    void f2(){
        f1();
    }
    
    void f3(){
        f2();
    }
    
    int main(){
        try{
            f3();
        }
        catch(int e){
            std::cout << "exception is occured!" << "\n";
            std::cout << "error code is " << e << "\n";
        }
    
    }
    

接下来我们来重点关注下try/catch语句块的使用方法:

  • 一个try语句块后面可以跟一到多个catch语句块
  • 每个catch语句块用于匹配一种类型的异常对象
  • catch语句块的匹配按照从上到下进行
  • 使用catch(...)匹配任意异常
  • catch中调用throw继续抛出相同的异常

在一个异常未处理完成(异常未被捕获)时抛出新的异常会导致程序崩溃

  • 不要在析构函数或operator delete函数重载版本中抛出异常
  • 通常来说,catch所接收的异常类型为引用类型

之后我们来了解一下异常与构造和析构函数之间的关系:

struct Str{
    Str(){throw 100;}
};

class Cla
{
public:
    Cla(){
        try{
        }
        // 这里的捕获语句不起作用
        catch(int e){
            std::cout << "exception is cathched in Cla::Cla" << std::endl;
            
        }
    }

private:
    Str m_mem;
};

int main(){
    try{
        Cla obj;    
    }
    catch(int e){
        std::cout << "exception is cathched in main" << std::endl;
    }
    
}

  • 使用function-try-block保护初始化逻辑,具体参考这里
    struct Str{
        Str(){throw 100;}
    };
    
    class Cla
    {
    public:
        Cla()
        try: m_mem()
        {
    
        }
        // 可以正常捕获
        catch(int ){
            std::cout << "exception is cathched in Cla::Cla" << std::endl;
            // 注意,根据标准对于构造函数在这里编译器会隐式添加一句throw
        }
    
    private:
        Str m_mem;
    };
    
    int main(){
        try{
            Cla obj;
        }
        catch(int e){
            std::cout << "exception is cathched in main" << std::endl;
        }
    
    }
    

    函数 try 块不捕捉从按值传递的函数形参的复制/移动构造函数和析构函数中抛出的异常:这些异常是在调用方的语境抛出的

  • 在构造函数中抛出异常:已经构造的成员会被销毁,但类本身的析构函数不会被调用

在C++中我们可以描述一个函数是否会抛出异常:

  • 如果函数不会抛出异常,则应表明以为系统提供更多的优化空间
    1. C++ 98 的方式: throw() / throw(int, char)
    2. C++11 后的改进: noexcept / noexcept(false)
  • noexcept
    1. 限定符:接收 false / true 表示是否会抛出异常
    2. 操作符:接收一个表达式,根据表达式是否可能抛出异常返回 false/true
      void fun() noexcept(true)
      {
      
      }
      
      int main(){
          std::cout << noexcept(fun()) << std::endl;
      }
      
    3. 在声明了 noexcept 的函数中抛出异常会导致 terminate 被调用,程序终止(即使有捕获代码,也不会被捕获)
    4. 不作为函数重载依据,但函数指针、虚拟函数重写时要保持形式兼容

接下来我们来看一下标准库中提供的一些异常
在这里插入图片描述

void fun(){
    throw std::runtime_error("Invalid input");
}

int main(){
    try{
        fun();
    }
    catch(std::runtime_error& e){
        std::cout << e.what() << std::endl;
    }
}

最后,我们一定要正确对待异常处理:

  • 不要滥用:异常的执行成本非常高
  • 不要不用:对于真正的异常场景,异常处理是相对高效、简洁的处理方式
  • 编写异常安全的代码
    void fun(){
        int * ptr = new int[3];
        throw 123;
        delete[] ptr;		// 抛出异常后会导致内存泄漏
    }
    
    void fun(){
        std::unique_ptr<int> ptr = std::make_unique<int>(3);
        throw 123;			// 异常安全的代码,不会导致内存泄漏
    }
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值