异常
异常即是在程序出现错误时抛出的问题
异常的三个关键字: try、catch、throw
- try{…保护代码…} : 尝试执行
- catch(异常对象){…执行语句…} : 捕捉异常后对应处理
- catch时优先就近匹配
- 添加catch(…) 可以捕获任意类型的异常(抛异常的最后底线)
但该异常是未知类型的 - 若异常抛出了整个程序都没有catch,则会终止程序
- throw 异常对象 : 抛出异常
可以抛出任意类型的异常- 抛出不同类型的对象,则会对应去寻找catch的处理代码
- 抛出异常时, 会生成一个异常对象的拷贝
因为抛出的异常对象可能是一个临时对象,所以会生成一个拷贝对象,这个拷贝的临时对象会在被catch以后销毁。(这里的处理类似于函数的传值返回) - 实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配,可以抛出的派生类对象,使用基类捕获
#include <iostream> #include <stdexcept> // 包含标准异常库 // 一个可能会抛出异常的函数 void divide(int dividend, int divisor) { if (divisor == 0) { // 抛出一个 std::runtime_error 异常 throw std::runtime_error("Division by zero is not allowed!"); } std::cout << dividend << " / " << divisor << " = " << dividend / divisor << std::endl; } int main() { try { // 尝试执行可能会抛出异常的代码 divide(10, 0); // 这将触发异常 } catch (const std::runtime_error& e) { // 捕获 std::runtime_error 类型的异常 // 处理异常(这里只是打印错误信息) std::cerr << "Caught an exception: " << e.what() << std::endl; } catch (...) { // 捕获所有其他类型的异常(通配符 catch 块) // 处理未知类型的异常(这里只是打印一个通用消息) std::cerr << "Caught an unknown exception." << std::endl; } // 继续执行 main 函数的其余部分(如果有的话) // ... return 0; }
异常基类与派生类
- 异常基类 → Exception(一般都自定义实现)
- 通过继承异常基类实现各种异常的派生类, 以区分不同类型的错误
只需要以引用形式去捕获父类异常即可构成多态调用子类的异常类方法- 基类的Exception存在虚函数
what()
, 派生类重写该虚函数即可 - 捕获异常的最后一定都要
catch(…)
, 以防万一
本质也是将what函数显示的字符串更改了,只是没有在派生类中重写而已,而是使用子类的消息赋值给了父类,在后面输出e.what()时也是输出对应的错误消息// 自定义异常类,继承自 std::exception class MyCustomException : public std::exception { private: std::string message; public: // 构造函数,用于设置异常消息 explicit MyCustomException(const std::string& msg) : message(msg) {} // 重写 what() 函数,返回自定义的异常描述 const char* what() const noexcept override { return message.c_str(); } };
// 自定义异常类,继承自 std::runtime_error class DivisionByZeroException : public std::runtime_error { public: explicit DivisionByZeroException(const std::string& what_arg) : std::runtime_error(what_arg) {} };
- 基类的Exception存在虚函数
重新抛异常
-
在一些场景下,异常被捕获了需要处理后重新抛出
- 如网络错误抛出异常后需要对异常捕获后自动重试n次, 若有成功则正常继续执行, n次失败后需要重新将异常抛出
- 如开辟了堆空间后, 出现了异常, 资源释放语句无法执行到, 则需要catch异常后, 释放了空间再重新抛出异常
#include <iostream> #include <stdexcept> #include <chrono> #include <thread> // 自定义的网络异常类 class NetworkException : public std::runtime_error { public: explicit NetworkException(const std::string& what_arg) : std::runtime_error(what_arg) {} }; // 模拟网络请求的函数,可能会抛出异常 bool performNetworkRequest() { // 这里模拟网络请求,有一定概率失败 static int failCount = 0; if (++failCount % 3 == 0) { // 假设有1/3的概率失败 throw NetworkException("Network request failed"); } std::cout << "Network request succeeded" << std::endl; return true; // 返回true表示请求成功 } // 尝试执行网络请求,并在失败时重试n次 bool retryNetworkRequest(int maxRetries) { for (int i = 0; i < maxRetries; ++i) { try { // 尝试执行网络请求 return performNetworkRequest(); } catch (const NetworkException& e) { // 捕获到网络异常,打印错误信息并等待一段时间后重试 std::cerr << "Caught exception: " << e.what() << ", retrying (" << i + 1 << "/" << maxRetries << ")" << std::endl; std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟等待一段时间 } } // 重试n次后仍然失败,重新抛出异常 throw NetworkException("Failed to perform network request after " + std::to_string(maxRetries) + " retries"); } int main() { try { // 尝试执行网络请求,并最多重试2次 if (retryNetworkRequest(2)) { std::cout << "Network request ultimately succeeded" << std::endl; } else { // 这里实际上不会到达,因为retryNetworkRequest函数在成功时会返回true std::cout << "This line should not be printed" << std::endl; } } catch (const NetworkException& e) { // 捕获到重新抛出的异常,处理它 std::cerr << "Final exception caught: " << e.what() << std::endl; } return 0; }
-
异常安全
- 构造函数和析构函数内最好不要抛异常
- new和delete抛异常导致资源无法释放, 需要通过智能指针解决
异常规范
-
C++98规范: 函数后面接
throw(异常类型1, 2, 3, …)
void foo() throw(std::runtime_error) { // ... }
-
C++11规范: 函数后面接关键字
noexcep
表示是否会抛异常void foo() noexcept(false) { // 或者 noexcept 如果函数不抛出异常 // ... } void foo() noexcept(true) { // do something that will not throw an exception }// 默认不写true/false即默认是true void foo() noexcept { // do something that will not throw an exception } // true即不会抛出异常,false即有可能抛出异常 // 但这个关键字只是说明,有时候仍然可能会抛出异常
noexcep关键字还可作为运算符使用,检查一个表达式是否可以抛出异常
bool result = noexcept(myFunction()); // 如果myFunction()不会抛出异常,result为true
异常优势
- 异常对象能包含的错误信息更清晰完善
- 当调用堆栈过深时能够准确定位到错误位置
- 很多的库都使用到了异常
- 使用异常使得各种函数都更好的处理错误
如当构造函数没有返回值则无法使用错误码方式处理
异常缺点
- 由于异常的跳跃性, 使得程序的执行流比较混乱
- 异常会有一些性能开销(但现代计算机可忽略不计)
- 异常很容易导致内存泄漏, 死锁等问题(需要用智能指针来处理资源)
- 标准库的异常体系定义的较差, 使得大家各自定义各自的异常体系
- 异常规范并非强制使用, 尽量使用异常的规范
总体而言, 异常的利大于弊, 日常开发中仍旧需要多使用异常,但在可以使用其他错误处理机制(如错误码)的情况下,避免使用异常。