
目录
[C 错误码 vs C++ 异常](#C 错误码 vs C++ 异常)
[1.1 异常核心定义](#1.1 异常核心定义)
[1.2 C 语言错误处理:错误码](#1.2 C 语言错误处理:错误码)
[1.3 C++ 异常优势](#1.3 C++ 异常优势)
[二、异常抛出 throw 与捕获 try-catch(基础语法 + 底层原理)](#二、异常抛出 throw 与捕获 try-catch(基础语法 + 底层原理))
[2.1 throw 抛出规则](#2.1 throw 抛出规则)
[1.throw 执行后,throw 后面所有代码永久不再执行,程序直接跳转匹配 catch](#1.throw 执行后,throw 后面所有代码永久不再执行,程序直接跳转匹配 catch)
[3. 编译器自动拷贝一份,放到全局异常缓冲区](#3. 编译器自动拷贝一份,放到全局异常缓冲区)
[4. catch 执行完,拷贝的副本自动销毁](#4. catch 执行完,拷贝的副本自动销毁)
[2.2 try-catch 完整语法结构](#2.2 try-catch 完整语法结构)
[三、重难点:栈展开(Stack Unwinding)分步详解](#三、重难点:栈展开(Stack Unwinding)分步详解)
[3.1 栈展开定义](#3.1 栈展开定义)
[3.2 标准执行步骤(固定流程)](#3.2 标准执行步骤(固定流程))
[3.3 栈展开两大关键副作用](#3.3 栈展开两大关键副作用)
[3.4 无匹配 catch 的后果](#3.4 无匹配 catch 的后果)
[四、异常匹配规则 + 自定义业务异常体系实战](#四、异常匹配规则 + 自定义业务异常体系实战)
[4.1 基础匹配规则](#4.1 基础匹配规则)
[4.2 允许隐式转换的 4 种特殊场景(面试高频)](#4.2 允许隐式转换的 4 种特殊场景(面试高频))
[4.3 多 catch 匹配优先级](#4.3 多 catch 匹配优先级)
[4.4 大型项目自定义异常完整代码(文档示例)](#4.4 大型项目自定义异常完整代码(文档示例))
[4.4.1 统一异常基类](#4.4.1 统一异常基类)
[4.4.2 各业务模块派生异常](#4.4.2 各业务模块派生异常)
[4.4.3 业务函数随机抛出对应异常](#4.4.3 业务函数随机抛出对应异常)
[4.4.4 main 捕获基类统一处理(多态)](#4.4.4 main 捕获基类统一处理(多态))
[4.4.5 设计思路分步讲解](#4.4.5 设计思路分步讲解)
[五、异常重新抛出 throw; 原理、场景、代码拆解](#五、异常重新抛出 throw; 原理、场景、代码拆解)
[5.1 使用场景](#5.1 使用场景)
[5.2 两种重抛语法核心区别(高频坑点)](#5.2 两种重抛语法核心区别(高频坑点))
[5.3 文档实战案例:消息发送重试逻辑分步拆解](#5.3 文档实战案例:消息发送重试逻辑分步拆解)
[6.1 问题根源](#6.1 问题根源)
[6.2 问题示例(文档代码)](#6.2 问题示例(文档代码))
[6.3 手动处理方案步骤](#6.3 手动处理方案步骤)
[6.4 最优工业方案:RAII(文档提及重点)](#6.4 最优工业方案:RAII(文档提及重点))
[6.5 析构函数抛异常禁忌(Effective C++ 条款 8)](#6.5 析构函数抛异常禁忌(Effective C++ 条款 8))
[七、异常规范:C++98 throw 与 C++11 noexcept](#七、异常规范:C++98 throw 与 C++11 noexcept)
[7.1 C++98 老式异常规范(已淘汰)](#7.1 C++98 老式异常规范(已淘汰))
[7.2 C++11 noexcept(主流标准)](#7.2 C++11 noexcept(主流标准))
[7.2.1 修饰函数 noexcept编辑](#7.2.1 修饰函数 noexcept编辑)
[7.2.2 运算符 noexcept(表达式)](#7.2.2 运算符 noexcept(表达式))
[7.2.3 使用场景](#7.2.3 使用场景)
[八、C++ 标准库内置异常继承体系](#八、C++ 标准库内置异常继承体系)
[8.1 顶层基类 std::exception](#8.1 顶层基类 std::exception)
[8.2 两大分支分类](#8.2 两大分支分类)
[8.3 开发规范](#8.3 开发规范)
[九、全文总结 + 面试高频考点](#九、全文总结 + 面试高频考点)
[9.1 核心知识点总结](#9.1 核心知识点总结)
一、什么是异常?
C 错误码 vs C++ 异常
1.1 异常核心定义
异常机制作用:将错误检测代码 和 错误处理代码分离
- 一段函数只负责识别运行时错误,抛出异常对象
- 上层调用链函数负责处理错误,底层函数不需要关心上层如何处理
底层只管报错,上层负责收拾烂摊子
分层理解:
- 底层函数 :只做一件事 ------ 判断有没有出错,出错直接
throw抛出异常,完全不用管报错之后要怎么做。 不用打印日志、不用重试、不用弹窗提示、不用返回特殊错误码,报错流程全部丢给调用它的上层- 上层函数 :调用底层代码,用
try-catch接住异常,在这里统一写错误处理逻辑
- 独立模块之间通过异常对象传递错误信息,解耦代码
1.2 C 语言错误处理:错误码
- 实现方式:函数出错返回数字编号,开发者拿到编号后查表获取错误描述;
- 缺点:
- 每次调用函数必须判断返回值,大量冗余 if 判断
- 错误仅数字,无法携带上下文(出错位置、SQL、请求类型等)
- 极易忘记判断返回值,漏处理错误,埋下线上隐患
1.3 C++ 异常优势
抛出的是对象,可以自定义类,封装错误信息、错误码、附加业务数据;信息完整,支持多态统一处理
二、异常抛出 throw 与捕获 try-catch(基础语法 + 底层原理)
2.1 throw 抛出规则
语法:throw 异常对象;
核心规则分步拆解
1. throw 执行后,throw 后面所有代码永久不再执行,程序直接跳转匹配 catch
2. 抛出局部对象会生成拷贝:
①抛出的对象如果是函数内局部变量,函数栈销毁后对象会失效
抛出局部对象的致命问题:函数栈销毁,局部变量直接失效
基础栈知识铺垫:
函数里定义的变量(string err("出错了");)存在函数栈帧上。 函数执行完 / 异常触发栈展开时,整个栈帧会被销毁,栈上所有局部变量立刻调用析构、内存回收,变量变成野内存
举个反例场景:

如果编译器不做拷贝会发生什么:
throw err执行,开始栈展开,销毁 test 函数栈帧- 局部变量
err被析构、内存释放 - 上层 catch 拿到的是一块已经回收的无效内存,读取会乱码、程序崩溃
这就是第一条说的:局部变量所在函数销毁后,原对象直接失效,没法给上层用
3. 编译器自动拷贝一份,放到全局异常缓冲区
为了解决上面的野内存问题,C++ 标准强制规定: 执行throw xxx时,编译器会调用拷贝构造函数,把你抛出的对象完整复制一份,存到一块独立、全局生命周期的内存(全局异常缓冲区)。
上面代码的真实执行流程:
⑴string err("除零错误"); 栈上创建局部 err
⑵throw err; → 编译器自动执行 string 全局副本 = err; 拷贝
⑶全局副本放在堆 / 全局缓冲区,不受函数栈销毁影响
⑷原局部 err 随 test 栈帧销毁,但是全局副本完好保留
不管中间多少层函数、栈怎么销毁,这份拷贝一直存在,直到 catch 处理
**4.**catch 执行完,拷贝的副本自动销毁
全局缓冲区里的副本不会永久占内存:
- 当
catch{}大括号内所有代码全部执行完毕,离开 catch 作用域; - 编译器自动调用副本的析构函数,释放全局缓冲区内存;
- 括号里备注「类似函数传值返回」: 函数
string func(){return s;}也是创建临时拷贝返回,临时对象用完自动销毁,逻辑一模一样。
②编译器自动生成异常对象拷贝,存放在全局异常缓冲区;
③catch 执行完毕后,拷贝的异常对象自动销毁(类似函数传值返回);
3.控制权跳转:从 throw 直接跳到匹配 catch,中间函数提前退出,栈上所有局部对象自动析构
2.2 try-catch 完整语法结构

规则:多个 catch 从上到下依次匹配,先匹配到优先执行
2.3除法异常案例
cpp
#include <iostream>
#include <string>
using namespace std;
double Divide(int a, int b) {
try {
if (b == 0) {
string s("Divide by zero condition!");
throw s; // 抛出string类型异常对象
} else {
return ((double)a / (double)b);
}
}
catch (int errid) { // 仅捕获int,和string不匹配
cout << errid << endl;
}
return 0;
}
void Func() {
int len, time;
cin >> len >> time;
try {
cout << Divide(len, time) << endl;
}
catch (const char* errmsg) { // 仅捕获char*,和string不匹配
cout << errmsg << endl;
}
}
int main()
{
while (1)
{
try
{
Func();
}
catch (const string& errmsg) // 匹配string异常
{
cout << errmsg << endl;
}
}
return 0;
}
触发除零异常(b=0)执行分步流程
- Divide 函数内部执行
throw string - 检索当前函数 catch:
catch(int)类型不匹配,跳过 - 当前函数无匹配处理器,触发栈展开,退出 Divide 栈帧
- 回到上层 Func 函数的 try 块,检索
catch(const char*) - string 和 const char * 类型不匹配,再次栈展开,退出 Func 栈帧
- 回到 main 函数 try 块,匹配
catch(const string&) - 进入 catch 代码块打印错误信息,异常处理完成
三、重难点:栈展开(Stack Unwinding)分步详解
3.1 栈展开定义
抛出异常后,逐层销毁当前函数栈帧,向上遍历整个函数调用链,寻找匹配 catch 处理器的全过程,称为栈展开。
3.2 标准执行步骤(固定流程)
假设调用链:main() → func3() → func2() → func1(),异常在 func1 抛出
- 判断 throw 是否位于
try{}代码块内; - 遍历当前函数所有 catch 分支,判断是否存在类型匹配:
- 存在匹配:执行对应 catch,栈展开终止;
- 无匹配:销毁当前函数所有局部对象,退出当前栈帧;
- 回到上层调用函数,重复步骤 1、2;
- 持续向上遍历,直到 main 函数;
- main 函数仍无匹配 catch:调用标准库
terminate(),程序直接强制终止
3.3 栈展开两大关键副作用
- 抛出点之后代码全部放弃执行,函数提前退出
- 每层栈帧内所有局部对象自动调用析构函数释放资源
3.4 无匹配 catch 的后果
异常一路栈展开到 main 仍找不到对应处理器 → 程序崩溃。 生产环境规范:main 函数末尾必须增加catch(...)兜底捕获
四、异常匹配规则 + 自定义业务异常体系实战
4.1 基础匹配规则
默认严格完全类型匹配 ,不会发生隐式转换。 例:throw string 无法被catch(char*)、catch(int)捕获
4.2 允许隐式转换的 4 种特殊场景(面试高频)
- 非常量 → 常量:
throw string匹配catch(const string&)(权限缩小,安全); - 数组 → 数组元素指针:
char buf[10]抛出,可被catch(const char*)捕获; - 函数类型 → 函数指针;
- 派生类 → 基类(项目最常用) 业务统一异常基类,所有模块异常继承基类;捕获基类引用即可统一处理全部派生异常,配合虚函数实现多态打印错误
4.3 多 catch 匹配优先级
多个 catch 均可匹配异常时,离 throw 位置更近、代码书写靠前的 catch 优先执行。 避坑:捕获基类的 catch 必须写在所有派生类 catch 后面,否则派生异常会被基类提前拦截
4.4 大型项目自定义异常完整代码(文档示例)
4.4.1 统一异常基类

1.成员变量(protected 保护)

2.构造函数(public)

- 参数 1
const string& errmsg:异常文字说明,const&避免字符串拷贝,提升性能 - 参数 2
int id:自定义错误编号 - 初始化列表
:_errmsg(errmsg), _id(id):优先初始化成员变量,比函数体内赋值效率更高 - 作用:抛出异常时
throw Exception("联机WebSocket断开", 1001);快速创建异常对象
3.虚函数 virtual string what() const

virtual虚函数 :核心设计点 ------ 支持多态重写 所有子类异常(比如RoomException、CardParseException)可以重写what(),追加专属信息(如房间 ID、卡牌 ID)const:调用该函数不会修改类内成员变量,const 对象也能调用- 功能:返回异常文字描述,对标 C++ 标准库
std::exception的what()接口,统一使用习惯
4. 获取错误码接口 getid()

外部捕获异常后,用来拿到数字错误码,做分支处理:

4.4.2 各业务模块派生异常
cpp
// SQL模块异常
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;
}
};
// HTTP服务异常
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;
};
4.4.3 业务函数随机抛出对应异常
cpp
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();
}
4.4.4 main 捕获基类统一处理(多态)
cpp
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.4.5 设计思路分步讲解
- 基类
Exception封装通用错误信息、错误码,虚函数what()预留重写接口 - 每个业务模块派生专属异常,扩展模块独有字段(SQL 语句、请求方式)
- 重写
what()拼接模块专属报错信息,区分错误来源 - 底层业务函数根据场景抛出对应派生异常
- 上层仅捕获基类引用,利用多态自动调用子类
what(),一行代码处理全部业务异常
五、异常重新抛出 throw; 原理、场景、代码拆解
5.1 使用场景
捕获异常后,局部做简单处理(重试、日志打印),但当前层级无法完全修复错误,需要将原始异常向上传递给外层调用者
5.2 两种重抛语法核心区别(高频坑点)
throw;(推荐):不带参数,原样抛出捕获到的原始异常对象,保留派生类完整信息,不会发生对象切片throw e;(禁止):拷贝生成新对象,若 e 是基类引用,会发生对象切片,丢失子类独有数据,多态失效
前置铺垫:先搞懂场景
我们自定义一套异常继承体系: 基类
Exception,派生类SqlException(子类独有字段:SQL 语句) 业务场景:底层抛出子类异常 → 中间层 catch 用基类引用接住,打印日志后想向上重抛给顶层处理
1. throw; 无参重抛(推荐,不会切片)
原理
不带参数的
throw;会直接复用全局缓冲区里原始抛出的完整异常对象,不会拷贝新对象,完整保留子类全部信息
完整测试代码

执行结果

关键点
throw;不产生新拷贝,直接传递全局缓冲区里完整子类对象;- 基类引用绑定的子类信息完全保留,多态正常生效;
- 上层可以正常捕获派生类、访问子类独有成员,无信息丢失。
2. throw e; 带参数重抛(禁止,会发生对象切片)
什么是对象切片?
e是基类引用const Exception& e,执行throw e;时,编译器会用基类拷贝构造函数,只复制基类部分成员,直接砍掉子类独有的全部数据 ,生成一个纯粹的基类临时对象。 子类独有的字段、重写虚函数全部丢失,多态失效,这就叫对象切片。
改上面代码,把中间层重抛换成 throw e
执行结果

出现的严重问题
- 子类独有的
sql字段直接被切掉、彻底丢失 - 虚函数不再走子类重写版本,多态失效
- 上层无法捕获派生类异常,只能捕获残缺基类,丢失关键业务信息
- 额外多一次对象拷贝,性能变差

5.3 文档实战案例:消息发送重试逻辑分步拆解
需求:网络波动(错误 id=102)最多重试 3 次;其他错误直接抛出给上层打印
cpp
void _SeedMsg(const string& s)
{
if(rand()%2==0)
{
throw HttpException("网络不稳定,发送失败",102,"put");
}
else if(rand()%7==0)
{
throw HttpException("你已经不是对象的好友,发送失败",103,"put");
}
else
{
cout<<"发送成功"<<endl;
}
}
void SendMsg(const string& s)
{
// 最多尝试4次(0~3)
for (size_t i = 0; i < 4; i++)
{
try
{
_SeedMsg(s);
break; // 发送成功跳出循环
}
catch (const Exception& e)
{
if (e.getid() == 102) // 网络波动,允许重试
{
if (i == 3) // 重试耗尽,重抛异常交给上层
throw;
cout << "开始第" << i + 1 << "重试" << endl;
}
else // 非网络错误,直接重抛,不重试
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 次调用发送接口
_SeedMsg - 抛出异常进入 catch,判断错误 id:
- id=102(网络不稳):未到第 3 次打印重试;i=3 时
throw;重抛 - id=103(非好友):直接
throw;,不重试
- id=102(网络不稳):未到第 3 次打印重试;i=3 时
- main 捕获重抛的原始异常,打印完整错误信息
六、异常安全:资源泄漏问题与解决方案
6.1 问题根源
throw 会中断代码执行;若在new、锁、文件打开后、资源释放前抛出异常,释放代码不会执行,造成内存 / 句柄 / 锁泄漏。
6.2 问题示例(文档代码)
cpp
double Divide(int a, int b)
{
if (b == 0)
{
throw "Division by zero condition!";
}
return (double)a / (double)b;
}
void Func()
{
int* array = new int[10]; // 堆内存申请
try
{
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
}
catch (...)
{
// 异常分支手动释放内存
cout << "delete []" << array << endl;
delete[] array;
throw; // 重抛异常,交给上层处理
}
// 正常流程释放内存
cout << "delete []" << array << endl;
delete[] array;
}
问题拆解
new int[10]申请堆内存- Divide 触发除零异常,直接跳转到 catch
- 如果不写 catch 内 delete,函数末尾
delete[]永远不会执行,内存泄漏
6.3 手动处理方案步骤
- 资源申请后全部业务代码放入 try 块
catch(...)中手动释放所有已申请资源- 使用
throw;重抛异常,上层继续处理错误 - try 正常走完,函数末尾正常释放资源
6.4 最优工业方案:RAII(文档提及重点)
手动释放代码冗余、极易遗漏,RAII 资源获取即初始化彻底解决泄漏:
- 资源封装到类,构造函数申请资源,析构函数释放资源
- 栈展开时局部对象自动析构,无论是否抛异常,资源一定释放
- 常用:
unique_ptr/shared_ptr智能指针、std::lock_guard锁守卫
6.5 析构函数抛异常禁忌(Effective C++ 条款 8)
- 风险:析构执行时如果正在栈展开,此时再抛出异常会存在两个活跃异常
- C++ 标准行为:直接调用
terminate()终止程序 - 规范:析构函数内部必须加 try-catch,就地处理所有异常,绝不向外抛出
七、异常规范:C++98 throw 与 C++11 noexcept
7.1 C++98 老式异常规范(已淘汰)
void func() throw();:声明函数不会抛出任何异常void func() throw(bad_alloc);:声明仅会抛出 bad_alloc 类型异常; 缺点:语法繁琐、编译器不强制校验,工程几乎不使用
7.2 C++11 noexcept(主流标准)
7.2.1 修饰函数 noexcept

规则分步拆解:
- 含义:开发者承诺该函数不会抛出异常
- 编译行为:编译器不做强制校验,函数内部 throw 只会报警告,编译通过
- 运行行为:若 noexcept 修饰函数实际抛出异常,程序直接调用
terminate()崩溃
7.2.2 运算符 noexcept(表达式)
编译期判断表达式是否可能抛出异常:
- 表达式不会抛异常 → 返回 true (1);
- 表达式可能抛异常 → 返回 false (0); 示例:

7.2.3 使用场景
标准库容器大量使用 noexcept,告知编译器函数无异常,编译器可做性能优化
八、C++ 标准库内置异常继承体系
8.1 顶层基类 std::exception
所有标准异常的父类,核心虚函数:

作用:返回异常描述字符串,派生类可重写自定义信息
8.2 两大分支分类
- std::logic_error 逻辑错误(代码编写错误,可提前规避)
- invalid_argument:非法参数
- length_error:容器长度超限
- out_of_range:数组 / 容器下标越界
- domain_error:数学定义域错误
- std::runtime_error 运行时错误(环境导致,无法提前预判)
- bad_alloc:new 分配内存失败
- bad_cast:dynamic_cast 转型失败
- bad_typeid:空指针调用 typeid
- overflow_error/underflow_error:数值溢出 / 下溢
- range_error:数值区间错误
8.3 开发规范
main 函数外层捕获catch(const std::exception& e)拦截全部标准库异常,搭配catch(...)兜底未知异常
九、全文总结 + 面试高频考点
9.1 核心知识点总结
- 异常分离错误检测与处理,对比 C 错误码携带完整对象信息
- throw 中断后续代码,抛出对象生成拷贝,栈展开逐层销毁栈帧查找 catch
- 异常匹配默认严格类型匹配,派生类转基类是项目自定义异常核心设计
throw;无参重抛保留原始异常,throw e会切片- 异常中断执行易造成资源泄漏,RAII 是最优解决方案,析构禁止抛异常
- C++11 noexcept 声明无异常函数,运行抛异常直接 terminate
- 标准库异常以 std::exception 为基类,分 logic_error 和 runtime_error 两大分支


