《C++进阶之C++11》【异常】
【异常】目录
- 前言:
- ------------ 异常处理 ------------
- 1. 什么是异常处理?
- 2. 怎么使用异常处理?
- 3. 异常的类型有哪些?
- 4. 异常处理有哪些需要注意的事情?
- 5. 异常处理流程是什么呢?(超详细)
- 6. 标准异常类有哪些?
- 7. 异常处理的核心价值是什么?
- 8. 如何在复杂项目中设计可扩展的异常处理方案?
- ------------ 异常重抛 ------------
- 1. 为什么需要异常重抛?
- 2. 怎么进行异常重抛?
- 3. 异常重抛在具体场景下怎么使用?
- ------------ 异常安全 ------------
- 1. 常见的异常安全有哪些?
- 2. 实际场景中怎么实现异常安全?
- ------------ 异常规范 ------------
- 1. 为什么需要异常规范?
- 2. C++98和C++11的异常规范有什么区别?
- 3. throw() 和 noexcept 的区别是什么?
- 4. 为什么使用noexcept替换throw()?
- 5. noexcept关键字怎么使用?
- 6. noexcept关键字在编译期的行为是什么?
- 7. 关于noexcept关键字的使用建议是什么?
- ------------ 标准异常类 ------------
- 1. 什么是标准异常类?
- 2. 异常类层次结构是什么?
- 3. 怎么使用异常类?
- 4. 使用标准异常类有什么好处?
往期《C++初阶》回顾:
《C++初阶》目录导航
往期《C++进阶》回顾:
/------------ 继承多态 ------------/
【普通类/模板类的继承 + 父类&子类的转换 + 继承的作用域 + 子类的默认成员函数】
【final + 继承与友元 + 继承与静态成员 + 继承模型 + 继承和组合】
【多态:概念 + 实现 + 拓展 + 原理】
/------------ STL ------------/
【二叉搜索树】
【AVL树】
【红黑树】
【set/map 使用介绍】
【set/map 模拟实现】
【哈希表】
【unordered_set/unordered_map 使用介绍】
【unordered_set/unordered_map 模拟实现】
/------------ C++11 ------------/
【列表初始化 + 右值引用】
【移动语义 + 完美转发】
【可变参数模板 + emplace接口 + 新的类功能】
【lambda表达式 + 包装器】
前言:
Hi~ 小伙伴们大家好呀!(ノ≧∀≦)ノ♪
今天已经是国庆叠加中秋的 8 天小长假第 4 天啦~不知道大家这几天是在出门游玩 🚗✧、宅家充电 📚✧,还是补觉回血 😴✧ 呢?
不管是哪种状态,都祝大家玩得尽兴、学得轻松、睡得踏实(〃’▽’〃)!,好好享受这段难得的假期时光!♪(´▽`)♪
不过放松之余,咱们的 C++ 学习也别断档哦~╰(▔∀▔)╯
今天就接着往下学,带大家认识一个超重要的新知识点 ——【异常】!✧(≖ ‸ ≖✿)!
掌握它能帮咱们更好地处理代码里的突发情况,赶紧一起开始吧!✧。٩(ˊᗜˋ)و✧*。
------------ 异常处理 ------------
1. 什么是异常处理?
异常处理(Exception Handling)
:是一种处理程序运行时错误的机制,它允许程序在检测到错误时跳转到专门的错误处理代码,而不是 直接崩溃或产生不可预测的行为
- 异常处理使代码的正常逻辑与错误处理逻辑分离,提高了代码的可读性和健壮性
异常处理的基本概念:
C++ 异常处理通过三个关键字实现:
try
:标识可能抛出异常的代码块catch
:捕获并处理特定类型的异常throw
:抛出一个异常
1. throw:抛出异常
当程序检测到错误时,使用throw关键字抛出一个异常(可以是任意类型的值,如:基本类型、自定义类型等)
if (divisor == 0) {throw "除数不能为0"; // 抛出字符串类型的异常 }
2. try:监控可能抛出异常的代码块
try块包裹可能抛出异常的代码,当其中的代码抛出异常时,程序会跳转到对应的catch块处理
try {// 可能抛出异常的代码 }
3. catch:捕获并处理异常
catch块用于捕获try块中抛出的异常,每个catch块指定其能处理的异常类型
当try块中抛出异常时,程序会寻找
第一个匹配类型的catch块
执行catch (异常类型 变量名) {// 异常处理逻辑 }
2. 怎么使用异常处理?
异常处理的基本语法:
/*-------------------------异常处理的基本语法-------------------------*//* 说明:
* 1. error_condition:错误条件
* 2. exception_type:异常类型
*/try
{// 可能抛出异常的代码if (error_condition) {throw exception_type("Error message");}
} catch (const exception_type& e)
{// 处理异常std::cerr << "Caught exception: " << e.what() << std::endl;
}
catch (...)
{// 捕获所有其他类型的异常std::cerr << "Unknown exception caught" << std::endl;
}
异常处理的执行流程:
- 程序执行
try
块中的代码- 若
try
块中没有异常抛出,catch
块会被跳过,继续执行后续代码- 若
try
块中通过throw
抛出异常,程序立即终止try
块的执行,寻找最近的、类型匹配的catch块- 执行匹配的
catch
块中的错误处理代码- 处理完成后,程序继续执行
catch
块之后的代码
代码示例:异常处理的使用
#include <iostream>
using namespace std;/*------------------------ 除法函数 ------------------------*/
double divide(int num, int divisor)
{if (divisor == 0) // 当除数为0时抛出异常{throw "错误:除数不能为0"; //注:抛出异常(此处为字符串类型)}return (double)num / divisor;
}/*------------------------ 主函数 ------------------------*/
int main()
{//1.定义三个变量用于计算和保存结果int a = 10, b = 0;double result;//2.监控可能抛出异常的代码try{//2.1:result = divide(a, b);//2.2:cout << "结果:" << result << endl; //注意:若divide抛出异常,这行代码不会执行}//3.捕获字符串类型的异常catch (const char* errorMsg){cout << "捕获到异常:" << errorMsg << endl;}//4.异常处理后,程序继续执行cout << "程序继续运行..." << endl;return 0;
}
3. 异常的类型有哪些?
异常可以是任意类型,包括:
基本类型
(如:int
、double
)字符串
(如:const char*
、std::string
)自定义类
(推荐用于复杂异常信息)
代码示例:自定义类类型的异常
#include <iostream>
#include <string>
using namespace std;/*---------------------------自定义异常类---------------------------*/
class DivideError : public exception //注:继承自标准库的exception类,可以兼容标准异常处理机制
{
private:string msg; // 用于存储异常的详细信息public: //1.实现:“构造函数”DivideError(string message) : msg(message){ }/* 说明:* 1. 接收异常信息字符串并初始化成员变量* 2. 参数message:描述异常原因的字符串*///2. 重写exception类的what()方法const char* what() const noexcept override{return msg.c_str(); // 将string转换为C风格字符串返回}/* 说明:* 1. 该方法返回异常的描述信息,是异常处理的标准接口* 2. const noexcept:保证该方法不会修改对象状态,也不会抛出异常*/
};/*---------------------------除法函数---------------------------*/
double divide(int num, int divisor)
{//1.检查除数是否为0,如果是则抛出异常if (divisor == 0) {//1.1:抛出自定义异常对象,包含具体的错误信息throw DivideError("除数不能为0(自定义异常)");}//2.执行正常的除法运算并返回结果return (double)num / divisor;
}int main()
{//1.try块:包含可能抛出异常的代码try{//1.1:调用divide函数cout << divide(10, 0) << endl; //注:此处传入除数0,会触发异常//1.2:输出提示信息cout << "除法运算完成" << endl; //注:如果上面的函数调用抛出异常,下面的代码不会执行} //2.catch块:捕获并处理特定类型的异常catch (const DivideError& e) //这里捕获DivideError类型的异常,使用const引用接收以避免对象拷贝{//2.1:调用异常对象的what()方法获取错误信息并输出cout << "捕获到异常:" << e.what() << endl;}//3.程序执行到这里,无论是否发生异常都会继续运行cout << "程序继续执行..." << endl;return 0;
}
4. 异常处理有哪些需要注意的事情?
1. 异常类型匹配:catch块只捕获与自身类型匹配的异常(不进行隐式类型转换)
- 例如,
catch(int)
无法捕获double
类型的异常- 若没有完全匹配的
catch
,C++ 允许以下有限的隐式转换,让异常能匹配到兼容的catch
转换场景 | 示例说明 |
---|---|
非常量 → 常量 | 抛出 string (非常量)可匹配 const string& (权限缩小,更安全) |
数组 → 指针 | 抛出 int arr[5] 可匹配 int* (数组退化为指针) |
函数 → 指针 | 抛出函数 void foo() 可匹配 void (*)() (函数退化为指针) |
派生类 → 基类 | 抛出 Derived (派生类)可匹配 Base& (多态场景核心规则) |
2. 捕获所有异常:使用catch(…)可以捕获任意类型的异常
若异常传播到
main
函数后仍无匹配的catch
,程序会调用std::terminate()
强制终止(通常表现为崩溃)为避免程序意外终止,建议在
main
中添加兜底捕获通常作为最后一个catch
块,处理未预料到的错误try {// 可能抛出异常的代码 }catch (const DivideError& e) {// 处理特定异常 } catch (...) {// 处理所有其他未捕获的异常cout << "发生未知异常" << endl; }
3. 异常的匹配:catch捕获逻辑:
抛出异常后,如果try块中抛出的异常在当前作用域没有匹配的catch块,异常会沿调用链向上传播到调用者的作用域,直到找到匹配的catch块或程序终止
匹配规则:
catch的参数类型需与抛出的异常对象类型一致(或兼容,如:继承关系)
若存在多个匹配的
catch
,选择离抛出位置最近的那个(栈展开时先遇到的)#include <iostream> #include <string> using namespace std;void func() {throw string("异常测试"); } int main() {//1.try{func();}//2.需特殊转换,才会匹配 const char*()catch (const char* e){cout << "捕获 C 字符串异常" << endl;}//3.完全匹配 string 类型catch (string& e){cout << "捕获 string 异常" << endl;} }
- 优先规则:完全匹配 > 隐式转换,就近捕获
查找范围:从 throw 所在函数开始,逐层向上遍历调用链(如:
funcA
→funcB
→main
),直到找到匹配的catch块
或程序终止
4. 异常对象的拷贝与销毁:抛出异常时,若异常对象是局部对象(如:函数内的栈对象),会生成一个拷贝传递给 catch 块(类似函数的传值返回)
- 这个拷贝的异常对象会在 catch 块执行结束后自动销毁,避免内存泄漏
5. 资源管理:异常可能导致程序跳过某些清理代码(如:释放内存、关闭文件)
- 因此推荐使用
RAII(资源获取即初始化)
机制(如:智能指针)管理资源,确保异常发生时资源能被正确释放
6. 不要滥用异常:异常适用于处理罕见且不可预测的错误(如:文件损坏、网络中断)
- 异常会增加程序复杂度,应避免滥用(如:用异常替代普通条件判断),否则会降低程序性能和可读性
7. 异常执行流程的细节
- throw的即时效果:
执行 throw 后,当前函数中 throw 之后的代码立即停止执行,程序跳转到匹配的 catch 块- 调用链的提前退出:
若 catch 块不在当前函数中,调用链上的函数会逐层提前退出(类似返回),直到进入 catch 所在函数- 对象的销毁规则:
异常触发后,调用链上所有局部对象(如:函数内的临时变量、未返回的对象)会被自动销毁(触发析构函数),保证资源释放(RAII 机制的基础)
#include <iostream>
#include <string>
using namespace std;/*-------------------------------自定义异常类-------------------------------*/
class DivideError
{
public://存储异常的具体描述信息string msg; //构造函数:初始化异常信息DivideError(string m): msg(m) { }
};
/* 说明:
* 1. 自定义异常类:用于表示除法运算中的错误
* 2. 推荐使用类对象传递异常信息,可包含更丰富的错误详情
*//*-------------------------------函数A-------------------------------*/
void funcA()
{//1.此处模拟检测到除数为0的错误,直接抛出自定义异常throw DivideError("除数不能为 0"); //异常对象包含具体的错误信息:"除数不能为 0"
}
/* 说明:
* 1. 函数A:负责抛出异常的函数
* 2. 当检测到除法错误时,抛出自定义的DivideError异常对象
*//*-------------------------------函数B-------------------------------*/
void funcB()
{//1.调用可能抛出异常的函数funcA(); //2.打印提示内容cout << "funcB:这条语句不会执行" << endl;
}
/* 说明:
* 1. 函数B:中间调用层,不处理异常
* 2. 调用funcA(),但不捕获其抛出的异常,异常会向上传播
*//*-------------------------------主函数-------------------------------*/
int main()
{//1.try块:包裹可能抛出异常的代码try{funcB(); // 调用funcB(),该函数间接调用可能抛出异常的funcA()}//2.第一个catch块:专门捕获DivideError类型的异常catch (DivideError& e) //注:使用引用接收异常对象,避免对象拷贝,提高效率{//2.1:输出异常信息cout << "捕获异常:" << e.msg << endl; //通过异常对象的msg成员获取错误描述}//3.第二个catch块:捕获所有其他未被处理的异常(通配符)catch (...) //注:作为兜底处理,确保所有异常都能被捕获,避免程序崩溃{cout << "捕获未知异常" << endl;}//4.异常处理完成后,程序继续执行后续代码cout << "程序正常结束" << endl;return 0;
}
执行流程解析:
funcA
抛出DivideError
异常 →funcA
立即停止执行funcB
中funcA
之后的代码(cout
)被跳过,异常传播到main
函数main
中的try
块匹配到catch (DivideError& e)
→ 执行错误处理逻辑- 调用链上的局部对象(如:
funcB
中的临时变量)被自动销毁
5. 异常处理流程是什么呢?(超详细)
C++ 异常处理流程(抛出 → 捕获 → 栈展开)
1. 异常抛出后的基本流程
当程序执行 throw 抛出异常时,会触发以下步骤:
暂停当前函数执行:抛出异常的位置会立即停止当前函数的正常执行流程,转而进入异常捕获逻辑
优先检查当前函数的 try/catch:如果 throw 出现在 try 块内部,程序会在当前函数中查找类型匹配的 catch 子句
- 若找到匹配的 catch,则跳转到该 catch 块执行错误处理
2. 栈展开:跨函数的异常传播
如果当前函数中没有 try/catch,或 catch 的类型不匹配,会触发栈展开:
- 退出当前函数:
当前函数中未执行的代码会被跳过,程序自动销毁当前函数的局部对象(触发析构函数)- 向上层调用链查找:
异常会传播到调用当前函数的外层函数,重复检查该函数的 try/catch- 持续传播直到找到匹配:
逐层向上遍历调用链(如:funcA
→funcB
→main
),直到找到匹配的catch
或到达程序入口
(main 函数)
3. 未捕获异常的最终结果
如果异常传播到 main 函数后,仍未找到匹配的 catch:
- 程序会自动调用标准库函数
std::terminate()
,直接终止程序运行(通常表现为崩溃)
4. 异常处理后的执行流程
一旦找到匹配的catch块并执行:
- 误处理代码会被执行
- 处理完成后,程序会继续执行 catch 块之后的代码(而非 throw 所在的原流程)
代码示例:异常处理流程
#include <iostream>
#include <string>
using namespace std;/*----------------------------除法函数----------------------------*/
double Divide(int a, int b)
{//1.包裹可能出现异常的代码try{//1.1:当除数 b 为 0 时抛出异常if (b == 0){//第一步:定义一个局部字符串对象,存储异常信息string s("Divide by zero condition!");//第二步:抛出异常类型为 string(实际是抛出局部对象的拷贝)throw s;}//1.2:正常除法运算返回结果else{return ((double)a / (double)b);}}//2.捕获int类型的异常catch (int errid) //注意:仅处理 int 类型的异常{cout << errid << endl; // 若捕获到 int 类型异常,输出错误码}return 0;
}/*----------------------------功能函数----------------------------*/
void Func()
{//1.定义两个变量并输出并进行赋值int len, time;cin >> len >> time;//2.包裹可能出现异常的代码try{cout << Divide(len, time) << endl; //调用 Divide 函数,输出结果}//3.捕获 const char* 类型的异常catch (const char* errmsg) {cout << errmsg << endl; //输出 C 风格字符串的异常信息}//4.输出函数名和行号,标记函数执行到此处cout << __FUNCTION__ << ":" << __LINE__ << " 执行" << endl;/* 说明:* 1. __FUNCTION__:当前函数名* 2. __LINE__:当前行号*/
}//:
/*----------------------------主函数----------------------------*/
int main()
{while (1) //循环调用 Func,持续测试异常{//1.定义两个变量并输出并进行赋值try{// 调用 Func 函数Func();}//2. 捕获string 类型的异常(与 Divide 中抛出的类型匹配)catch (const string& errmsg){cout << errmsg << endl; //输出 string 类型的异常信息 }}return 0;
}
关键逻辑注释:
1. Divide函数的问题:
- 抛出的异常是
string
类型(throw s;
),但catch
块只处理int
类型- 导致异常无法在此函数内捕获,会继续传播到上层调用
2. 异常的实际传播路径:
Divide
中抛出string
→ 未被Divide
的catch (int)
捕获- → 传播到
Func
的try
块 →Func
的catch (const char*)
也不匹配- → 继续传播到
main
的catch (const string&)
→ 最终被捕获
3. catch类型不匹配的影响:
Divide
的catch (int)
、Func
的catch (const char*)
均无法匹配string
类型的异常,导致异常必须传播到main
才能被处理
4. 调试信息 FUNCTION 和 LINE:
- 宏
__FUNCTION__
输出当前函数名,__LINE__
输出代码行号,用于标记程序执行位置(如Func:129 执行
)
核心总结:
- 异常处理的核心逻辑:通过 throw 中断正常流程,通过 try/catch 捕获,通过栈展开跨函数传播
- 栈展开的价值:自动销毁调用链上的局部对象,保证资源释放(RAII 机制的基础)
- 未捕获异常的风险:会导致程序崩溃,因此建议在合适的层级(如
main
函数)添加兜底catch(...)
6. 标准异常类有哪些?
C++ 标准库定义了一系列异常类(继承自std::exception),用于标准库函数抛出的异常,例如:
std::bad_alloc
:new
分配内存失败时抛出std::out_of_range
:访问容器(如:vector
)越界时抛出(如:vector::at()
)std::invalid_argument
:无效参数时抛出这些异常类都可以通过
catch(const std::exception& e)
捕获,并通过e.what()
获取异常描述。
代码示例:
#include <iostream>
#include <vector>
#include <stdexcept> // 包含标准异常类的头文件,提供std::exception及其派生类
using namespace std;int main()
{//1.创建一个vector容器并初始化vector<int> v = { 1, 2, 3 };//2.try块:包含可能抛出异常的代码try{//1.使用vector的at()方法会进行边界检查cout << v.at(10) << endl; /* 说明:* 1. 尝试访问索引为10的元素,而容器只有3个元素(索引0,1,2)* 2. 此时会抛出out_of_range类型的异常*///2.打印输出信息cout << "元素访问成功" << endl; //注:如果上面的代码抛出异常,下面的语句不会执行}//3.第一个catch块:专门捕获out_of_range类型的异常catch (const out_of_range& e) //注:out_of_range是std::exception的派生类,用于表示范围越界错误{ //3.1:调用异常对象的what()方法,获取异常的描述信息cout << "异常:" << e.what() << endl;}//4.第二个catch块:捕获所有其他标准异常(std::exception及其派生类)catch (const exception& e) //注:作为兜底处理,确保所有标准异常都能被捕获{cout << "标准异常:" << e.what() << endl;}//5.异常处理完成后,程序继续执行cout << "程序继续运行..." << endl;return 0;
}
总结:通过异常处理,C++ 程序能够更优雅地应对运行时错误,提高了代码的健壮性和可维护性。
7. 异常处理的核心价值是什么?
异常处理机制让程序中独立开发的模块,能在运行时高效传递错误信息并处理。
它的核心优势是 “分离错误检测与处理逻辑”
检测
错误的代码(如:文件读取失败)只需抛出异常
,无需关心如何处理处理
错误的代码(如:重试、提示用户)只需捕获异常
,无需关心错误如何触发
对比 C 语言的错误码方案:
- C 语言通过
错误码标识错误
,但需手动查询错误码含义(如:查文档才知errno=2
是文件不存在),且错误码无法携带复杂信息(如:自定义错误描述)- C++ 异常通过
抛出对象传递错误
,可携带更丰富的上下文(如:错误详情、调用栈),处理更灵活
C++ 异常的设计,本质是通过 “对象传递错误 + 调用链跳转 + 自动资源清理”,解决传统错误码的缺陷:
- 错误信息更丰富(用对象携带详情)
- 错误处理更灵活(跨函数、跨模块捕获)
- 资源释放更可靠(自动销毁局部对象)
8. 如何在复杂项目中设计可扩展的异常处理方案?
下面的代码是一个模拟多层服务调用中异常处理机制的示例程序。
主要功能是展示大型项目中如何
设计异常体系
、抛出异常
以及统一捕获处理异常
#include<iostream>
#include<string>
#include<thread>#include<cstdlib> // rand、srand所需头文件
#include<ctime> // time函数所需头文件
#include<chrono> // 时间相关工具(this_thread::sleep_for)
using namespace std;// ===================== 异常类体系 =====================/*------------------------------【异常基类设计】------------------------------*/
class Exception //定义通用异常接口,派生出各模块异常
{ protected:string _errmsg; // 错误描述信息int _id; // 错误码(可用于分类处理)public://1.构造函数:初始化错误信息和错误IDException(const string& errmsg, int id):_errmsg(errmsg), _id(id){ }//2.虚函数:返回异常描述(多态关键,子类需重写)virtual string what() const{return _errmsg;}//3.获取错误IDint getid() const{return _id;}
};/*------------------------------【SQL模块异常】------------------------------*/
class SqlException : public Exception //继承Exception,补充SQL语句信息
{
private:const string _sql; // 存储触发异常的SQL语句public://1.构造函数:传递基类参数 + SQL语句SqlException(const string& errmsg, int id, const string& sql):Exception(errmsg, id), _sql(sql){ }//2.重写what:拼接SQL异常信息virtual string what() const override{string str = "SqlException:";str += _errmsg;str += "->";str += _sql;return str;}
};/*------------------------------【缓存模块异常】------------------------------*/
class CacheException : public Exception //继承Exception,基础错误场景
{
public://1.构造函数:传递基类参数CacheException(const string& errmsg, int id):Exception(errmsg, id){ }//2.重写what:标识缓存异常virtual string what() const override{string str = "CacheException:";str += _errmsg;return str;}
};/*------------------------------【HTTP模块异常】------------------------------*/
class HttpException : public Exception //继承Exception,补充请求类型
{
private:const string _type; // 请求类型(如get/post)public://1.构造函数:传递基类参数 + HTTP请求类型HttpException(const string& errmsg, int id, const string& type):Exception(errmsg, id), _type(type){ }//2.重写what:拼接HTTP请求类型 + 错误virtual string what() const override{string str = "HttpException:";str += _type;str += ":";str += _errmsg;return str;}
};// ===================== 模块功能函数 =====================/*------------------------------【SQL管理模块】------------------------------*/
void SQLMgr() //随机触发“SQL异常”
{//情况一:1/7概率触发异常 ----> 这里以及下面的异常触发条件都是只是为了帮助理解,无实际意义if (rand() % 7 == 0){// 抛SQL异常:带错误信息+SQL语句throw SqlException("权限不足", 100, "select * from name = '张三'");}//情况二:正常调用else{cout << "SQLMgr 调用成功" << endl;}
}/*------------------------------【缓存管理模块】------------------------------*/
void CacheMgr() //随机触发“缓存异常”
{//1.//情况一:1/5概率触发“权限不足”if (rand() % 5 == 0){throw CacheException("权限不足", 100);}//情况二:1/6概率触发“数据不存在”else if (rand() % 6 == 0){throw CacheException("数据不存在", 101);}//情况三:正常调用else{cout << "CacheMgr 调用成功" << endl;}//2.调用SQL模块(模拟多层依赖)SQLMgr();
}// :
/*------------------------------【HTTP服务模块】------------------------------*/
void HttpServer() //随机触发“HTTP异常”
{ //1.//情况一: 1/3概率触发“资源不存在”(get请求)if (rand() % 3 == 0){throw HttpException("请求资源不存在", 100, "get");}//情况二:1/4概率触发“权限不足”(post请求)else if (rand() % 4 == 0){throw HttpException("权限不足", 101, "post");}//情况三:正常调用else{cout << "HttpServer调用成功" << endl;}//2.调用缓存模块(模拟多层依赖)CacheMgr();
}// ===================== 主逻辑 =====================
int main()
{//1.初始化随机数种子(按时间)srand(time(0));//2.持续模拟服务调用while (1){//2.1:线程休眠1秒(避免高频输出)this_thread::sleep_for(chrono::seconds(1));//2.2:包含可能抛出异常的代码try{HttpServer(); // 启动HTTP服务(触发多层调用)}//2.3:专门:捕获基类Exception类型的异常catch (const Exception& e) //注意:利用多态,派生类异常会匹配到基类引用{cout << "捕获异常:" << e.what() << endl;cout << "错误码:" << e.getid() << endl;}//2.4:兜底:捕获所有未处理异常catch (...){cout << "Unkown Exception" << endl;}}return 0;
}
核心设计解析:
1. 异常体系分层
- 基类
Exception
定义通用接口(what
/getid
),派生类(SqlException
等)补充模块特有信息catch (const Exception& e)
可统一处理所有模块异常,符合开闭原则(新增模块只需继承基类)
2. 多态的关键作用
- 派生类重写what(),捕获基类引用时会自动调用派生类的what
- 实现 “一次捕获,多类型处理”,简化异常逻辑
3. 模块异常补充信息
SqlException
存储 SQL 语句、HttpException
存储请求类型- 便于定位问题(生产环境可结合日志快速排查)
4. 服务调用链路
异常会沿调用链向上传播,最终被
main
的catch (Exception&)
捕获HttpServer() → CacheMgr() → SQLMgr()
5. 随机触发逻辑
rand() % N == 0
控制异常概率,模拟真实服务的偶发错误sleep_for(1s)
控制输出频率,避免刷屏
------------ 异常重抛 ------------
1. 为什么需要异常重抛?
在复杂业务中,捕获异常后常需
分类处理
:对特定类型异常做特殊逻辑,其他异常则继续向上传播。
假设一个函数需要:捕获异常后,先检查是否是特定类型(如:
SqlException
)
若是:执行特殊处理(如:回滚事务)
若不是:将异常重新抛出,交给外层调用链处理
2. 怎么进行异常重抛?
核心语法:throw 重新抛出当前异常
捕获异常后,用无参数的 throw; 可直接抛出当前捕获的异常对象(不改变异常类型)
示例代码:
void processRequest()
{//1.包含可能抛出异常的代码try{callService(); // 可能抛出多种异常的逻辑}//2.捕获基类异常(多态场景)catch (const Exception& e) {//2.1:特殊处理特定异常if (typeid(e) == typeid(SqlException)) {// 假设是数据库异常,执行回滚rollbackTransaction();cout << "已回滚事务:" << e.what() << endl;}//2.2:其他异常重新抛出else {throw; // 不改变异常,继续向上传播}}
}int main()
{//1.包含可能抛出异常的代码try {processRequest();}//2.最终捕获并处理所有异常catch (const Exception& e) {cout << "外层处理异常:" << e.what() << endl;}
}
关键逻辑说明:
1. 分类处理
- 用
typeid
或动态类型
判断(如:if (dynamic_cast<SqlException*>(&e))
)识别异常类型- 对特定异常执行定制逻辑(如:资源清理、日志记录)
2. 重新抛出的作用
- 让更外层的调用链处理异常(如:main函数统一兜底)
- 保留异常的原始类型和上下文,避免信息丢失
3. 对比:throw e; vs throw;
throw e;
:会拷贝异常对象,可能切片(若e
是基类引用,派生类信息丢失)throw;
:直接抛出当前捕获的异常对象,无拷贝,保留完整类型信息
总结:通过
throw;
重新抛出,既能实现局部异常的特殊处理,又能保留异常的完整上下文,是复杂异常流程中的关键技巧。
3. 异常重抛在具体场景下怎么使用?
代码示例:HTTP 相关场景下的异常重抛的使用
#include <iostream>
#include <string>
#include <ctime>
#include <cstdlib>
using namespace std;/*-------------------------基类异常-------------------------*/
class Exception
{
public://1.纯虚函数what:返回异常的“描述信息”// 作用:多态调用时,能返回具体异常的详细说明(如 "网络不稳定")virtual string what() const = 0;//2.纯虚函数getid:返回异常的“错误码”// 作用:用于分类处理异常(如:102 代表网络错误、103 代表业务错误)virtual int getid() const = 0;//3.虚析构函数~Exception:// 1)确保派生类对象销毁时,先调用派生类析构,再调用基类析构// 2)使用 =default 让编译器生成默认实现,简洁且符合规范virtual ~Exception() = default;
};/*-------------------------HTTP 异常-------------------------*/
class HttpException : public Exception
{
private:string _msg; // 异常的具体描述信息(如:"网络不稳定,发送失败")int _id; // 异常对应的错误码(如:102 代表网络错误)string _type; // HTTP 请求类型(如:"put"/"get",区分不同场景的 HTTP 操作)public://1.构造函数:初始化私有成员HttpException(const string& msg, int id, const string& type): _msg(msg), // 初始化异常描述_id(id), // 初始化错误码_type(type) // 初始化 HTTP 请求类型{ }//2.重写基类的纯虚函数 what()string what() const override{return "HttpException: " + _type + " -> " + _msg; //返回包含 HTTP 场景信息的异常描述}//3.重写基类的纯虚函数 getid()int getid() const override //返回异常对应的错误码(让外层代码通过错误码分类处理){return _id;}
};/*-------------------------模拟发送消息的底层函数-------------------------*/void _SeedMsg(const string& s) //可能抛出异常
{if (rand() % 2 == 0) // 随机数模拟异常概率{// 抛出网络不稳定异常(102 号错误)throw HttpException("网络不稳定,发送失败", 102, "put");}else if (rand() % 7 == 0) {// 抛出好友关系异常(103 号错误)throw HttpException("你已经不是对象的好友,发送失败", 103, "put");}else {// 模拟发送成功cout << "发送成功: " << s << endl;}
}/*-------------------------封装重试逻辑的发送函数-------------------------*/void SendMsg(const string& s) //核心异常处理
{for (size_t i = 0; i < 4; ++i) //最多重试 3 次{//1.1:包含可能抛出异常的代码try {//第一步:尝试发送消息_SeedMsg(s);//第二步:发送成功则跳出循环break;}//1.2:捕获Exception类型的异常catch (const Exception& e) {//情况一:错误码是102if (e.getid() == 102) {// 重试 3 次后仍失败,重新抛出异常if (i == 3) {cout << "重试 3 次均失败,网络太差!" << endl;throw;}//输出重试提示cout << "开始第 " << (i + 1) << " 重试" << endl;}//情况二:错误码不是102else {throw; //直接重新抛出}}}
}/*-------------------------主函数-------------------------*/
int main()
{//1.初始化随机数种子(按当前时间)srand(time(0));//2.定义字符串来接受用户输入的消息string str;//3.持续接收用户输入while (cin >> str) { try {//发送消息并处理可能的异常SendMsg(str);}catch (const Exception& e) {//精准:输出异常详细信息cout << "捕获异常: " << e.what() << endl << endl;}catch (...) {//兜底:捕获未知异常cout << "Unknown Exception" << endl;}}return 0;
}
------------ 异常安全 ------------
1. 常见的异常安全有哪些?
异常安全是要避免因异常抛出,导致程序出现资源泄漏、执行流程失控等风险。
主要体现在以下两类场景:
一、普通函数执行中因异常引发的资源泄漏
- 当程序执行流程中申请了资源(如:动态内存、锁、文件句柄等),后续需要释放这些资源时
- 若中间逻辑抛出异常,会跳过后续的资源释放代码,最终造成资源泄漏
举个示例:
void func() {//1.申请内存资源int* p = new int[100]; //2.中间逻辑可能抛出异常if (/* 某种错误条件 */){throw "内存操作异常";}//3.正常流程下的资源释放delete[] p; //注意异常安全:若异常被抛出,delete[] p 不会执行,内存无法释放,造成泄漏 }
安全问题:若异常被抛出,
delete[] p
不会执行,内存无法释放,造成泄漏。
解决思路:
用try-catch主动捕获异常,在捕获后补充资源释放逻辑,必要时可重新抛出异常让上层处理
void func() {//1.申请内存资源int* p = new int[100];//2.包含可能抛出异常的代码try{if (/* 错误条件 */){throw "内存操作异常";}}//3.捕获所有其他未被处理的异常catch (...){//3.1:异常时释放资源delete[] p; //3.2:重新抛出,让上层感知错误throw; }//4.正常流程释放delete[] p; }
更推荐RAII(资源获取即初始化)机制(如:智能指针unique_ptr/shared_ptr),让资源的释放由对象析构自动完成,无需手动控制
// 函数功能:演示使用智能指针unique_ptr实现异常安全的内存管理
void func()
{// 1. 使用unique_ptr智能指针管理动态数组内存unique_ptr<int[]> p(new int[100]);/* 说明:* 1. unique_ptr是C++11引入的智能指针,遵循RAII(资源获取即初始化)原则* 2. 这里分配了一个包含100个int元素的动态数组* 3. 模板参数<int[]>表示管理的是int类型的数组*/// 2. 模拟可能抛出异常的错误检查if (/* 错误条件 */) // 假设这里是某种错误条件判断,实际使用时会替换为具体的判断逻辑{//2.1:当错误条件满足时,抛出异常throw "内存操作异常"; //异常类型为const char*(C风格字符串),描述错误信息}// 3. 后续正常业务逻辑(此处省略)// ...// 4. 无需手动释放内存/* 说明:* 1. 无论函数是正常执行结束,还是因抛出异常而提前退出* 2. unique_ptr会在离开其作用域(即func()函数结束)时自动调用delete[]* 3. 释放所管理的动态数组内存,彻底避免内存泄漏*/
}
二、析构函数中抛出异常的风险
- 析构函数的职责是 释放对象持有的资源(如:关闭文件、释放锁等)
- 若析构函数执行中抛出异常,且未妥善处理,会引发更复杂的资源泄漏
假设析构函数要释放 10 个资源,执行到第 5 个时抛出异常,程序会直接退出析构流程,导致后续 5 个资源无法释放。
举个示例:
class Resource { public:~Resource(){releaseResource1(); // 释放第1个资源releaseResource2(); // 释放第2个资源(假设此处抛异常)releaseResource3(); // 若上面抛异常,这里不会执行// ... 后续7个资源释放逻辑} private:void releaseResource2() {throw "释放资源2失败";} };
安全问题:若
releaseResource2()
抛异常,releaseResource3()
及之后的资源释放逻辑会被跳过,造成泄漏。
解决原则:(参考《Effective C++》条款 8)
析构函数中应避免抛出异常
若无法避免(如:某些资源释放必然可能抛异常),需在析构函数内部用try-catch捕获并处理,不让异常 “逃离” 析构函数
~Resource() {try {releaseResource2();}catch (...) {logError("释放资源2失败");}releaseResource3(); // 继续执行后续资源释放逻辑 }
或设计接口,让调用者在对象销毁前,主动处理可能抛异常的资源释放逻辑,避免析构函数中处理复杂风险
异常安全的核心目标:
通过合理设计代码,确保无论是否抛出异常,资源都能被正确释放,程序执行流程不会因异常出现不可控的泄漏或崩溃
RAII 是 C++ 中解决此类问题的 “最优解”,而析构函数中谨慎处理异常,是保证对象销毁时资源完整释放的关键
2. 实际场景中怎么实现异常安全?
#include <iostream>
#include <string>
using namespace std;/*------------------------除法函数------------------------*/
double Divide(int a, int b)
{//1.当除数 b 为 0 时抛出异常if (b == 0){//抛出 C 风格字符串异常,表示“除以 0”的错误throw "Division by zero condition!";}//2.正常计算除法并返回结果return static_cast<double>(a) / b;
}/*------------------------功能函数------------------------*/
void Func()
{//1.动态分配一个大小为 10 的 int 数组(需要手动释放内存)int* array = new int[10];//2.包含可能抛出异常的代码try{//2.1:从控制台输入两个整数int len, time;cin >> len >> time;//2.2:调用 Divide 函数,可能抛出异常cout << Divide(len, time) << endl;}//3.捕获所有类型的异常(通配符捕获)catch (...){//3.1:释放动态分配的数组内存cout << "delete [] " << array << endl;delete[] array;//3.2:重新抛出异常,让上层调用者继续处理throw; //这里不会改变异常的类型,只是将异常传递给更外层的 catch}//4.释放动态分配的数组内存cout << "delete [] " << array << endl; //注意:如果上面的 try 块中没有抛出异常,会执行到这里delete[] array;
}/*------------------------主函数------------------------*/
int main()
{//1.包含可能抛出异常的代码try{Func(); // 调用 Func 函数,可能抛出异常}//2.捕获 const char* 类型的异常(与 Divide 中抛出的类型匹配)catch (const char* errmsg){cout << errmsg << endl; // 输出异常信息}//3.捕获 std::exception 类型的异常(C++ 标准库异常)catch (const exception& e){cout << e.what() << endl; //输出异常的详细描述(通过 what() 方法)}//4.捕获所有其他类型的异常(兜底处理)catch (...){cout << "Unknown Exception" << endl; // 输出未知异常的提示}return 0;
}
详细注释说明
Func
函数:1. 动态内存分配:
- 使用
new int[10]
动态分配一个大小为10
的数组,需要在适当的地方用delete[]
释放内存,否则会导致内存泄漏
2. try-catch 块:
- try块 :
- 从控制台输入两个整数
len
和time
,然后调用Divide
函数- 如果
Divide
抛出异常,会立即跳转到catch
块- catch (…)块 :
- 使用通配符
...
捕获所有类型的异常(无论异常是什么类型,都会被捕获)- 捕获异常后:
- 先释放动态分配的数组内存(避免内存泄漏)
- 然后通过
throw;
重新抛出异常,让上层调用者(如:main
函数)继续处理- 内存释放 :
- 如果
try
块中没有抛出异常,会执行到try-catch
块之后的代码,同样需要释放动态分配的数组内存
------------ 异常规范 ------------
1. 为什么需要异常规范?
异常规范(Exception Specification,也叫异常说明)
:存在的意义,本质是解决 “函数行为的可预测性” 问题—— 让开发者和编译器能提前知道函数会不会抛异常、抛哪些异常,从而让代码更安全、更易维护。可以从以下几个维度理解:
1. 对开发者:明确错误处理边界
想象你调用一个函数
sendRequest()
,如果不知道它会不会抛异常,你得时刻担心:
- 是不是要加 try/catch?
- 该捕获哪些类型的异常?
- 异常会如何影响程序流程?
有了异常规范(比如:
noexcept
或 C++98 的throw()
),调用者能清晰判断:// 明确承诺:不会抛异常 → 调用者无需 try/catch void sendRequest() noexcept { ... }// 明确声明:可能抛 NetworkError 或 TimeoutError → 调用者针对性捕获 void sendRequest() throw(NetworkError, TimeoutError) { ... }
价值:减少调用者的 “猜测成本”,让错误处理更精准。
2. 对编译器:优化代码 & 提前发现问题
编译器知道函数的异常行为后,能做两件关键事:
(1)优化代码生成
如果函数声明
noexcept
,编译器会认为它 “绝对安全”,可以:
- 省略异常处理的额外代码(如:栈展开的准备逻辑)
- 更激进地内联函数(因为无需保留异常回溯信息)
(2)提前发现矛盾
C++98 的
throw(类型列表)
会强制检查:如果函数实际抛出的异常类型不在列表里,编译器会报错。虽然这个特性因过于严格被弃用,但本质是想阻止 “意外抛异常”
3. 对项目:统一异常处理规范
大型项目中,团队可以约定:
- 哪些函数必须
noexcept
(如:工具类的辅助函数)- 哪些函数需要明确异常类型(如:网络模块抛
NetworkException
)这样,新人接手代码时能快速理解:
- 调用
noexcept
函数无需处理异常。- 调用声明
throw(...)
的函数,必须处理指定类型的异常。价值:降低团队协作的沟通成本,让异常处理逻辑更一致。
一句话总结:异常规范的本质是给函数的 “错误反馈方式” 贴标签:
- 让调用者知道 “该不该处理异常”
- 让编译器知道 “能不能优化代码”
- 让团队知道 “怎么统一处理错误”
现代 C++ 中,
noexcept
是更简洁、更实用的选择,但核心目标从未改变:让异常处理更可控。
2. C++98和C++11的异常规范有什么区别?
C++98 的异常规范:throw()
C++98 用
throw(类型列表)
声明函数可能抛出的异常类型:
语法示例 | 含义说明 |
---|---|
void func() throw(); | 函数不抛出任何异常 |
void func() throw(int); | 函数仅可能抛出 int 类型异常 |
void func() throw(int, string); | 函数可能抛出 int 或 string 异常 |
C++11 的异常规范:noexcept关键字
因 C++98 的
throw(...)
过于复杂,C++11 改用更简洁的noexcept
:
语法 | 含义说明 |
---|---|
void func() noexcept; | 函数不会抛出任何异常(编译期承诺) |
void func(); | 函数可能抛出任意异常(无约束) |
3. throw() 和 noexcept 的区别是什么?
C++98 用
throw(类型列表)
声明异常规范(如:throw(int)
表示仅抛int
异常),但因设计复杂已被弃用。
noexcept
是更简洁的替代方案:
特性 | throw() | noexcept |
---|---|---|
语法 | 需枚举所有可能抛出的类型 | 仅需 noexcept 或表达式 |
编译器检查 | 强制检查(违反则编译报错) | 不强制检查(仅做承诺) |
异常处理 | 抛异常时调用unexpected() | 抛异常时调用terminate() |
适用场景 | 已被标准弃用,仅兼容旧代码 | 现代 C++ 推荐使用 |
4. 为什么使用noexcept替换throw()?
C++98 的
throw(类型列表)
设计太复杂:
- 枚举所有可能的异常类型,写起来麻烦、维护成本高
- 编译器强制检查,导致 “改一个异常类型就得改所有调用处”
noexcept
做了减法:
- 只需关注 “会不会抛异常”,不用关心 “抛什么类型”
- 编译器不强制检查(但运行时会崩溃惩罚违规行为),更灵活
5. noexcept关键字怎么使用?
noexcept
:是 C++11 引入的异常规范关键字,用于声明函数是否可能抛出异常。
- 核心作用:明确函数的异常行为,帮助编译器优化代码并让调用者提前知晓风险
noexcept关键字的两种常见形式:
noexcept
声明函数不会抛出任何异常(编译期承诺)void func() noexcept {// 函数体中不应有 throw 语句,也不应调用可能抛异常的函数 }
noexcept(表达式)
根据编译期表达式的结果决定是否抛出异常:
若表达式为
true
,等价于noexcept
(不抛异常)若表达式为
false
,函数可能抛异常(等价于不写noexcept
)// 检测 add 函数是否抛异常,决定当前函数是否声明为 noexcept void wrapper() noexcept(noexcept(add(1, 2))) {add(1, 2); }
6. noexcept关键字在编译期的行为是什么?
noexcept
是 “编译期承诺”,但编译器不会强制检查 :
- 若声明noexcept的函数
实际抛出异常
:
- 程序会调用
std::terminate()
直接终止(通常崩溃)- 若函数
内部有throw
或调用了可能抛异常的函数
:
- 编译器仍会编译通过(部分编译器会警告),但运行时可能崩溃
7. 关于noexcept关键字的使用建议是什么?
推荐用 noexcept 的场景:
- 简单操作:如:基础数学运算(
add
、sub
)、访问器(getter
)等确定不会抛异常的函数- 析构函数:默认隐含
noexcept
(除非显式取消),析构函数抛异常会导致资源释放逻辑中断- 交换函数:容器的
swap
若声明为noexcept
,可提升性能(避免不必要的拷贝)
不建议用 noexcept 的场景:
- 可能失败的操作:如:内存分配(
new
可能抛bad_alloc
)、文件操作(可能抛 IO 异常)等- 需要通知错误的函数:若函数失败需通过异常传递错误信息(如:网络请求失败),不应声明
noexcept
------------ 标准异常类 ------------
1. 什么是标准异常类?
在 C++ 中,标准库定义了一套
异常类体系
,方便开发者处理程序运行过程中出现的各种错误情况。这些标准异常类都继承自
std::exception
,构成了一个层次分明的体系。cplusplus网站上关于
标准异常类
的介绍:exception - C++ Reference
2. 异常类层次结构是什么?
std::exception
:是整个标准异常类体系的基类。
- 它定义了一些基本接口,最常用的是
what()
成员函数,用于返回异常的描述信息
其派生关系如下:
- std::exception:基类,提供了what() 函数,返回一个const char* 类型的异常信息。
- std::runtime_error:运行时异常,用于表示那些在程序运行时才能检测到的错误。
- 比如,除零错误、无效的函数参数等
- 它是
std::exception
的直接派生类- std::logic_error:逻辑错误,这类错误是在程序编写时就可以避免的。
- 比如,使用未初始化的变量、容器越界访问等
- 它同样派生自
std::exception
std::runtime_error
和std::logic_error
又各自有一些派生类,用于表示更具体的异常情况:
- std::runtime_error的常见派生类:
std::overflow_error
:表示算术溢出错误
- 例如:整数加法或乘法导致溢出时抛出
std::underflow_error
:表示算术下溢错误std::range_error
:表示计算结果超出了有意义的值域范围- std::logic_error的常见派生类:
std::invalid_argument
:当函数接收到无效的参数时抛出
- 比如:在进行字符串到数值的转换时,传入的字符串格式不正确
std::length_error
:当试图创建一个超出std::string
或其他标准容器最大长度的对象时抛出std::out_of_range
:用于表示访问容器时超出有效范围
- 比如:访问
std::vector
时使用了一个不存在的索引
// 标准异常类的层次结构std::exception
├── std::logic_error
│ ├── std::invalid_argument
│ ├── std::domain_error
│ ├── std::length_error
│ └── std::out_of_range
├── std::runtime_error
│ ├── std::range_error
│ ├── std::overflow_error
│ ├── std::underflow_error
│ └── std::system_error (C++11)
├── std::bad_alloc
├── std::bad_cast
└── std::bad_typeid
3. 怎么使用异常类?
一、“常用异常类”的使用示例
#include <iostream>
#include <string>
#include <stdexcept>int main()
{try {//1.模拟容器越界访问,会抛出std::out_of_range异常std::string str = "hello";char c = str.at(10);//2.模拟无效参数,会抛出std::invalid_argument异常int num = std::stoi("abc");//3.模拟算术溢出,会抛出std::overflow_error异常int a = 2147483647;int b = 1;int result = a + b;}catch (const std::out_of_range& e) {std::cout << "捕获到out_of_range异常: " << e.what() << std::endl;}catch (const std::invalid_argument& e) {std::cout << "捕获到invalid_argument异常: " << e.what() << std::endl;}catch (const std::overflow_error& e){std::cout << "捕获到overflow_error异常: " << e.what() << std::endl;}catch (const std::exception& e) {std::cout << "捕获到其他异常: " << e.what() << std::endl;}return 0;
}
二、“自定义异常类”的使用示例
除了使用标准异常类,开发者还可以根据实际需求自定义异常类。
通常会继承自
std::exception
或者它的派生类
,以便复用其接口和特性。
#include <iostream>
#include <exception>
#include <string>// 自定义异常类,继承自std::runtime_error
class MyCustomException : public std::runtime_error
{
public:MyCustomException(const std::string& msg) : std::runtime_error(msg) {}
};int main()
{try {// 抛出自定义异常throw MyCustomException("这是一个自定义的运行时异常");}catch (const MyCustomException& e) {std::cout << "捕获到自定义异常: " << e.what() << std::endl;}catch (const std::exception& e) {std::cout << "捕获到其他异常: " << e.what() << std::endl;}return 0;
}
4. 使用标准异常类有什么好处?
标准异常类的优势:
- 统一的异常处理方式:标准异常类提供了统一的接口和层次结构,方便开发者在不同的项目中采用一致的异常处理逻辑
- 提高代码可读性和可维护性:使用标准异常类能够清晰地表达程序中出现的错误类型,使代码的意图更加明确,后续维护时也更容易理解和处理
- 与标准库的无缝集成:标准库中的函数在遇到错误时会抛出相应的标准异常,熟悉标准异常类体系有助于更好地使用标准库,并处理其可能产生的错误
总之:C++ 的标准异常类体系为处理程序运行过程中的错误提供了强大且方便的工具,合理使用它们可以使程序更加健壮和可靠。