异常处理
异常处理用于处理程序在调用过程中的非正常行为:
- 传统的处理方法:传返回值表示函数调用是否正常结束
- 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)
- C++ 98 的方式:
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<int> ptr = std::make_unique<int>(3); throw 123; // 异常安全的代码,不会导致内存泄漏 }