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

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

第十六章:其他工具与技术(一) 异常处理

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

  • 传统的处理方法:传返回值表示函数调用是否正常结束
  • 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 ptr = std::make_unique(3);
        throw 123;			// 异常安全的代码,不会导致内存泄漏
    }
    

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/4995836.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-11-14
下一篇 2022-11-14

发表评论

登录后才能评论

评论列表(0条)

保存