C++ 异常处理:理论、栈展开与最佳实践
C++ 异常处理机制允许程序在运行时检测并处理错误,将问题检测与解决分离。核心包括抛出(throw)与捕获(catch)、栈展开(Stack Unwinding)过程、异常匹配规则及重新抛出。异常安全涉及资源泄漏风险,推荐使用 RAII 和智能指针。C++11 引入 noexcept 简化异常规范。标准库提供基于 std::exception 的继承体系。通过自定义异常类和多态捕获可构建健壮的错误处理架构。

C++ 异常处理机制允许程序在运行时检测并处理错误,将问题检测与解决分离。核心包括抛出(throw)与捕获(catch)、栈展开(Stack Unwinding)过程、异常匹配规则及重新抛出。异常安全涉及资源泄漏风险,推荐使用 RAII 和智能指针。C++11 引入 noexcept 简化异常规范。标准库提供基于 std::exception 的继承体系。通过自定义异常类和多态捕获可构建健壮的错误处理架构。

看库文件(非官方文档): Cplusplus.com
这个文档在 C++98、C++11 时候还行,之后就完全没法用了……
准官方文档(同步更新)——还 可以看语法:C++准官方参考文档
这个行,包括 C++26 都同步了,我们以后主要会看这个。
官方文档(类似论坛): Standard C++
这个网站上面会有很多大佬,类似于论坛。
在 C 语言里面,异常的处理机制——通过错误码的形式处理错误,比较麻烦。异常处理机制允许程序中独立开发的部分能够在运行时就出现的问题进行通信并做出相应的处理,异常使得我们能够将问题的检测与解决问题的过程分开,程序的一部分负责检测问题的出现,然后解决问题的任务传递给程序的另一部分,检测环节无须知道问题的处理模块的所有细节。
C 语言主要通过错误码的形式处理错误,错误码本质就是对错误信息进行分类编号,拿到错误码以后还要去查询错误信息,比较麻烦。异常时抛出一个对象(可以抛出任何类型的异常),这个对象可以函数更全面的各种信息(包含各种各样的信息)。
程序出现问题时,我们通过抛出(throw)一个对象来引发一个异常,该对象的类型以及当前的调用链决定了应该由哪个 catch 的处理代码来处理该异常。
被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。根据抛出对象的类型和内容,程序的抛出异常部分告知异常处理部分到底发生了什么错误。
当 throw 执行时,throw 后面的语句将不再被执行。程序的执行从 throw 位置跳到与之匹配的 catch 模块,catch 可能是同一函数中的一个局部的 catch,也可能是调用链中另一个函数中的 catch,控制权从 throw 位置转移到了 catch 位置。
抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个局部对象,所以会生成一个拷贝对象,这个拷贝的对象会在 catch 子句后销毁(这里的处理类似于函数的传值返回)。
抛出异常后,程序暂停当前函数的执行,开始寻找与之匹配的 catch 子句,首先检查 throw 本身是否在 try 块内部,如果在则查找匹配的 catch 语句,如果有匹配的,则跳到 catch 的地方进行处理。
如果当前函数中没有 try/catch 子句,或者有 try/catch 子句但是类型不匹配,则退出当前函数,继续在外层调用函数链中查找,上述查找的 catch 过程被称为栈展开。
如果到达 main 函数,依旧没有找到匹配的 catch 子句,程序会调用标准库的 terminate 函数终止程序。
如果找到匹配的 catch 子句处理后,catch 子句后面的代码会继续执行。
注意:e.what(); 返回包含异常的字符串。
上图中,[ LINE ] 的作用:获取你是哪个位置的宏。
一般情况下抛出对象和 catch 是类型完全匹配的,如果有多个类型匹配的,就选择离它位置更近的那个(类型匹配时遵循'就近原则')。
但是也有一些例外,允许从非常量向常量的类型转换,也就是权限缩小;允许数组转换成指向数组元素类型的指针,函数被转换成指向函数的指针;允许从派生类向基类类型的转换,这个点非常实用,实际中继承体系基本都是用这个方式设计的。
如果到 main 函数,异常仍旧没有被匹配就会终止程序,不是发生严重错误的情况下,我们是不期望程序终止的,所以一般 main 函数中最后都会使用 catch(…),它可以捕获任意类型的异常,但是我们不知道异常错误是什么。
有时 catch 到一个异常对象后,需要对错误进行分类,其中的某种异常错误需要进行特殊的处理,其他错误则重新抛出异常给外层调用链处理。捕获异常后需要重新抛出,直接 throw;就可以把捕获的对象直接拋出。
异常抛出后,后面的代码就不再执行,前面申请了资源(内存、锁等),后面进行释放,但是中间可能会抛异常就会导致资源没有释放,这里由于异常就引发了资源泄漏,产生安全性的问题。中间我们需要捕获异常,释放资源后面再重新抛出,当然后面智能指针章节讲的 RAII 方式解决这种问题是更好的。
其次析构函数中,如果抛出异常也要谨慎处理,比如析构函数要释放 10 个资源,释放到第 5 个时抛出异常,则也需要捕获处理,否则后面的 5 个资源就没释放,也资源泄漏了。《EffectiveC++》中的第 8 个条款也专门讲了这个问题,别让异常逃离析构函数。
对于用户和编译器而言,预先知道某个程序会不会抛出异常大有裨益,知道某个函数是否会抛出异常有助于简化调用函数的代码。
C++98 中函数参数列表的后面接 throw),表示函数不抛异常,函数参数列表的后面接 throw(类型 1,类型 2…) 表示可能会抛出多种类型的异常,可能会抛出的类型用逗号分割。
C++98 的方式这种方式过于复杂,实践中并不好用,C++11 中进行了简化,函数参数列表后面加 noexcept 表示不会抛出异常,啥都不加表示可能会抛出异常。
编译器并不会在编译时检查 noekcept,也就是说如果一个函数用 noexcept 修饰了,但是同时又包含了 throw 语句或者调用的函数可能会抛出异常,编译器还是会顺利编译通过的(有些编译器可能会报个警告)。但是一个声明了 noexcept 的函数抛出了异常,程序会调用 terminate 终止程序。
noexcept(expression) 还可以作为一个运算符去检测一个表达式是否会抛出异常,可能会则返回 false,不会就返回 true。
标准库链接: std::exception
C++ 标准库也定义了一套自己的一套异常继承体系库,基类是 exception,其它的都是它的派生类,所以我们日常写程序,需要在主函数捕获 exception 即可,要获取异常信息,调用 what 函数,what 是一个虚函数,派生类可以重写。
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <exception>
#include <thread>
#include <chrono>
using namespace std;
// 延时功能
// 一般大型项目才会使用异常,下面我们模拟设计一个服务的几个模块
// 每个模块的继承都是 Exception 的派生类,每个模块可以添加自己的数据
// 最后捕获时,我们捕获基类即可
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 HttpSever() {
if (rand() % 3 == 0) {
throw HttpException("申请资源不存在", 100, "get");
} else if (rand() % 4 == 0) {
throw HttpException("权限不足", 101, "post");
} else {
cout << "HttpSever 调用成功" << endl;
}
CacheMgr();
}
void _Sending(const string& s) {
if (rand() % 2 == 0) {
throw HttpException("网络不稳定,发送失败", 102, "put");
} else if (rand() % 4 == 0) {
throw HttpException("你已经不是对象的好友,发送失败", 103, "put");
} else {
cout << "发送成功" << endl;
}
}
// 网络不稳定,要求重试 3 次,均失败
void SendMsg(const string& s) {
for (size_t i = 0; i < 4; i++) {
try {
_Sending(s); // 走到这里代表成功了,跳出循环
break;
} catch (const Exception& e) {
if (e.getid() == 102) {
if (i == 3) throw;
cout << "开始第" << i + 1 << "重试" << endl;
} else {
// 重新抛出异常
throw;
}
}
}
}
double Divide(int a, int b) noexcept {
// 当 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;
try {
cout << Divide(len, time) << endl;
} catch (...) {
cout << "delete []" << array << endl;
delete[] array;
// 重新抛出,捕获到什么就抛出什么
throw;
}
cout << "delete[]" << array << endl;
delete[] array;
}
int main() {
srand(time(0));
while (1) {
this_thread::sleep_for(chrono::seconds(1)); // 延迟 1 秒
try {
HttpSever();
} catch (const Exception& e) { // 这里捕获基类,基类对象和派生类对象都可以被捕获
// 多态调用
cout << e.what() << endl;
} catch (...) { // 任意类型的对象
cout << "Unknown Exception" << endl;
}
}
return 0;
}
输出结果示例:
HttpSever 调用成功
CacheMgr 调用成功
SQLMgr 调用成功
...
HttpException:get : 申请资源不存在
...
void SendMsg(const string& s) { for (size_t i = 0; i < 4; i++) { try { _Sending(s); break; } catch (const Exception& e) { if (e.getid() == 102) { if (i == 3) throw; cout << "开始第" << i + 1 << "重试" << endl; } else { throw; } } } }
double Divide(int a, int b) noexcept { if (b == 0) { throw "Division by zero condition!"; } return (double)a / (double)b; }
void Func() { int* array = new int[10]; int len, time; try { cout << Divide(len, time) << endl; } catch (...) { cout << "delete []" << array << endl; delete[] array; throw; } cout << "delete[]" << array << endl; delete[] array; }
int main() { int i = 0; cout << noexcept(Divide(1, 2)) << endl; cout << noexcept(Divide(1, 0)) << endl; cout << noexcept(Func()) << endl; cout << noexcept(++i) << endl; return 0; }
// 输出结果: // 0 // 0 // 0 // 1

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online