一、前言:为什么C++必须掌握异常机制?
到目前为止,我们学习了C++语法、面向对象、STL容器、STL算法、字符串处理,全部都是正常逻辑代码。
但在真实工程、后端服务、大型项目中,程序永远无法保证代码100%按预期执行:
-
动态内存开辟失败、内存耗尽
-
文件打开失败、路径不存在、权限不足
-
网络连接断开、读写超时、数据格式错误
-
数组越界、参数非法、空指针调用
如果没有异常处理,程序一旦出错直接崩溃退出、闪退、断连,极其不健壮。
C++异常机制核心作用:分离正常业务逻辑与错误处理逻辑,程序出错不崩溃、可捕获、可恢复、可定位!
彻底吃透C++异常全套体系,搞定工程容错、面试高频、内存避坑!
二、传统错误处理方式的致命弊端
在C语言和早期C++中,错误处理依赖 返回值、全局错误码,存在大量问题。
2.1 返回值判错(最常见)
cpp
bool openFile(string path)
{
FILE* fp = fopen(path.c_str(), "r");
if(fp == nullptr)
{
return false; // 错误返回false
}
// 正常逻辑
fclose(fp);
return true;
}
2.2 传统写法三大致命问题
-
代码臃肿:大量if判错逻辑,淹没核心业务代码
-
错误层级传递麻烦:底层错误需要层层return上传
-
极易遗漏:开发者经常不判断返回值,导致隐藏bug
-
无法携带错误信息:true/false只能知对错,不知错误原因、错误位置
异常机制完美解决以上所有问题:正常逻辑干净纯粹,错误统一捕获、统一处理、携带详细错误信息。
三、try-throw-catch 核心语法与执行流程
3.1 三大关键字作用
-
throw:主动抛出异常,可以抛出任意类型数据(int、string、自定义类)
-
try:包裹可能出错的风险代码块,监控异常
-
catch:捕获对应类型异常,执行错误恢复逻辑
3.2 最简可运行Demo
cpp
#include <iostream>
#include <string>
using namespace std;
void test(int a)
{
if(a == 0)
{
// 抛出异常
throw "除数不能为0!";
}
cout << 100 / a << endl;
}
int main()
{
try
{
// 风险代码
test(0);
}
catch(const char* err)
{
// 异常捕获处理
cout << "捕获异常:" << err << endl;
}
cout << "程序继续运行,不会崩溃" << endl;
return 0;
}
3.3 完整执行流程
-
执行try块内所有代码
-
遇到throw,立刻终止当前代码执行
-
跳出当前函数,逐层匹配对应类型的catch
-
匹配成功,执行catch异常处理逻辑
-
catch执行完毕,继续向后执行程序,不会直接退出
四、异常捕获匹配规则与多类型捕获
4.1 匹配规则(面试重点)
-
异常匹配严格匹配类型,不做隐式类型转换
-
多个catch按从上到下顺序匹配
-
通用捕获
catch(...)必须放在最后
4.2 多类型异常捕获实战
cpp
void func(int type)
{
if(type == 1) throw 100;
if(type == 2) throw "参数非法";
if(type == 3) throw 3.14;
}
int main()
{
try
{
func(2);
}
catch(int e)
{
cout << "捕获int异常:" << e << endl;
}
catch(const char* e)
{
cout << "捕获字符串异常:" << e << endl;
}
catch(...)
{
cout << "捕获未知类型异常" << endl;
}
return 0;
}
五、栈展开(Stack Unwinding)核心原理【难点】
5.1 什么是栈展开?
当内层函数抛出异常没有被当前函数捕获 时,会终止当前函数执行、销毁当前栈帧局部对象,逐层向上回溯,直到找到匹配的catch。
这个逐层销毁栈帧、回溯查找异常处理器 的过程,就是栈展开。
5.2 栈展开代码实战(直观理解)
cpp
#include <iostream>
using namespace std;
void func3()
{
cout << "func3 抛出异常" << endl;
throw "func3出错";
}
void func2()
{
cout << "func2 执行" << endl;
func3();
cout << "func2 后续代码不会执行" << endl;
}
void func1()
{
cout << "func1 执行" << endl;
func2();
}
int main()
{
try
{
func1();
}
catch(const char* err)
{
cout << "捕获异常:" << err << endl;
}
return 0;
}
核心结论 :异常抛出点之后的代码全部不再执行,逐层栈展开,直到被捕获。
5.3 栈展开最大作用
栈展开过程中,栈上局部对象会自动调用析构函数释放资源,一定程度避免内存泄漏。
六、C++标准异常体系
C++提供统一的标准异常基类 std::exception ,所有系统异常都继承此类,头文件 #include <stdexcept>。
6.1 常用标准异常类
-
exception:所有异常的基类
-
runtime_error:运行时错误(运行中触发)
-
logic_error:逻辑错误(代码逻辑问题)
-
out_of_range:下标越界异常
-
invalid_argument:参数非法异常
-
bad_alloc:内存开辟失败异常(new失败)
6.2 标准异常通用接口
所有标准异常都重写 what() 虚函数,返回异常描述信息。
6.3 标准异常实战代码
cpp
#include <iostream>
#include <stdexcept>
using namespace std;
int main()
{
try
{
// 模拟内存开辟失败
int* p = new int[100000000000];
}
catch(const bad_alloc& e)
{
cout << "内存分配失败:" << e.what() << endl;
}
catch(const exception& e)
{
cout << "通用异常:" << e.what() << endl;
}
return 0;
}
七、自定义异常类(工程标准写法)
工程开发中,不会直接抛字符串、数字,而是继承标准exception类,自定义异常,携带错误码、错误信息、错误位置。
7.1 完整自定义异常模板(可直接复用)
cpp
#include <iostream>
#include <stdexcept>
#include <string>
using namespace std;
// 自定义异常类,继承标准异常基类
class MyException : public exception
{
private:
string msg;
public:
MyException(string errMsg) : msg(errMsg) {}
// 重写what虚函数,返回错误信息
virtual const char* what() const noexcept override
{
return msg.c_str();
}
};
void login(string name, int pwd)
{
if(name.empty())
{
throw MyException("用户名不能为空!");
}
if(pwd < 6)
{
throw MyException("密码长度不足6位!");
}
}
int main()
{
try
{
login("", 123);
}
catch(const MyException& e)
{
cout << "业务异常:" << e.what() << endl;
}
catch(const exception& e)
{
cout << "系统异常:" << e.what() << endl;
}
return 0;
}
工程规范:自定义异常必须继承std::exception,统一异常体系,方便全局捕获、日志打印、错误统计。
八、异常接口声明 noexcept
8.1 旧版throw()声明(C++11废弃)
-
void func() throw():不会抛出任何异常 -
void func() throw(int):只抛出int类型异常
8.2 C++11 全新关键字 noexcept(重点)
noexcept :修饰函数,声明此函数不会抛出异常,编译器可优化代码、提升性能。
cpp
// 声明该函数绝对不会抛出异常
void printInfo() noexcept
{
cout << "无异常输出" << endl;
}
STL大量容器、移动构造、赋值函数都默认带noexcept,是现代C++性能优化重要手段。
九、异常与内存泄漏:致命坑点+解决方案
9.1 异常导致内存泄漏场景(高频踩坑)
堆内存开辟后、抛出异常前,未执行delete,栈展开只会释放栈内存,堆内存永久泄漏!
cpp
void leakTest()
{
// 堆内存开辟成功
int* p = new int[10];
// 中途抛出异常
throw "出错了!";
// 永远不会执行,内存泄漏
delete[] p;
}
9.2 两大解决方案(工程通用)
-
RAII机制:智能指针、容器管理内存(最优方案)
-
catch中手动释放资源:捕获异常后先释放资源再抛出
9.3 智能指针解决异常内存泄漏(推荐)
cpp
#include <memory>
void safeTest()
{
// 智能指针托管,异常栈展开自动析构释放
unique_ptr<int[]> p(new int[10]);
throw "异常触发";
// 无需手动delete,绝对不泄漏
}
十、工程异常编码规范
-
- 禁止抛基础类型(int/char*/double),统一抛自定义异常类
-
- 底层函数只抛异常不处理,上层业务统一捕获处理
-
- 禁止空catch、禁止吃掉异常,必须打印日志或恢复现场
-
- 资源申请后出现异常,必须保证资源正常释放(RAII优先)
-
- 不会出错的函数尽量加noexcept,开启编译器优化
-
- 捕获异常遵循先子类后父类顺序,精准捕获
十一、大厂面试真题问答
Q1 C++异常栈展开是什么?过程是什么?
异常未被当前栈帧捕获时,会逐层向上回溯,销毁每一层栈帧的局部对象、释放栈资源、终止后续代码执行,直到找到匹配的catch块,这个过程就是栈展开。栈展开能自动释放栈资源,但无法释放堆内存。
Q2 异常为什么会导致内存泄漏?怎么解决?
堆内存手动new后,中途抛出异常,delete代码无法执行,栈展开不处理堆内存,导致泄漏。解决方案:使用RAII机制(智能指针、STL容器)托管资源,无需手动释放。
Q3 catch(...)的作用和使用场景?
捕获所有未知类型异常,用于兜底保护,防止程序崩溃。必须放在所有catch最后,只用于全局异常拦截、日志记录、程序兜底。
Q4 noexcept关键字作用?
修饰函数无异常抛出,编译器可进行深度优化,STL大量移动构造、容器操作默认使用noexcept,提升程序运行效率。
Q5 传统错误码和异常机制对比?
错误码代码冗余、易遗漏、无详细信息、传递麻烦;异常分离正常逻辑与错误逻辑、携带详细错误信息、层级传递简单、程序可容错不崩溃,适合大型工程。
十二、今日总结
彻底通关C++异常处理全套体系:
✅ 传统错误处理弊端与异常机制核心优势
✅ try-throw-catch完整执行流程、多类型捕获规则
✅ 栈展开Stack Unwinding底层原理与代码实战
✅ C++标准异常体系、内置异常类、what()接口
✅ 工程级自定义异常类标准写法(可直接复用)
✅ noexcept无异常声明、C++11新特性优化
✅ 异常导致内存泄漏的根源与RAII解决方案
✅ 企业级异常编码规范、避坑准则
✅ 面试高频真题完整复盘