一. 异常的核心概念与基本语法\
- 异常处理机制允许程序中独立开发的部分能够在运行时就出现的问题进行通信并做出相应的处理,异常使得我们能够将问题的检测与解决问题的过程分开,程序的一部分负责检测问题的出现,然后解决问题的任务传递给程序的另一部分,检测环节无需知道问题的处理模块的所有细节
- C语言主要通过错误码的形式处理错误,错误码的本质就是对错误信息进行分类编号,拿到错误码以后还要去查询错误信息,比较麻烦。而异常是抛出一个对象,这个对象可以函数更全面的拿到各种信息。
1.1 异常的核心思想
- 抛出(throw):程序遇到错误时,通过
throw抛出一个异常对象(可是任意类型,推荐自定义异常类); - 捕获(catch):通过
catch语句捕获指定类型的异常,执行对应的处理逻辑; - try 块:
try包裹可能抛出异常的代码,后续紧跟一个或多个catch块,用于匹配异常
分析:
- 程序出现问题时,我们通过抛出(throw)一个对象来引发一个异常,该对象的类型以及当前调用链决定了应该由那个catch的处理代码来处理该异常。
- 被选中的处理代码是调用链中与该对象类型匹配且抛出异常位置最近的那一个。根据抛出对象的类型和内容,程序的抛出异常部分告知异常处理部分到底发生了什么错误。
- 当 throw 执行时,throw 后面的语句将不再被执行。程序的执行从throw位置跳到与之匹配的 catch 模块,catch可能是同一函数中的一个局部的 catch,也可能是调用链中另一个函数的catch,控制权从throw位置转移到了catch位置。这里还有两个重要的含义:1. 沿着调用链的函数可能提早结束退出。2. 一旦程序开始执行异常处理,沿着调用链创建的对象都将销毁。
- 抛出异常对象后,会生成一个异常对象的1拷贝,因为抛出的异常对象可能是一个局部对象,所以会生成一个拷贝对象,这个拷贝的对象会在catch子句后销毁。( 这里的处理类似于函数的传值返回)
1.2 基础语法格式和最简示例
基本语法格式:
/*----------------------------------------------------------------- try { // 可能抛出异常的代码 可能出错的函数(); } catch (异常类型1& e) { // 处理类型1异常 } catch (异常类型2& e) { // 处理类型2异常 } catch (...) { // 捕获任意类型异常(兜底处理) } -----------------------------------------------------------------*/最简示例(除零异常):
#include<exception> double Divide(int a, int b) { // 当 b == 0 时抛出异常 if (b == 0) { //string s("Divide by zero condition!"); //throw s; throw exception("Divide by zero condition!"); } else { return ((double)a / (double)b); } } void Func() { try { int len, time; cin >> len >> time; cout << Divide(len, time) << endl; } catch (const exception& e) { cout << e.what() << endl; } cout << "Func():" << __LINE__<< endl; } int main() { while (1) { try { Func(); } // 异常会先匹配最适配的 catch (const string& s) { cout << s << endl; } catch (const exception& e) { cout << e.what() << endl; } catch (...) // 任意类型的对象 { cout << "未知异常" << endl; } cout << "Func():" << __LINE__ << endl; } return 0; }二. 异常的核心机制:栈展开与匹配规则
2.1 栈展开
抛出异常后,程序会暂停当前函数执行,沿调用链向上查找匹配的catch块,这个过程称为 “栈展开”:
- 检查当前函数的
try/catch块,若找到匹配的catch,则执行处理逻辑; - 若未找到,销毁当前函数的局部对象,退出当前函数,继续向上查找;
- 重复步骤 1-2,直到找到匹配的
catch; - 若到达
main函数仍未找到,调用terminate函数终止程序。
补充:
- 抛出异常后,程序暂停当前函数的执行,开始寻找与之匹配的
catch子句,首先检查throw本身是否在try块内部,如果在则查找匹配的catch语句,如果有匹配的,则跳到catch的地方进行处理。 - 如果当前函数中没有
try/catch子句,或者有try/catch子句但是类型不匹配,则退出当前函数,继续在外层调用函数链中查找,上述查找的catch过程被称为栈展开。 - 如果到达
main函数,依旧没有找到匹配的catch子句,程序会调用标准库的terminate函数终止程序。 - 如果找到匹配的
catch子句处理后,catch子句代码会继续执行。
栈展开示例:
void Func1() { throw "Func1抛出异常"; // 抛出异常 } void Func2() { Func1(); // 调用Func1,不处理异常 } void Func3() { Func2(); // 调用Func2,不处理异常 } int main() { try { Func3(); // 调用Func3 } catch (const char* errmsg) { // 捕获Func1抛出的异常(栈展开:Func1→Func2→Func3→main) cout << "捕获异常:" << errmsg << endl; } return 0; }2.2 异常捕获的匹配规则
捕获异常时,遵循 “精确匹配优先、兼容转换次之” 的原则:
- 优先匹配与抛出对象类型完全一致的
catch; - 支持有限的类型转换:
- 非常量→常量(
int→const int); - 数组→数组元素指针(
int[5]→int*); - 派生类→基类(最实用,用于自定义异常体系);
- 非常量→常量(
- 若有多个
catch块,按顺序匹配,匹配成功后不再检查后续catch; catch (...)可捕获任意类型异常,通常作为兜底,避免程序终止。
补充:
- 一般情况下抛出对象和
catch是类型完全匹配的,如果有多个类型匹配的,就选择离它位置更近的那个。 - 但是也有一些例外,允许从非常量向常量的类型转换,也就是权限缩小;允许数组转换成指向数组元素类型的指针,函数被转换成指向函数的指针;允许从派生类向基类类型的转换,这个点非常实用,实际中继承体系基本都是用这个方式设计的。
- 如果到
main函数,异常仍旧没有被匹配就会终止程序,不是发生严重错误的情况下,我们是不期望程序终止的,所以一般main函数中最后都会使用catch(...),它可以捕获任意类型的异常,但是无法知道异常错误是什么。
三. 自定义异常体系:大型项目的最佳实践
在大型项目中,直接抛出基本类型(如字符串、整数)的异常难以区分错误类型,推荐自定义异常类体系(基于继承),统一异常接口,便于管理和扩展。
代码实现:
#include<thread> // 一般大型项目程序才会使用异常,下面我们模拟设计一个服务的几个模块 // 每个模块的继承都是Expection的派生类,每个模块可以添加自己的数据 // 最后捕获的时候,我们捕获基类就可以,通过多态可以打印不同信息 class Exception { public: Exception(const string& errmsg,int id) :_errmsg(errmsg) ,_id(id) { } virtual string what() const { return _errmsg; } int getid()const { return _id; } protected: string _errmsg; int _id; }; class SqlException :public Exception { public: SqlException(const string& errmsg,int id,const string& sql) :Exception(errmsg,id) ,_sql(sql) { } virtual string what() const { string str = "SqlException:"; str += _errmsg; str += "->"; str += _sql; return str; } private: const string _sql; }; class CacheException : public Exception { public: CacheException(const string& errmsg, int id) :Exception(errmsg, id) { } virtual string what() const { string str = "CacheException:"; str += _errmsg; return str; } }; class HttpException : public Exception { public: HttpException(const string& errmsg, int id, const string& type) :Exception(errmsg, id) , _type(type) { } virtual string what() const { string str = "HttpException:"; str += _type; str += ":"; str += _errmsg; return str; } private: const string _type; }; void SQLMgr() { if (rand() % 7 == 0) { throw SqlException("权限不足", 100, "select * from name = '张三'"); } else { cout << "SQLMgr 调用成功" << endl; } } void CacheMgr() { if (rand() % 5 == 0) { throw CacheException("权限不足", 100); } else if (rand() % 6 == 0) { throw CacheException("数据不存在", 101); } else { cout << "CacheMgr 调用成功" << endl; } SQLMgr(); } 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(); } catch (const Exception& e) // 这里捕获基类,基类对象和派生类对象都可以被捕获 { // 多态调用 cout << e.what() << endl; } catch (...) { cout << "Unkown Exception" << endl; } } return 0; }部分输出演示:
四. 异常的高级用法
4.1 异常重新抛出
有时捕获异常后,无法完全处理(如仅记录日志),或需要根据错误类型分流处理,可通过throw;重新抛出异常,让外层调用链继续处理。
示例:网络请求重试
// 下面程序模拟展示了聊天时发送消息,发送失败补货异常,但是可能在 // 电梯地下室等场景手机信号不好,则需要多次尝试 // 如果多次尝试都发送不出去,则就需要捕获异常再重新抛出, // 其次如果不是网络差导致的错误,捕获后也要重新抛出。 void _SendMsg(const string& s) { if (rand() % 2 == 0) { throw HttpException("网络不稳定,发送失败",102, "put"); } else if (rand() % 7 == 0) { throw HttpException("你已经不是对方的好友,发送失败", 102, "put"); } else { cout << "发送成功" << endl; } } // 网络不稳定,要求重试三次,均失败 void SendMsg(const string& s) { for (size_t i = 0; i < 4; i++) { try { _SendMsg(s); // 走到这里,如果没有抛异常导致结束 // 那就代表成功了,可以执行到这个break,跳出循环 break; } catch (const Exception& e) { if (e.getid() == 102) { if (i == 3) throw; cout << "开始第" << i + 1 << "重试" << endl; } else { // 重新抛出异常 // throw e; throw; } } } } int main() { srand(time(0)); string str; while (cin >> str) { try { SendMsg(str); } catch (const Exception& e) { cout << e.what() << endl << endl; } catch (...) { cout << "Unkown Exception" << endl; } } return 0; }4.2 异常安全:避免资源泄漏
异常抛出后,当前函数后续代码不再执行,若之前申请了资源(内存、锁、文件句柄),未及时释放会导致资源泄漏,这是异常使用的核心痛点。
解决方案:
- 手动捕获释放:在
catch中释放资源后重新抛出异常; - RAII 机制:利用类的构造 / 析构自动管理资源(推荐,如智能指针、自定义资源管理类)后面的博客中还会再详细讲的;
- 析构函数不抛异常:析构函数若抛出异常,可能导致资源释放不完全,需在析构函数内部捕获处理。
示例:
double Divide(int a, int b) { // 当b == 0时抛出异常 if (b == 0) { throw "Division by zero condition!"; } return (double)a / (double)b; } void Func() { // 这里可以看到如果发生除0错误抛出异常,那下面的array就没有得到释放。 // 所以这里捕获异常后并不处理异常, // 异常还是交给外层处理,这里捕获了再重新抛出去。 int* array = new int[10]; int len, time; cin >> len >> time; try { cout << Divide(len, time) << endl; } catch (...) { cout << "delete []" << array << endl; delete[] array; // 重新抛出,捕获到什么抛出什么 throw; } cout << "delete []" << array << endl; delete[] array; } int main() { try { Func(); } catch (const char* errmsg) { cout << errmsg << endl; } catch (...) { cout << "Unkown Exception" << endl; } return 0; }4.3 异常规范( noexcept )
C++11 提供noexcept关键字,用于声明函数是否会抛出异常,帮助编译器优化代码:
函数声明 noexcept:表示函数不会抛出异常;函数声明 noexcept(表达式):表达式为true时,证明该函数不抛异常(主要是用来确认和验证);- 若声明
noexcept的函数实际抛出异常,程序会调用terminate终止(根本没有机会捕获)
补充:
- 对于用户和编译器而言,预先知道某个程序会不会抛出异常大有裨益,知道某个函数是否会抛出异常有助于简化调用函数的代码。
- C++98 中,函数参数列表的后面接
throw(),表示函数不抛异常;函数参数列表的后面接throw(类型1, 类型2...),表示可能会抛出多种类型的异常,可能会抛出的类型用逗号分割。 - C++98 的方式过于复杂,实践中并不好用,C++11 中进行了简化:函数参数列表后面加
noexcept表示不会抛出异常,啥都不加表示可能会抛出异常。 - 编译器并不会在编译时检查
noexcept,也就是说如果一个函数用noexcept修饰了,但同时又包含了throw语句或者调用的函数可能会抛出异常,编译器还是会顺利编译通过(有些编译器可能会报个警告)。但是一个声明了noexcept的函数抛出了异常,程序会调用terminate终止程序。 noexcept(expression)还可以作为一个运算符去检测一个表达式是否会抛出异常,可能会则返回false,不会就返回true。
实际示例:
// C++11标记不会抛异常的方法 // double Divide(int a, int b) noexcept // C++98用来标记会抛异常的方法 // double Divide(int a,int b) throw(const char*) // 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; }五. C++ 标准库异常体系
C++ 标准库提供了一套预定义的异常继承体系,基类为std::exception,派生类对应不同类型的标准异常(如内存分配失败、数组越界),可直接使用或继承扩展。
标准库异常体系核心类:
| 异常类 | 用途 | 错误信息获取方式 |
|---|---|---|
std::exception | 所有标准异常的基类 | what()(虚函数) |
std::bad_alloc | new分配内存失败时抛出 | what()返回 “bad alloc” |
std::out_of_range | 数组/容器越界时抛出 | what()返回越界信息 |
std::invalid_argument | 无效参数时抛出 | what()返回参数错误信息 |
- 不过我们日常的话一般使用
std::exception就OK了