【C + +】异常处理:深度解析与实战
🌟个人主页:第七序章
🌈专栏系列:C++
目录
❄️前言:
☀️一、C 语言中传统的错误处理方式
☀️二、用“外卖点餐”来理解错误处理
🌙2.1 情况一 | 客户端错误
🌙2.2 情况二 | 程序自身的问题
🌙2.3 情况三 | 环境问题
☀️三、C++异常处理简介
🌙3.1 三个关键字:try / throw / catch
☀️四、异常的使用
🌙4.1 异常的抛出与匹配规则
⭐4.1.1 throw可以抛出任意类型的对象
⭐4.1.2 异常处理的“就近原则”
⭐4.1.3 找不到匹配的 catch
⭐4.1.4 异常对象的拷贝机制
⭐4.1.5 catch(…) 与未捕获异常的处理
🌙4.2 在函数调用链中异常栈展开匹配原则
🌙4.3 异常的重新抛出
🌙4.4 异常安全
⭐4.4.1 抛异常出现内存泄漏
⭐4.4.2 异常安全的基本原则
🌙4.5 异常规范(Exception Specification)
☀️五、自定义异常体系
☀️六、C++标准库的异常体系
🌙6.1 std::exception异常继承实操
☀️七、 C++ 异常的优缺点
🌙7.1 异常优点
🌙7.2 异常缺点
🌻共勉:
❄️前言:
上一篇我们学习了AVL树的平衡机制与实现,今天我们来学习一下C + +的异常处理。
☀️一、C 语言中传统的错误处理方式
终止程序(如 assert)
优点:适用于开发阶段快速发现严重错误。
缺点:用户体验差,程序在运行时一旦遇到严重错误(如内存访问违规、除以零),会立即终止,难以接受。
返回错误码(如通过 errno)
优点:灵活,允许程序继续运行,适合错误可恢复的场景。如系统的很多库的接口函数都是通过把错误码放到errno中,表示错误。
缺点:程序员需手动检查返回值并查找错误码含义,增加了开发复杂度。
在实际开发中,C 语言主要采用返回错误码的方式进行错误处理。对于一些致命错误(如数组越界),虽然属于运行时行为,但如果被编译器静态检查到,往往会强制终止程序以避免更严重的问题。
☀️二、用“外卖点餐”来理解错误处理
想象你正在用手机点外卖,点的是一份奶茶。整个点餐的过程就像一个运行中的程序,每一个操作(比如选择口味、下单、付款)都可能出错。如果出错了就直接关闭整个APP,那你肯定会觉得这个软件太差劲了,对吧?
所以我们来看看,具体会遇到哪些“错误”,程序应该怎么合理地处理它们。
🌙2.1 情况一 | 客户端错误
【场景】:余额不足,付款失败
你下单准备付款,但微信钱包里没钱了。如果这时程序直接崩溃或退出,那你可能连点别的奶茶的机会都没有了,非常不合理。
【 正确做法】:提示“余额不足”,引导你去充值或者换付款方式。 这个错误是可以预料并处理的客户端错误
🌙2.2 情况二 | 程序自身的问题
【场景】:点击“支付”按钮没有反应
你点了“支付”,但是页面没动。这时候可能是程序写得有问题,按钮绑定错了,或者后端接口挂了。你作为用户也许看不懂程序错误日志,但开发者需要知道这里出问题了。
【正确做法】:程序不崩溃,但把这个错误悄悄记录到日志里,方便程序员以后排查。
🌙2.3 情况三 | 环境问题
【场景】::网络不好,订单没发出去
你在地铁里网不好,付款的时候一直转圈圈。程序应该不会立刻提示“失败”,而是先尝试重新连接几次,实在不行了,再告诉你“网络连接失败,请稍后重试”。
【正确做法】:设置重试机制,允许等待→重连→最终提示失败
这样的流程。属于环境导致的问题,可以尝试恢复处理
一个健壮的程序,应该像一个优秀的服务员,面对突发状况不会慌张,而是冷静地根据情况选择恰当的应对策略。
☀️三、C++异常处理简介
异常(Exception)是一种用于处理程序运行中出现错误的机制。当一个函数在执行过程中发现自己无法处理的问题时,它可以通过抛出异常来将错误交由其调用者(直接或间接)处理。
🌙3.1 三个关键字:try / throw / catch
【throw | 抛出异常】
当程序检测到某个问题无法继续执行时,可以使用 throw
语句将异常抛出。
可以抛出任何类型的异常对象(如整数、字符串、自定义类等)。
【try | 捕获尝试】
将可能抛出异常的代码块包裹在 try
块中。如果在该块中抛出了异常,程序会跳转到匹配的 catch
块执行。
【catch | 捕获处理】
用于处理 try
块中抛出的异常。可以定义多个 catch
块,分别捕获不同类型的异常。
如果没有匹配的
catch
块,异常将继续向上传递,直到被捕获或导致程序终止。
try
{// 受保护的代码:这里放可能抛出异常的语句
}
catch (const ExceptionType1& e) // ← 捕获第 1 类异常
{// TODO: 针对 ExceptionType1 的处理
}
catch (const ExceptionType2& e) // ← 捕获第 2 类异常
{// TODO: 针对 ExceptionType2 的处理
}
// ...
catch (const ExceptionTypeN& e) // ← 捕获第 N 类异常
{// TODO: 针对 ExceptionTypeN 的处理
}
/* 可选:兜底捕获,防止漏网之鱼
catch (...)
{// TODO: 处理所有未被前面 catch 捕获的异常
}
*/
☀️四、异常的使用
🌙4.1 异常的抛出与匹配规则
⭐4.1.1 throw可以抛出任意类型的对象
C++ 编译器在运行时会根据你 throw
的对象的类型,去调用链中寻找第一个匹配的 catch 块。匹配规则和函数参数传递类似,是基于类型兼容的匹配。
【关键理解】:
throw 后面的对象类型决定了哪个 catch 能处理它。
catch 是类型敏感的,不支持自动类型转换,例如 throw 3.14 无法被 catch(int) 捕获。
类型可以是引用,也可以是对象,但推荐使用 const 引用 以避免拷贝。
⭐4.1.2 异常处理的“就近原则”
当程序中通过 throw 抛出一个异常时,C++ 会在调用栈中自下而上寻找一个类型匹配的catch块来处理这个异常。第一个匹配成功的 catch 块将会被激活,其他的将被忽略。
【就近原则】 :异常总是由“离抛出位置最近、类型匹配”的 catch 块处理。
【场景】:多个函数嵌套,异常向上传播直到就近匹配
void inner()
{throw std::string("Error: file not found");
}
void middle()
{inner(); // 没有 try-catch,异常会继续向上传播
}
void outer()
{try{middle();}catch (const std::string& e){std::cout << "Caught string in outer(): " << e << '\n';}
}
int main()
{outer();
}
main → outer() → middle() → inner()
↑ ↑
try-catch? throw
⭐4.1.3 找不到匹配的 catch
- 异常会沿着调用栈一路向上传播
- 如果直到
main()
都没人处理,程序会调用std::terminate()
立即崩溃- 因此,建议在最外层程序入口处设置兜底的 catch (…)来防止程序异常退出
⭐4.1.4 异常对象的拷贝机制
在 C++ 中,使用 throw 抛出异常对象时,通常会发生一次对象的拷贝或移动。这是因为异常对象的生命周期需要延长:从 throw 抛出开始,直到被 catch 块捕获并处理完毕。
这种处理方式类似于函数的按值传参和返回过程。所幸在现代 C++ 中,如果异常类型支持右值引用和移动构造(例如 std::string),那么这一步通常会通过移动构造完成,几乎不会带来额外的深拷贝开销。
⭐4.1.5 catch(…) 与未捕获异常的处理
在 C++ 中,catch (...)
是一个特殊的捕获形式,它可以捕获任何类型的异常,无论是基本类型、标准库对象,还是用户自定义类型。
结果:虽然不知道是
double
类型,但异常不会导致程序崩溃。
【局限性】:catch(...) 无法提供异常的具体信息,也无法访问抛出的对象,因此你无法得知异常的类型或内容,只能作通用处理或记录。
【使用建议】:catch(...) 可以作为异常处理的兜底机制,用于捕获所有类型的异常,防止程序异常崩溃。但它无法获取异常的具体信息,不能做有针对性的处理,因此不应过度依赖。
建议仅在程序的最外层(如 main 函数或线程入口)使用 catch(...) 做统一的日志记录或友好退出,而在正常逻辑中,应优先使用类型明确的 catch 来处理已知异常类型。
🌙4.2 在函数调用链中异常栈展开匹配原则
当异常被抛出时,程序会先检查 throw
是否在 try
块内部,若是,则沿调用链向上查找匹配的 catch
。如果当前函数没有匹配的 catch
,则退出当前函数,继续在上层函数中查找,这个过程称为栈展开。
如果一直到 main 函数都找不到匹配的处理代码,程序将被终止。因此,实际中建议在程序入口处添加 catch(...) 来兜底异常。异常一旦被捕获,程序将从对应的 catch 块后继续执行。
🌙4.3 异常的重新抛出
在实际开发中,一个
catch
块可能无法完全处理某个异常,此时可以先进行局部处理(如日志记录、资源清理等),然后将异常重新抛出,交由更高层的调用者继续处理。
这称为异常的重新抛出,通常用于确保异常信息不会被吞掉,且能得到更合适的处理。
【示例演示】:
void inner()
{throw std::string("inner error");
}
void middle()
{try {inner();}catch (const std::string& e){std::cout << "main() caught: " << e << '\n';throw;}
}
int main()
{try {middle();}catch (const std::string& e){std::cout << "main() caught again: " << e << '\n';}
}
【使用要点】:
- 使用
throw;
(不带对象)可以将当前捕获的异常原样抛出;- 重新抛出前可以做一些局部处理(如资源释放、防止内存泄漏);
- 不建议用
throw e;
(抛出变量),那样会复制异常对象,可能丢失原始类型信息。
🌙4.4 异常安全
⭐4.4.1 抛异常出现内存泄漏
【示例演示】:
double Division(int len, int time)
{if (time == 0){throw "除0错误";}return (double)len / (double)time;
}void Func()
{int* array1 = new int[10]; // 动态分配资源try{int len, time;cin >> len >> time;cout << Division(len, time) << endl;}catch (const char* errmsg){cout << "delete [] " << array1 << endl;delete[] array1; // 清理资源throw errmsg; // 重新抛出异常}cout << "delete [] " << array1 << endl;delete[] array1; // 正常释放资源
}int main()
{try{Func();}catch (const char* errmsg){cout << errmsg << endl;}return 0;
}
【分析说明】
如果
Division
抛出异常,程序会跳转到catch
块;在
catch
中先释放了动态数组,再用throw
将异常重新抛出;如果没有
catch
块手动释放,程序跳过后面的delete[]
,就会导致内存泄漏;
这说明:在异常发生前分配的资源,如果不能在异常路径上正确释放,就会造成资源泄露,这就是典型的异常安全问题。这个处理方法相对来说并不能解决本质问题,如果有多个这种这种情况,就得做多次处理。
⭐4.4.2 异常安全的基本原则
构造函数中尽量避免抛出异常,否则对象可能未完全构造,使用时容易出错。
析构函数中不要抛出异常,否则在对象销毁过程中可能导致资源无法正确释放。
C++ 中异常容易导致资源泄漏,例如 new 后异常未能 delete,或 lock 后异常未能 unlock,严重时会造成内存泄漏或死锁。
推荐使用 RAII(资源获取即初始化)思想,用对象生命周期管理资源,如使用智能指针和锁管理类,自动释放资源,避免人为遗漏。
🌙4.5 异常规范(Exception Specification)
异常规范用于声明一个函数可能抛出哪些异常类型,目的是让函数调用者有所预期。但需要注意:C++ 的异常规范不是强制机制,而是一种“道德规范”。
【常见形式】:
// 这里表示这个函数会抛出A/B/C/D中的某种类型的异常
void fun() throw(A,B,C,D);
// 这里表示这个函数只会抛出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 中新增的noexcept,表示不会抛异常
thread() noexcept;
thread (thread&& x) noexcept;
【潜在问题】:
尽管 C++ 提供了异常规范(如 throw() 和 noexcept),但在实际中它们更像是一种“道德约定”,而非强制规则:即使你声明函数不抛异常,编译器通常也不会严格检查,因为完整分析 调用链的成本很高,尤其在大型工程中几乎不可行。
所谓“道德规范”,是指语言设计者假设开发者会自觉遵守规范,但并不会强制执行。然而,现实中总有人违反规则。
【解决措施】:
在复杂项目中,异常规范往往难以写全、写对。为简化这类问题,C++11 引入了 noexcept,统一异常声明风格:
使用 noexcept 表示函数不会抛异常;
如果不写,默认该函数可能抛异常。
☀️五、自定义异常体系
在实际工程开发中,异常处理非常常见,但也容易出现以下问题:
- 项目庞大、多人协作,异常风格难以统一;
- C++ 中
throw
可以抛出任意类型,如果缺乏规范,容易出现异常未被正确捕获,导致程序崩溃;- 随意抛出各种类型的异常,调用者难以处理、问题难以定位,调试成本高。
为了解决这些问题,很多公司或大型项目会选择自定义异常体系,统一管理异常行为。
【常见做法】:
- 定义一个统一的异常基类,如
BaseException
;- 大家抛出的都是继承的派生类对象,捕获一个基类即可
- 同时可通过多态机制,获取具体的错误信息或类型。
基类可以相当于是一个框架,派生类是具体的异常。然后去具体实现异常的内容,然后抛异常只需要抛派生类,捕捉异常只需要捕捉基类即可。
最基础的异常类至少需要包含错误编号和错误描述两个成员变量
测试代码如下:
//基类
//异常
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;
};
其他模块如果要对这个异常类进行扩展,必须继承这个基础的异常类,可以在继承后的异常类中按需添加某些成员变量,或是对继承下来的虚函数what进行重写,使其能告知程序员更多的异常信息.
例如:
//网络异常
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 HttpServerException : public Exception{
public:HttpServerException(const string& errmsg, int id, const string& type):Exception(errmsg, id), _type(type){}virtual string what() const{string str = "HttpServerException:";str += _type;str += ":";str += _errmsg;return str;}
private:const string _type;
};void SQLMgr(){if (rand() % 7 == 0){throw SqlException("权限不足", 100, "select * from name = '张三'");}cout << "调用成功" << endl;
}void CacheMgr(){if (rand() % 5 == 0){throw CacheException("权限不足", 100);}else if (rand() % 6 == 0){throw CacheException("数据不存在", 101);}SQLMgr();
}void seedmsg(const string& s){if (rand() % 2 == 0){throw HttpServerException("网络不稳定,发送失败", 102, "put");}else if (rand() % 3 == 0){throw HttpServerException("你已经不是对象的好友,发送失败", 103, "put");}else{cout << "发送成功" << endl;}
}void HttpServer(){// 失败以后,再重试3次for (size_t i = 0; i < 4; i++){try{seedmsg("今天一起看电影吧");break;}catch (const Exception& e){if (e.getid() == 102){if (i == 3)throw e;cout << "开始第" << i + 1 << "重试" << endl;}else{throw e;}}}CacheMgr();
}
下面进行测试:
int main(){while (1){this_thread::sleep_for(chrono::seconds(1));try {HttpServer();}catch (const Exception& e) // 这里捕获父类对象就可以{using std::chrono::system_clock;// 多态system_clock::time_point today = system_clock::now();std::time_t tt;tt = system_clock::to_time_t(today);std::cout << "todat is: " << ctime(&tt);cout << ctime(&tt) << e.what() << endl << endl;}catch (...){cout << "Unkown Exception" << endl;}}return 0;
}
测试运行:
例如:
#include <iostream>
#include <string>
#include <sstream>
#include <cstdlib>
#include <ctime>
#include <windows.h>using namespace std;// 通用异常基类
class Exception {
public:Exception(const string& errmsg, int id): _errmsg(errmsg), _id(id) {}virtual string what() const {return _errmsg;}protected:string _errmsg;int _id;
};// SQL 异常
class SqlException : public Exception {
public:SqlException(const string& errmsg, int id, const string& sql): Exception(errmsg, id), _sql(sql) {}string what() const override {ostringstream oss;oss << "SqlException: " << _errmsg << " -> " << _sql;return oss.str();}private:string _sql;
};// 缓存异常
class CacheException : public Exception {
public:CacheException(const string& errmsg, int id): Exception(errmsg, id) {}string what() const override {return "CacheException: " + _errmsg;}
};// HTTP 异常
class HttpServerException : public Exception {
public:HttpServerException(const string& errmsg, int id, const string& type): Exception(errmsg, id), _type(type) {}string what() const override {ostringstream oss;oss << "HttpServerException: [" << _type << "] " << _errmsg;return oss.str();}private:string _type;
};// 模拟 SQL 服务
void SQLMgr() {if (rand() % 7 == 0) {throw SqlException("权限不足", 100, "SELECT * FROM users WHERE name = '张三'");}
}// 模拟缓存服务
void CacheMgr() {if (rand() % 5 == 0) {throw CacheException("缓存权限不足", 200);} else if (rand() % 6 == 0) {throw CacheException("缓存中找不到数据", 201);}SQLMgr();
}// 模拟 HTTP 服务
void HttpServer() {if (rand() % 3 == 0) {throw HttpServerException("请求资源不存在", 300, "GET");} else if (rand() % 4 == 0) {throw HttpServerException("访问权限不足", 301, "POST");}CacheMgr();
}// 主函数:统一捕获异常
int main() {srand((unsigned int)time(0));while (true) {Sleep(500); // 模拟服务器循环处理请求try {HttpServer();}catch (const Exception& e) {cout << "捕获异常: " << e.what() << endl;}catch (...) {cout << "未知异常发生" << endl;}}return 0;
}
☀️六、C++标准库的异常体系
C++ 标准库提供了一套定义在 <exception>
头文件中的标准异常类,这些异常按照继承关系组织成一个层次结构。我们可以在程序中直接使用这些标准异常类型来处理常见错误。
下面是 C++ 标准异常类的继承体系结构图:
🌙6.1 std::exception异常继承实操
实际上,我们也可以通过继承
std::exception
来实现自己的异常类。但在实际工程中,很多公司更倾向于像前面那样自定义一套异常继承体系,这是因为 C++ 标准库提供的异常类在功能上相对简单,难以满足复杂系统的需求。
因此,这里我们不再展开对标准异常的介绍,下面给出一个简单的测试代码作为对比。
【示例演示】:
#include <iostream>
#include <exception>void LoadFile(const std::string& filename) {// 抛出一个标准异常throw std::runtime_error("File not found: " + filename);
}int main() {try {LoadFile("config.json");} catch (const std::exception& e) {// 只能获取字符串描述,无法区分错误类型、来源模块等std::cout << "Error: " << e.what() << std::endl;}return 0;
}
【分析代码】:
[std::runtime_error] :这就创建了一个异常对象,内部保存了这个字符串,程序就中断并进入 try-catch 流程。
[e.what()] : e.what() 是 std::exception 类中的一个 虚成员函数,返回一个 const char* 类型的字符串,表示异常的描述信息。
【统一接口捕获,利用多态的特性】:
catch (const std::exception& e)
你是用引用捕获异常对象,无论你抛的是
std::runtime_error
、std::logic_error
还是其他std::exception
的子类,都能用这个统一的接口来获取错误信息。
☀️七、 C++ 异常的优缺点
🌙7.1 异常优点
【信息表达清晰】
异常对象可以封装丰富的错误信息,相比传统错误码方式更具表达力,甚至可包含堆栈信息,便于定位 Bug。【避免层层传递错误码】
传统错误处理需要在函数调用链中逐层返回错误码,写法冗余且易出错;而异常机制能自动中断执行流,直接跳转到最外层 catch 块,简化了错误处理逻辑。
int ConnectSql() {if (...) return 1; // 用户名错误if (...) return 2; // 权限不足 }int ServerStart() {if (int ret = ConnectSql() < 0)return ret;int fd = socket();if (fd < 0)return errno; }int main() {if (ServerStart() < 0)... // 错误处理 }
若使用异常,出错可直接跳转至 main() 中的 catch,无需逐级传递。
【与第三方库兼容性好】
很多主流 C++ 库(如 Boost、gtest、gmock 等)都广泛使用异常机制,使用这些库时也必须具备异常处理能力。【适用于无法返回错误码的场景】
某些函数(如构造函数、重载 operator[] 等)无法通过返回值传递错误,使用异常能更自然地处理错误情况。
🌙7.2 异常缺点
【执行流乱跳,调试困难】
异常会在程序运行时打断正常的控制流,使得执行流程变得混乱。这使得调试和分析程序变得困难,尤其是在异常传播较深时,定位问题较为复杂。【性能开销】
异常机制会带来一定的性能开销。虽然在现代硬件上,这种开销已经微乎其微,但在高性能要求的场景(如游戏开发或实时系统)中,仍然需要谨慎使用。【资源管理复杂,易导致内存泄漏】
C++ 没有垃圾回收机制,资源的管理完全依赖开发者。在异常机制下,如果资源管理不当,可能导致内存泄漏、死锁等问题。因此,必须使用 RAII(资源获取即初始化)来保证资源的正确管理,这增加了学习成本。【C++ 标准库的异常体系设计不够完善】
C++ 标准库的异常体系较为简化,无法提供更详细的错误信息(如错误码、模块来源等),导致开发者往往需要自定义异常体系,这样的做法使得异常处理在不同项目间变得更加混乱。【异常使用规范不明确,可能导致维护困难】
异常机制需要严格规范使用,否则会增加维护的难度。随意抛出异常或不合理的异常设计,可能使得外层捕获异常的用户遭遇极大的困扰。为了避免这种问题,应遵循以下两点规范:抛出的所有异常类型应统一继承自一个基类;
函数是否抛出异常、抛出什么异常,应使用 noexcept 明确标注。
🌻共勉:
以上就是本篇博客的所有内容,如果你觉得这篇博客对你有帮助的话,可以点赞收藏关注支持一波~~🥝