在C++开发中,程序运行时的非法运算、资源调用、接口请求等突发问题无法完全避免。早期C语言依靠错误码 处理异常,存在信息单一、排查繁琐、代码耦合度高的问题。而C++专属的异常处理机制 ,实现了业务逻辑与错误处理逻辑的解耦,能够精准传递错误详情、分层处理异常,是大型项目稳定性保障的核心语法。
本文将结合课件源码,系统拆解C++异常处理全知识点,每个知识点配套真实开发使用场景,兼顾原理、代码实战与工程落地,零基础也能轻松看懂。
一、C++异常核心概念(对比C语言错误码)
1.1 异常机制核心原理
C++异常处理机制的核心价值是分离问题检测与问题处理流程:程序某模块负责检测运行时错误并抛出异常,无需关心错误处理细节;另一专属模块负责捕获并处理异常,实现职责拆分。
相比于C语言错误码机制,优势极其明显:
-
C语言错误码:仅为数字编号,错误信息单一,获取错误码后需手动查表解析详情,代码冗余,跨层级传递困难。
-
C++异常机制 :抛出异常对象,可封装错误描述、错误编号、报错位置、关联参数等完整信息,支持跨函数、跨层级传递,排查效率大幅提升。
✅ 实战使用场景
适用于大型业务项目、服务端程序、接口开发、资源操作场景。例如数据库查询、网络请求、文件读写、数学运算(除零、越界)等大概率出现运行时错误的场景,替代繁琐的错误码判断,简化代码结构。小型工具类、高性能极简底层模块可酌情使用错误码。
二、异常的抛出与捕获(基础核心语法)
2.1 核心语法规则
C++异常处理依赖 try-throw-catch 三件套,核心执行逻辑:
-
try:包裹可能出现异常的业务代码,监控代码运行状态;
-
throw:检测到错误时,主动抛出异常对象,终止当前代码执行;
-
catch:捕获匹配类型的异常对象,执行错误处理逻辑。
关键特性:throw执行后,后续代码立即终止,程序控制权直接跳转至匹配的catch模块;抛出的局部异常对象会生成拷贝,避免局部变量销毁导致的异常信息丢失,拷贝对象将在catch执行完毕后自动销毁。
2.2 基础实战代码
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类型异常(本次场景不匹配,仅作演示)
cout << errid << endl;
}
return 0;
}
void Func() {
int len, time;
cin >> len >> time;
try {
cout << Divide(len, time) << endl;
}
// 捕获const char*类型异常(类型不匹配,无法捕获string异常)
catch (const char* errmsg) {
cout << errmsg << endl;
}
cout <<__FUNCTION__<<":" << __LINE__ << "行执行" << endl;
}
int main() {
while (1) {
try {
Func();
}
// 精准捕获string类型异常
catch (const string& errmsg) {
cout << errmsg << endl;
}
}
return 0;
}
2.3 代码核心解析
本次代码体现了异常跨层级捕获特性:Divide函数抛出string异常,Func函数的catch仅匹配char*类型,无法捕获,异常向上传递,最终由main函数的catch精准捕获并处理。
✅ 实战使用场景
适用于分层开发场景:底层工具函数(运算、IO、数据库操作)只负责检测错误、抛出异常,不处理业务报错;上层业务函数统一捕获异常,实现统一报错、日志记录、容错处理,保证底层代码纯粹性。
三、栈展开(异常传递核心机制)
3.1 原理详解
栈展开是C++异常传递的核心底层机制:当函数抛出异常后,若当前函数无匹配的catch处理器,系统会销毁当前函数栈帧、退出当前函数,向上逐层遍历函数调用链,寻找匹配的catch语句。
完整执行流程:
-
检测throw语句是否位于try块内,若无匹配catch,退出当前函数栈;
-
逐层向上遍历外层调用函数,重复匹配查找逻辑;
-
若遍历至main函数仍无匹配处理器,系统调用terminate函数终止整个程序。
经典调用链路示例:main()->func3()->func2()->func1(),func1抛出异常后,逐层销毁func1、func2、func3栈帧,最终由main函数捕获处理。
✅ 实战使用场景
用于多层嵌套调用的服务程序,例如接口转发、多级业务处理、链式函数调用。无需每一层函数都编写异常处理逻辑,仅需在顶层统一捕获,简化多层代码的异常处理逻辑,同时保证异常不丢失。
四、异常匹配规则(精准捕获机制)
4.1 基础匹配规则
默认情况下,异常捕获要求抛出类型与catch类型完全匹配 ;若存在多个匹配的catch,优先选择距离throw位置最近的处理器。
4.2 特殊兼容转换规则(工程高频使用)
-
常量权限转换:非常量异常对象可被const引用类型catch捕获(权限缩小,保证异常对象不可修改,更安全);
-
指针隐式转换:数组可转为元素类型指针、普通函数可转为函数指针(兼容C风格异常);
-
继承多态转换:派生类异常可被基类catch捕获(自定义异常体系的核心原理)。
补充兜底方案:catch(...) 可捕获任意类型异常,作为程序最后一道防线,避免程序直接崩溃,但无法获取具体错误信息。
✅ 实战使用场景
继承转换规则是大型项目统一异常处理的核心:自定义各类业务异常(数据库、缓存、网络)并继承统一基类,顶层只需捕获基类异常,即可处理所有子业务异常,简化全局异常治理。catch(...)多用于main函数末尾,防止未知异常导致服务宕机。
五、自定义异常体系(工程级实战)
5.1 设计思想
C++标准异常仅提供基础错误信息,无法适配业务细分场景。实际开发中,我们会基于统一异常基类,派生业务专属异常类(数据库、缓存、网络请求等),封装专属错误字段,实现错误精细化区分、精准定位问题。
5.2 完整实战代码
cpp
#include <iostream>
#include <string>
#include <thread>
#include <chrono>
#include <cstdlib>
#include <ctime>
using namespace std;
// 统一异常基类
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 += "->执行SQL:";
str += _sql;
return str;
}
private:
const string _sql; // 报错的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; // 请求方式:get/post/put
};
// 数据库业务模块
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();
}
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;
}
5.3 核心亮点解析
利用多态特性,通过基类Exception引用捕获所有派生类异常,无需为每个业务异常单独编写捕获逻辑,代码高度复用;每个自定义异常封装专属业务字段,报错信息精准可追溯。
✅ 实战使用场景
后端服务、分布式项目、模块化开发场景。将异常按业务模块拆分(数据库、缓存、网关、接口),报错时可直接定位故障模块,便于日志统计、异常监控、线上问题快速排查,是企业级项目的标准异常开发规范。
六、异常重新抛出(分层容错核心)
6.1 原理与语法
部分场景中,当前模块仅能处理特定类型异常 ,剩余异常需要交给上层业务处理,此时使用异常重新抛出 机制。语法极简:捕获异常后直接写throw;,无需携带参数,可完整传递原异常对象及所有信息,无信息丢失。
6.2 核心使用逻辑
-
可自愈异常:当前模块重试、修复处理,无需上抛;
-
不可自愈异常:直接重新抛出,交由顶层模块统一处理(日志、告警、返回前端报错)。
6.3 完整实战代码
cpp
#include <iostream>
#include <string>
#include <cstdlib>
#include <ctime>
using namespace std;
// 复用前文异常基类与HttpException类
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 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 _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)
{
// 最多重试3次
for (size_t i = 0; i < 4; i++)
{
try
{
_SeedMsg(s);
break; // 发送成功,退出重试
}
catch (const Exception& e)
{
// 网络异常:重试自愈
if (e.getid() == 102)
{
// 重试3次仍失败,上抛异常
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;
}
✅ 实战使用场景
网络请求、消息推送、接口调用等可重试自愈场景。例如网络波动、瞬时超时等临时异常,本地重试修复;权限不足、参数错误等不可逆异常,直接上抛上层,统一返回报错信息、记录日志,兼顾容错性和业务规范性。
七、异常安全问题(避坑核心)
7.1 核心风险
异常抛出会终止当前代码执行 ,若函数中提前申请了堆内存、文件句柄、锁资源等,未执行释放代码就抛出异常,会直接导致资源泄漏,引发内存溢出、死锁、文件占用等严重问题。
重点禁忌:禁止析构函数抛出异常!若析构释放多个资源时中途抛异常,剩余资源无法释放,造成永久性资源泄漏。
7.2 安全解决方案
-
捕获异常后,优先手动释放已申请资源,再通过
throw;重新抛出异常; -
工程最优方案:使用RAII机制(智能指针、容器),依托对象生命周期自动管理资源,从根源避免泄漏。
7.3 安全实战代码
cpp
#include <iostream>
#include <string>
using namespace std;
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;
}
int main()
{
try
{
Func();
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
catch (const exception& e)
{
cout << e.what() << endl;
}
catch (...)
{
cout << "Unkown Exception" << endl;
}
return 0;
}
✅ 实战使用场景
所有涉及手动资源申请的场景:动态内存分配、文件打开、互斥锁加锁、数据库连接申请等。必须做好异常安全防护,避免长期运行的服务程序因资源泄漏逐渐崩溃。
八、异常规范(C++98/C++11语法迭代)
8.1 语法迭代与规则
异常规范用于声明函数的异常抛出能力,方便开发者预判函数风险,简化调用逻辑,C++标准对其做了大幅优化:
-
C++98 老旧规范 :
throw()表示不抛异常;throw(类型1,类型2)表示仅抛出指定类型异常,语法繁琐、实用性差; -
C++11 全新规范 :简化为
noexcept,修饰函数表示绝对不抛出异常;无修饰则默认可抛异常; -
补充特性:
noexcept(表达式)可作为运算符,检测表达式是否会抛异常,返回布尔值。
核心注意:编译器不强制校验noexcept,若noexcept修饰的函数违规抛异常,程序会直接调用terminate终止运行。
8.2 实战代码
cpp
#include <iostream>
using namespace std;
// C++11 noexcept修饰:声明函数不抛异常(实际违规抛异常)
double Divide(int a, int b) noexcept
{
if (b == 0)
{
throw "Division by zero condition!";
}
return (double)a / (double)b;
}
int main()
{
try
{
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
catch (...)
{
cout << "Unkown Exception" << endl;
}
// 检测表达式是否会抛出异常
int i = 0;
cout << noexcept(Divide(1,2)) << endl; // 无异常:true(1)
cout << noexcept(Divide(1,0)) << endl; // 有异常:false(0)
cout << noexcept(++i) << endl; // 无异常:true(1)
return 0;
}
✅ 实战使用场景
用于底层工具函数、STL源码、高性能模块。对于确定不会出错的基础函数、容器接口、数学工具函数,添加noexcept修饰,帮助编译器优化代码,提升程序运行效率,同时规范函数行为。
九、C++标准库异常体系
C++标准库内置完整的异常继承体系,所有标准异常的基类为std::exception ,提供核心虚函数what(),用于返回异常详情,派生类可重写该函数自定义报错信息。
开发规范:自定义异常建议继承std::exception基类,统一全局异常体系,便于顶层统一捕获、统一处理,兼容标准库异常,避免异常体系混乱。
十、总结:C++异常工程最佳实践
-
逻辑解耦:业务逻辑与错误处理分离,代码更整洁、易维护;
-
分层处理:底层抛异常、中层容错重试、顶层统一捕获处理;
-
规范继承:自定义异常继承std::exception,搭建模块化异常体系;
-
安全优先:杜绝析构抛异常,异常场景必释放资源,优先使用RAII;
-
合理规范:稳定底层函数添加noexcept,优化性能、规范接口。