C++ 异常
目录
引言
在程序开发中,错误处理是确保代码健壮性的关键环节。C++ 通过异常机制提供了一种强大的错误处理方式,它允许开发者将错误检测与处理逻辑分离,使代码更加清晰和模块化。相较于传统的错误码返回方式,异常机制能够携带更丰富的错误信息,并通过调用栈展开实现跨函数边界的错误传递。本文将详细介绍 C++ 异常的概念、使用方法、标准库异常体系以及异常安全等核心内容,帮助你掌握这一重要的编程技术。
1. 异常的概念及使用
1.1 异常的概念
- 异常处理机制允许程序中独立开发的部分能够在运行时就出现的问题进行通信并做出相应的处理,异常使得我们能够将问题的检测与解决问题的过程分开,程序的一部分负责检测问题的出现,然后解决问题的任务传递给程序的另一部分,检测环节无须知道问题的处理模块的所有细节。
- C语言主要通过错误码的形式处理错误,错误码本质就是对错误信息进行分类编号,拿到错误码以后还要去查询错误信息,比较麻烦;而异常机制则是抛出一个对象,这个对象可以包含更全面的错误信息。
1.2 异常的抛出和捕获
- 程序出现问题时,我们通过抛出(
throw
)一个对象来引发一个异常,该对象的类型以及当前的调用链决定了应该由哪个catch的处理代码来处理该异常。 - 被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。根据抛出对象的类型和内容,程序的抛出异常部分告知异常处理部分到底发生了什么错误。
- 当
throw
执行时,throw
后面的语句将不再被执行。程序的执行从throw
位置跳到与之匹配的catch
模块,catch
可能是同一函数中的一个局部的catch
,也可能是调用链中另一个函数中的catch
,控制权从throw
位置转移到了catch
位置。 - 这里还有两个重要的含义:1、沿着调用链的函数可能提早退出;2、一旦程序开始执行异常处理程序,沿着调用链创建的对象都将销毁。
- 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个局部对象,所以会生成一个拷贝对象,这个拷贝的对象会在
catch
子句后销毁。(这里的处理类似于函数的传值返回)
1.3 栈展开
- 异常处理流程:抛出异常后,程序暂停当前函数的执行,开始寻找与之匹配的
catch
子句。首先检查throw
本身是否在try
块内部,如果在则查找匹配的catch
语句,如果有匹配的,则跳到catch
的地方进行处理。 - 如果当前函数中没有
try/catch
子句,或者有try/catch
子句但是类型不匹配,则退出当前函数,继续在外层调用函数链中查找,上述查找的catch
过程被称为栈展开。 - 如果到达
main
函数依旧没有找到匹配的catch
子句,程序会调用标准库的terminate
函数终止程序。 - 如果找到匹配的
catch
子句并处理完成后,catch
子句代码会继续执行。
代码示例:
#include <iostream> #include <string> using namespace std; /** * @brief 执行两个整数的除法运算 * @param a 被除数 * @param b 除数 * @return double 返回a除以b的结果 * @throws string 当除数为0时抛出字符串异常 */ double Divide(int a, int b) { try { // 检查除数是否为0 if (b == 0) { // 创建异常信息字符串 string s("Divide by zero condition!"); throw s; // 抛出string类型异常 } else { // 执行除法运算,将结果转换为double类型返回 return ((double)a / (double)b); } } // 捕获int类型的异常(虽然代码中不会抛出这种类型) catch (int errid) { cout << "Error ID: " << errid << endl; } // 如果捕获了其他未处理的异常,会执行到这里 return 0; // 默认返回值 } /** * @brief 从用户输入获取数据并调用除法函数 * @throws const char* 可能抛出字符串字面量异常 */ void Func() { int len, time; // len是被除数,time是除数 // 从标准输入读取两个整数 cin >> len >> time; try { // 调用Divide函数并输出结果 cout << Divide(len, time) << endl; } // 捕获const char*类型的异常(虽然Divide函数不会抛出这种类型) catch (const char* errmsg) { cout << "Error: " << errmsg << endl; } // 输出当前函数名和行号信息(用于调试) cout << __FUNCTION__ << ":" << __LINE__ << "行执行" << endl; } /** * @brief 主函数,程序入口 */ int main() { // 无限循环,直到用户手动终止程序 while (1) { try { Func(); // 调用Func函数 } // 捕获string类型的异常(这是Divide函数实际抛出的异常类型) catch (const string& errmsg) { cout << "Exception caught: " << errmsg << endl; } } return 0; // 程序正常退出(实际上由于while(1)永远不会执行到这里) }
1.4 查找匹配的处理代码
异常匹配规则:一般情况下抛出对象和catch是类型完全匹配的,如果有多个类型匹配的,就选择离它位置更近的那个。
但也有一些例外:允许从非常量向常量的类型转换(权限缩小);允许数组转换成指向数组元素类型的指针,函数被转换成指向函数的指针;允许从派生类向基类类型的转换(这个点非常实用,实际中继承体系基本都是用这个方式设计的)。
如果到main函数时异常仍旧没有被匹配,程序就会终止。在不发生严重错误的情况下,我们通常不希望程序终止,因此一般会在main函数最后使用
catch(...)
,它可以捕获任意类型的异常,但无法获取具体的异常信息。代码示例:
#include <iostream> #include <string> #include <thread> // 用于线程相关操作 #include <ctime> // 用于时间种子 #include <chrono> // 用于时间间隔 #include <cstdlib> // 用于rand()函数 using namespace std; /* * 异常基类Exception * 作用:作为所有自定义异常的基类,提供基本的错误信息和错误ID */ class Exception { public: // 构造函数:初始化错误信息和错误ID Exception(const string& errmsg, int id) : _errmsg(errmsg), _id(id) {} // 虚函数what():返回错误信息(子类可以重写) virtual string what() const { return _errmsg; } // 获取错误ID int getid() const { return _id; } protected: string _errmsg; // 错误信息 int _id; // 错误ID }; /* * SQL异常类(继承自Exception) * 特点:增加了SQL语句信息 */ class SqlException : public Exception { public: // 构造函数:初始化基类信息并添加SQL语句 SqlException(const string& errmsg, int id, const string& sql) : Exception(errmsg, id), _sql(sql) {} // 重写what():返回包含SQL语句的完整错误信息 virtual string what() const { string str = "SqlException:"; str += _errmsg; str += "->"; str += _sql; return str; } private: const string _sql; // 引发异常的SQL语句 }; /* * 缓存异常类(继承自Exception) * 特点:简单的缓存相关错误 */ class CacheException : public Exception { public: // 构造函数:直接使用基类构造 CacheException(const string& errmsg, int id) : Exception(errmsg, id) {} // 重写what():添加缓存异常前缀 virtual string what() const { string str = "CacheException:"; str += _errmsg; return str; } }; /* * HTTP异常类(继承自Exception) * 特点:增加了HTTP请求类型信息 */ class HttpException : public Exception { public: // 构造函数:初始化基类信息并添加HTTP类型 HttpException(const string& errmsg, int id, const string& type) : Exception(errmsg, id), _type(type) {} // 重写what():返回包含HTTP类型的完整错误信息 virtual string what() const { string str = "HttpException:"; str += _type; str += ":"; str += _errmsg; return str; } private: const string _type; // HTTP请求类型(GET/POST等) }; /* * SQL管理函数 * 功能:模拟SQL操作,有1/7概率抛出SQL异常 */ void SQLMgr() { if (rand() % 7 == 0) { throw SqlException("权限不足", 100, "select * from name = '张三'"); } else { cout << "SQLMgr 调用成功" << endl; } } /* * 缓存管理函数 * 功能:模拟缓存操作,可能抛出两种缓存异常或调用SQL管理 */ void CacheMgr() { if (rand() % 5 == 0) { throw CacheException("权限不足", 100); } else if (rand() % 6 == 0) { throw CacheException("数据不存在", 101); } else { cout << "CacheMgr 调用成功" << endl; } SQLMgr(); // 调用SQL管理 } /* * HTTP服务函数 * 功能:模拟HTTP服务,可能抛出两种HTTP异常或调用缓存管理 */ void HttpServer() { if (rand() % 3 == 0) { throw HttpException("请求资源不存在", 100, "get"); } else if (rand() % 4 == 0) { throw HttpException("权限不足", 101, "post"); } else { cout << "HttpServer调用成功" << endl; } CacheMgr(); // 调用缓存管理 } int main() { srand(time(0)); // 设置随机数种子 // 无限循环模拟服务持续运行 while (1) { this_thread::sleep_for(chrono::seconds(1)); // 每秒执行一次 try { HttpServer(); // 调用HTTP服务 } // 捕获基类异常(多态特性:可以捕获所有派生类异常) catch (const Exception& e) { cout << e.what() << endl; // 打印错误信息 } // 捕获其他所有未处理的异常 catch (...) { cout << "Unkown Exception" << endl; } } return 0; }
1.5 异常重新抛出
异常处理与重新抛出:有时捕获到一个异常对象后,需要对错误进行分类,其中某种异常错误需要进行特殊处理,其他错误则重新抛出异常给外层调用链处理。捕获异常后需要重新抛出时,直接使用
throw;
即可将捕获的对象原样抛出。代码示例:
#include <iostream> #include <string> #include <ctime> #include <cstdlib> using namespace std; // 自定义异常类(假设已定义) class Exception { public: virtual const char* what() const = 0; virtual int getid() const = 0; }; // HTTP异常类(继承自Exception) class HttpException : public Exception { private: string msg; // 异常信息 int id; // 错误码 string method; // HTTP方法类型 public: HttpException(const string& msg, int id, const string& method) : msg(msg), id(id), method(method) {} const char* what() const override { return msg.c_str(); } int getid() const override { return id; } }; /** * 模拟发送消息的核心函数 * @param s 要发送的消息内容 * @throws HttpException 可能抛出两种HTTP异常: * 1. 网络不稳定异常(错误码102) * 2. 非好友关系异常(错误码103) */ void _SeedMsg(const string& s) { // 随机数模拟不同场景 if (rand() % 2 == 0) { // 50%概率模拟网络不稳定 throw HttpException("网络不稳定,发送失败", 102, "put"); } else if (rand() % 7 == 0) { // ~14%概率模拟非好友关系 throw HttpException("你已经不是对方的好友,发送失败", 103, "put"); } else { // 剩余情况模拟发送成功 cout << "[" << s << "] 发送成功" << endl; } } /** * 发送消息的封装函数,包含重试机制 * @param s 要发送的消息内容 * @throws Exception 可能抛出原始异常或重试失败后的异常 */ void SendMsg(const string& s) { // 最多尝试4次(1次初始尝试 + 3次重试) for (size_t i = 0; i < 4; i++) { try { _SeedMsg(s); // 尝试发送消息 break; // 发送成功则跳出循环 } catch (const Exception& e) { // 仅对网络不稳定错误(102)进行重试 if (e.getid() == 102) { if (i == 3) { // 如果已经重试3次仍然失败,抛出异常 throw; } cout << "开始第" << i + 1 << "次重试..." << endl; } else { // 其他错误直接重新抛出 throw; } } } } int main() { // 初始化随机数种子 srand(time(0)); string str; cout << "请输入要发送的消息(输入q退出):" << endl; // 循环读取用户输入 while (cin >> str) { if (str == "q") break; try { SendMsg(str); // 尝试发送消息 } catch (const Exception& e) { // 捕获已知异常并打印错误信息 cout << "错误: " << e.what() << endl << endl; } catch (...) { // 捕获未知异常 cout << "发生未知异常" << endl; } } return 0; }
1.6 异常安全问题
异常处理与资源释放:异常抛出后,后面的代码就不再执行,前面申请了资源(内存、锁等)但后续释放操作可能因异常跳过,导致资源泄漏,引发安全性问题。此时需捕获异常,先释放资源再重新抛出异常(后续智能指针章节介绍的RAII方式是更优解决方案)。
此外,析构函数中若抛出异常也需谨慎处理,例如析构函数需释放10个资源,若释放到第5个时抛出异常,则必须捕获处理以确保剩余资源释放,否则仍会导致泄漏。《Effective C++》第8条款专门强调:别让异常逃离析构函数。
代码示例:
#include <iostream> #include <exception> // 标准异常头文件 using namespace std; /** * @brief 执行两个整数的除法运算 * @param a 被除数 * @param b 除数 * @return double 返回a/b的浮点数结果 * @throws const char* 当除数为0时抛出字符串异常 */ double Divide(int a, int b) { // 检查除数是否为0 if (b == 0) { // 抛出字符串类型的异常,表示除零错误 throw "Division by zero condition!"; } // 将两个整数转换为double类型后做除法,确保结果是浮点数 return (double)a / (double)b; } /** * @brief 测试函数,演示异常处理和资源释放 * @note 此函数会动态分配内存,并在异常发生时确保内存被释放 */ void Func() { // 动态分配一个包含10个整数的数组 int* array = new int[10]; try { int len, time; // 从标准输入读取两个整数 cin >> len >> time; // 调用Divide函数并输出结果 cout << Divide(len, time) << endl; } catch (...) // 捕获所有类型的异常 { // 在异常发生时首先释放之前分配的内存 cout << "delete []" << array << endl; delete[] array; // 重新抛出捕获到的异常,由上层调用者处理 throw; // 注意:这里不指定异常类型,保留原始异常 } // 如果没有发生异常,正常释放内存 cout << "delete []" << array << endl; delete[] array; } /** * @brief 主函数,程序的入口点 * @return int 程序退出状态码 */ int main() { try { // 调用可能抛出异常的Func函数 Func(); } catch (const char* errmsg) // 捕获字符串类型的异常 { // 处理Divide函数抛出的字符串异常 cout << errmsg << endl; } catch (const exception& e) // 捕获标准异常 { // 处理标准库异常 cout << e.what() << endl; } catch (...) // 捕获所有其他类型的异常 { // 处理未知类型的异常 cout << "Unkown Exception" << endl; } return 0; // 程序正常退出 }
1.7 异常规范
对于用户和编译器而言,预先知道某个程序会不会抛出异常大有裨益,知道某个函数是否会抛出异常有助于简化调用函数的代码。
C++98中函数参数列表的后面接
throw()
,表示函数不抛异常,函数参数列表的后面接throw(类型1, 类型2...)
表示可能会抛出多种类型的异常,可能会抛出的类型用逗号分割。C++98的方式过于复杂,实践中并不好用,C++11中进行了简化,函数参数列表后面加
noexcept
表示不会抛出异常,啥都不加表示可能会抛出异常。编译器并不会在编译时检查
noexcept
,也就是说如果一个函数用noexcept
修饰了,但是同时又包含了throw
语句或者调用的函数可能会抛出异常,编译器还是会顺利编译通过的(有些编译器可能会报个警告)。但是一个声明了noexcept
的函数抛出了异常,程序会调用terminate
终止程序。noexcept(expression)
还可以作为一个运算符去检测一个表达式是否会抛出异常,可能会则返回false
,不会就返回true
。代码示例:
// C++98 // 这⾥表⽰这个函数只会抛出bad_alloc的异常 void* operator new (std::size_t size) throw (std::bad_alloc); // 这⾥表⽰这个函数不会抛出异常 void* operator delete (std::size_t size, void* ptr) throw(); // C++11 size_type size() const noexcept; iterator begin() noexcept; const_iterator begin() const noexcept; double Divide(int a, int b) noexcept { // 当b == 0时抛出异常 if (b == 0) { throw "Division by zero condition!"; } return (double)a / (double)b; } int main() { try { int len, time; cin >> len >> time; cout << Divide(len, time) << endl; } catch (const char* errmsg) { cout << errmsg << endl; } catch (...) { cout << "Unkown Exception" << endl; } int i = 0; cout << noexcept(Divide(1,2)) << endl; cout << noexcept(Divide(1,0)) << endl; cout << noexcept(++i) << endl; return 0; }
2. 标准库的异常
- 标准库的异常
- C++标准库定义了一套自己的异常继承体系,基类是
exception
。日常编程时,只需在主函数中捕获exception
即可获取异常信息。异常信息可通过调用what()
函数获得,该函数是虚函数,派生类可以重写以实现自定义异常信息。