在程序开发中,错误处理是不可或缺的重要环节。C 语言通过错误码处理错误的方式,不仅需要开发者手动查询错误信息,还无法传递丰富的错误上下文。而 C++ 的异常机制则提供了更优雅、更灵活的解决方案,它将问题的检测与处理分离,让程序各模块能够独立开发且高效协作。本文将从异常的核心概念出发,逐步深入到抛出捕获、栈展开、实战应用等关键知识点,带你全面掌握 C++ 异常的使用技巧与最佳实践。
1. 异常的核心概念
异常是程序运行时出现的非预期情况(如除零错误、资源不足、网络异常等),C++ 的异常处理机制允许程序在检测到异常时,通过特定方式通知相关模块并进行针对性处理。
1.1 异常与错误码的本质区别
• 错误码:本质是对错误的分类编号,仅能传递简单的错误标识,开发者需要额外查询错误含义,且错误传播需要手动传递,代码冗余度高。
• 异常:以对象形式抛出,可封装详细的错误信息(如错误描述、发生位置、相关参数等),无需手动传递,能自动沿调用链传播至合适的处理模块。
错误码使用场景:
(1)定义错误码常量:
cpp
#define FILE_OPEN_ERROR -1 // 文件打开失败的错误码
#define READ_SUCCESS 0 // 读取成功的错误码
(2)函数返回错误码:
函数 ReadFileContent 执行文件打开操作,若失败则返回 FILE_OPEN_ERROR,成功则返回 READ_SUCCESS:
cpp
int ReadFileContent(const char* filename)
{
FILE* file = fopen(filename, "r");
if (file == NULL)
{
return FILE_OPEN_ERROR; // 文件打开失败,返回错误码
}
// (省略)读取文件内容的逻辑...
fclose(file);
return READ_SUCCESS; // 操作成功,返回成功码
}
(3)调用方判断错误码:
main 函数调用 readFileContent 后,根据返回的错误码分支处理:
cpp
int main()
{
int result = ReadFileContent("nonexistent.txt");
if (result == FILE_OPEN_ERROR)
{
printf("无法打开文件。\n");
}
else
{
printf("文件读取成功。\n");
}
return 0;
}
1.2 异常的核心价值
**• 分离错误检测与处理逻辑:**检测异常的模块无需知晓处理细节,处理模块也无需关注检测过程,符合高内聚低耦合的设计原则。
**• 传递丰富的错误信息:**异常对象可携带任意自定义数据,相比错误码能提供更完整的上下文。
**• 简化错误处理流程:**避免多层嵌套的条件判断,让正常逻辑与错误处理逻辑清晰分离,代码可读性更强。
2. 异常的抛出与捕获
异常处理的核心流程包括:抛出异常(throw)、捕获异常(catch)、匹配处理逻辑,这一过程需要遵循严格的规则。
2.1 异常的抛出(throw)
当程序检测到异常时,通过throw关键字抛出一个异常对象,该对象的类型和内容将决定后续的处理逻辑。
关键特性:
• throw执行后,其后面的语句将不再执行,程序直接跳转到匹配的catch模块。
• 抛出的异常对象会生成一个拷贝(类似函数传值返回),即使原对象是局部变量,拷贝对象也会持续到catch处理完成后销毁。
示例:
cpp
double Divide(int a, int b)
{
// 检测除零异常,抛出字符串类型的异常对象
if (b == 0)
{
string s("Divide by zero condition!");
throw s;
}
return (double)a / (double)b;
}
2.2 异常的捕获(try-catch)
异常通过try-catch语句捕获,try块包裹可能抛出异常的代码,catch块定义异常处理逻辑。
语法结构:
cpp
try {
// 可能抛出异常的代码
riskyOperation();
} catch (异常类型1& e) {
// 处理类型1的异常
} catch (异常类型2& e) {
// 处理类型2的异常
} catch (...) {
// 捕获任意类型的异常(兜底处理)
}
捕获规则:
• 首先检查throw是否在try块内部,若在则查找匹配的catch。
• 若当前函数无匹配的catch,则退出当前函数,沿调用链向上查找(栈展开过程)。
• 若直至main函数仍无匹配的catch,程序将调用terminate函数终止。
示例:
cpp
void Func()
{
int len, time;
cin >> len >> time;
try
{
// 可能抛出异常的除法操作
cout << Divide(len, time) << endl;
}
catch (const char* errmsg)
{
// 捕获C风格字符串类型异常
cout << errmsg << endl;
}
cout << __FUNCTION__ << ":" << __LINE__ << "行执行" << endl;
}
int main()
{
while (1)
{
try
{
Func();
}
catch (const string& errmsg)
{
// 捕获string类型异常(匹配Divide函数抛出的异常)
cout << errmsg << endl;
}
}
return 0;
}
输出结果:

3. 栈展开
当异常在当前函数未被捕获时,程序会启动栈展开(Stack Unwinding)过程,沿函数调用链向上查找匹配的catch块,这是异常传播的核心机制。
3.1 栈的展开流程
假设存在函数调用链:main() -> Func3() -> Func2() -> Func1(),且在Func1()中抛出异常:
检查Func1()中throw是否在try块内,若有匹配catch则处理,否则退出Func1()栈帧。
进入Func2()栈帧,检查是否有匹配catch,无则退出Func2()栈帧。
重复上述过程,依次遍历Func3()、main()栈帧。
若main()中仍无匹配catch,程序调用terminate终止。
如图所示:

3.2 栈展开的关键影响
**• 函数提前退出:**栈展开过程中,未执行完的函数会被强制退出。
**• 对象自动销毁:**沿调用链创建的局部对象会在栈展开时被析构,避免内存泄漏。
示例流程图:

4. 异常的匹配规则
catch块与抛出的异常对象匹配时,遵循 "精确匹配优先、特殊转换允许" 的原则,具体规则如下:
4.1 精确匹配
大多数情况下,catch的参数类型需与异常对象类型完全一致,例如抛出string类型异常,需用catch (const string& e)捕获。
4.2 允许的隐式类型转换
以下三种特殊转换在匹配时被允许,为异常处理提供灵活性:
• 非常量到常量的转换:抛出string对象,可被catch (const string& e)捕获(权限缩小)。
• 数组 / 函数到指针的转换:抛出char[]数组,可被catch (const char* e)捕获。
**•**派生类到基类的转换:抛出派生类异常对象,可被基类类型的catch捕获(最实用的规则)。
4.3 兜底捕获(catch (...))
catch (...)可捕获任意类型的异常,通常作为最后一个catch块,用于处理未预期的异常,避免程序终止。但需注意:catch (...)无法获取异常的具体信息,仅能提供通用错误提示。
**实战场景:**大型项目中,通常定义一个基类Exception,各模块的异常类继承自该基类,捕获时只需捕获基类即可处理所有派生类异常:
cpp
// 基类异常
class Exception
{
public:
Exception(const string& errmsg, int id)
: _errmsg(errmsg), _id(id)
{}
//what函数,表明发生了什么异常
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 override
{
return "SqlException:" + _errmsg + "->" + _sql;
}
private:
const string _sql;
};
// HTTP模块异常(派生类)
class HttpException : public Exception
{
// 实现略...
};
// 捕获时只需匹配基类
int main()
{
try
{
HttpServer(); // 可能抛出SqlException/HttpException等派生类异常
}
catch (const Exception& e)
{ // 基类引用捕获所有派生类异常
cout << e.what() << endl; // 多态调用,输出具体异常信息
}
catch (...) //兜底捕获,如果出现异常但在前面没捕获到就会到这儿来
{
cout << "Unknown Exception" << endl;
}
return 0;
}
5. 异常的重新抛出
有时catch捕获异常后,无法完全处理(如需要外层模块记录日志、重试操作等),此时需将异常重新抛出,交给外层调用链处理,使用throw;语句即可(无需指定异常对象)。
**• 部分处理 + 外层兜底:**捕获异常后进行局部处理(如释放资源),再将异常抛出给外层。
**• 分类处理:**对特定类型异常特殊处理,其他异常转发给外层。
**• 重试机制:**网络异常等场景下,重试失败后再抛出异常。
实战示例:消息发送重试:
cpp
void _SendMsg(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
{
_SendMsg(s);
break; // 发送成功,退出循环
}
catch (const Exception& e)
{
if (e.getid() == 102)
{ // 仅网络异常重试
if (i == 3)
{ // 重试3次失败,重新抛出
throw;
}
cout << "开始第" << i + 1 << "次重试" << endl;
}
else
{ // 其他异常直接抛出
throw;
}
}
}
}
6. 异常安全:防止内存泄漏
异常抛出后,程序可能跳过后续代码,若之前申请的资源(内存、锁、文件句柄等)未被释放,会导致资源泄漏,这是异常处理中需重点关注的问题。
6.1 常见的异常安全问题
请看以下场景:
cpp
void Func()
{
int* array = new int[10]; // 申请内存资源
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl; // 若抛出异常,下面的delete不会执行
delete[] array; // 可能无法执行,导致内存泄漏
}
如果在Divide环节出现异常,那么会直接跳到catch部分,不会走下面的delete[],此时就出现了内存泄漏。
6.2 解决方案
(1)手动捕获异常并释放资源
在try-catch中捕获所有异常,释放资源后重新抛出:
cpp
void Func()
{
int* array = new int[10];
try
{
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
}
catch (...)
{
// 捕获所有异常,释放资源后重新抛出
delete[] array;
throw;
}
// 正常执行时释放资源
delete[] array;
}
(2)使用 RAII 机制(推荐)
RAII(Resource Acquisition Is Initialization)即 "资源获取即初始化",通过对象的构造函数申请资源,析构函数释放资源。由于栈展开时局部对象会自动析构,因此能保证资源一定被释放,智能指针(unique_ptr、shared_ptr)是 RAII 的典型应用:
cpp
void Func()
{
// 智能指针自动管理内存,异常时会自动析构释放
unique_ptr<int[]> array(new int[10]);
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
}
智能指针部分我们会在下一篇文章中进行详细讲解,如不了解请自行观看。
(3)析构函数禁止抛出异常
析构函数若抛出异常,可能导致后续资源无法释放(如析构函数释放 10 个资源,第 5 个时抛出异常,剩余 5 个资源泄漏)。《Effective C++》明确指出:别让异常逃离析构函数。
7. 异常规范
异常规范用于告知开发者和编译器,函数是否会抛出异常、可能抛出哪些类型的异常,帮助简化调用者的错误处理逻辑。
7.1 C++98 异常规范(已过时)
通过throw(类型列表)指定函数可能抛出的异常类型,throw()表示不抛出异常:
cpp
// 仅抛出bad_alloc异常
void* operator new(std::size_t size) throw(std::bad_alloc);
// 不抛出异常
void* operator delete(std::size_t size, void* ptr) throw();
缺点:语法复杂,编译器检查宽松,实践中实用性低。
7.2 C++11 异常规范(推荐)
• noexcept: 表示函数不会抛出异常。
• 无标注: 表示函数可能抛出任意类型异常。
**• noexcept(表达式):**作为运算符,判断表达式是否可能抛出异常,返回bool值。
关键特性:
• 编译器不强制检查noexcept,但若标注noexcept的函数抛出异常,程序会调用terminate终止。
**•**noexcept可优化性能:编译器无需为可能的异常传播生成额外代码。
示例如下:
cpp
// 标注为不抛出异常(但实际可能抛出,编译器不报错但运行时会终止)
double Divide(int a, int b) noexcept
{
if (b == 0)
{
throw "Division by zero condition!";
}
return (double)a / (double)b;
}
int main()
{
// 测试noexcept运算符
int i = 0;
cout << noexcept(Divide(1, 2)) << endl; // 输出1(表达式本身不抛异常)
cout << noexcept(Divide(1, 0)) << endl; // 输出1(noexcept判断的是函数标注,而非实际行为)
cout << noexcept(++i) << endl; // 输出1(自增操作不抛异常)
return 0;
}
输出结果:

这里我们注意,在main中noexcept作为一个运算符去检测一个表达式是否会抛出异常,这里与我们实际传什么参数,是否会触发Divide的异常无关,它只要有可能触发异常,就会返回true(1),反之返回false(0)。
8. C++ 标准库的异常体系
C++ 标准库提供了一套预定义的异常继承体系,基类为std::exception,所有标准库异常均继承自该类,且重写了what()虚函数用于返回异常信息。
8.1 标准库异常体系结构

8.2 标准库异常的使用
cpp
int main()
{
try
{
vector<int> v;
v.at(10); // 超出vector范围,抛出out_of_range异常
}
catch (const std::exception& e) // 捕获所有标准库异常
{
cout << "标准库异常:" << e.what() << endl;
}
return 0;
}
输出结果:

结语
好好学习,天天向上!有任何问题请指正,谢谢观看!