异常处理用于处理程序在调用过程中的非正常行为:
- 传统的处理方法:传返回值表示函数调用是否正常结束
- 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 代码段
- 如果匹配则执行其中的逻辑,之后执行 catch 后续的代码
- 如果不匹配则继续进行栈展开,直到 跳出 “ ” 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++中我们可以描述一个函数是否会抛出异常:
- 如果函数不会抛出异常,则应表明以为系统提供更多的优化空间
- C++ 98 的方式: throw() / throw(int, char)
- C++11 后的改进: noexcept / noexcept(false)
- noexcept
- 限定符:接收 false / true 表示是否会抛出异常
- *** 作符:接收一个表达式,根据表达式是否可能抛出异常返回 false/true
void fun() noexcept(true) { } int main(){ std::cout << noexcept(fun()) << std::endl; }
- 在声明了 noexcept 的函数中抛出异常会导致 terminate 被调用,程序终止(即使有捕获代码,也不会被捕获)
- 不作为函数重载依据,但函数指针、虚拟函数重写时要保持形式兼容
接下来我们来看一下标准库中提供的一些异常
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; // 异常安全的代码,不会导致内存泄漏 }
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)