目录
1,异常的概念及使用
1.1,异常的概念
1.2,异常的抛出和捕获
1.3,栈展开
1.4,异常的重新抛出
1.5,异常安全问题
1.6,异常规范
2,标准库中的异常
小结:
1,异常的概念及使用
1.1,异常的概念
异常处理是C++用于管理 程序运行时错误的核心机制,通过try,catch,throw等关键字实现。它允许将错误检测与处理逻辑分离,提升代码的可读性和健壮性。
1.2,异常的抛出和捕获
- 程序出现问题时,我们通过抛出(throw)一个对象来引发一个异常,该对象的类型和当前的调用链决定了应该由哪个catch来捕获处理该异常。
- 当throw执行时,throw后面的代码不再执行。程序的执行从throw位置跳到与之匹配的catch模块。catch可能是同一个函数中的一个局部的catch,也可能是调用链中另一个函数中的catch。一旦程序开始异常处理,沿着调用链的对象都将被销毁。
- 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个局部对象,所以会生成一个拷贝对象,拷贝对象再catch子句后销毁。
代码示例:
#include <iostream>
#include <stdexcept>double divide(int a, int b) {
if (b == 0) {
throw std::runtime_error("除数不能为零!"); //抛出异常对象
}
return a / b;
}int main() {
try {
std::cout << divide(10, 0) << std::endl;
} catch (const std::runtime_error& e) { //捕获异常,处理异常
std::cerr << "错误: " << e.what() << std::endl;
}
return 0;
}
// 输出:错误: 除数不能为零!
1.3,栈展开
- 抛出 异常后,程序暂停当前函数的执行,开始寻找与之匹配的catch子句,首先检查throw是否再try块内部,如果在则查找匹配的catch语句,如果由匹配的,则跳到catch地方进行处理。
- 如果当前函数没有try/catch子句,或者有但是类型不匹配,退出当前函数,继续在外层调用函数链中查找,上述查找catch的过程称为栈展开。
- 如果到达main函数,依旧没有找到匹配的catch子句,则程序会调用terminate函数终止程序。有时候达到main函数,还没有找到匹配的,而我们是不希望终止程序的,所以main函数最后一般都使用catch(...),它可以捕获任意类型的异常,但不知道异常错误是什么。
- 如果找到匹配的catch子句处理后,catch子句代码会继续执行。
1.4,异常的重新抛出
有时catch到一个异常对象 后,需要对错误进行分类,其中的某种异常错误 需要进行特殊处理,其他异常则重新抛出给外层调用链处理。捕获异常后重新抛出,直接throw,就可以把捕获的对象重新抛出。
常见应用场景:
1,中间层处理部分逻辑
在多层调用中,中间层捕获异常后记录日志或释放资源,但不处理异常的根本原因:
void middleLayer() {
try {
someRiskyOperation();
} catch (const std::exception& e) {
logError(e.what()); // 记录日志
throw; // 重新抛出,让上层处理
}
}
2,包装异常
捕获原始异常后,抛出一个新的异常类型(如自定义异常),同时保留原始异常信息:
try {
// ...
} catch (const DatabaseException& e) {
throw MyAppException("Database error: " + std::string(e.what()));
}
3,跨线程异常传递
使用 std::exception_ptr
保存异常,稍后在另一线程中重新抛出:
std::exception_ptr eptr;
try {
// 可能抛出异常的代码
} catch (...) {
eptr = std::current_exception(); // 捕获并保存异常
}// 在另一个线程中重新抛出
if (eptr) {
std::rethrow_exception(eptr);
}
1.5,异常安全问题
- 异常抛出后,后面的代码就不在执行了,前面申请了资源(内存),后面进行释放。但是中间可能会抛异常,导致资源没有释放,这里就由于异常引发了资源泄露,产生安全性的问题。这里需要使用智能指针的RAII方式解决。
- 还有析构函数中,如果抛出异常也要谨慎处理,比如要释放 10个资源,释放到第5个时抛出异常,导致后面的资源没有释放,导致资源泄露了。
1.6,异常规范
C++11中 ,函数的参数列表后加一个noexcept表示该函数不会抛异常。但如果一个声明了noexcept的函数抛出了异常,程序会调用terminate终止程序。
noexcept还可以作为运算符去检测一个表达式是否会抛异常。
-
noexcept(expression)
:根据式表达的结果来决定是否可能抛出异常。如果表达式为true
,则表示不会抛出异常;如果为false
,则表示可能抛出异常。
2,标准库中的异常
C++标准库定义了一套自己的异常体系,基类是exception,所以我们在捕获异常时,捕获exception即可,要获取异常信息,调用what()函数,what()是一个虚函数,派生类可以重写。
(1)C++标准库中常见的类型(继承自:std::exception)
-
std::logic_error
:程序逻辑错误(如无效参数)。 -
std::runtime_error
:运行时错误(如文件未找到)。 -
std::bad_alloc
:内存分配失败(new
失败时抛出)。
(2)自定义异常
通过继承std::exception
创建自定义异常类:
class MyException : public std::exception {
public:
MyException(const char* msg) : message(msg) {}
const char* what() const noexcept override {
return message.c_str();
}
private:
std::string message;
};// 使用
throw MyException("自定义异常!");
小结:
(1)异常处理的性能影响
抛出异常时:栈展开(Stack Unwinding)和类型匹配会带来一定开销,不适合高频场景。
(2) 最佳实践
1,优先使用RAII(如智能指针):确保资源的自动释放
2,避免在析构函数中抛出异常:可能会导致程序终止(若异常未被捕获)
3,明确异常规格:使用noexcept标记不会抛异常的函数。
4,捕获异常按引用:避免对象切片如:catch (const std::exception& e)。