1.异常的概念以及运用
1.1 什么是异常
核心概念:异常 (Exception)
异常是程序在运行时发生的、偏离正常执行流程的意外事件或错误状态。它可以是硬件问题(如除零)、资源问题(如内存不足、文件不存在)、逻辑错误(如下标越界)等。
异常处理机制的优势 (与C语言错误码对比)
C语言错误码方式(如返回 -1 或 NULL)存在一些固有缺陷,而异常处理正是为了解决这些问题而设计的。
1.2 异常的抛出与捕获
- 核心机制
-
抛出与匹配 :程序通过
throw一个对象来引发异常。系统会根据这个对象的类型 和当前的调用链 ,自动寻找与该对象类型匹配且离抛出异常位置最近的catch处理器。 -
类型与内容 :抛出对象的类型决定了匹配哪个
catch,而对象携带的内容(如错误信息)则用于向处理器描述发生了什么错误。 -
控制权转移 :一旦
throw执行,其后的语句不再运行 ,控制权会从throw位置直接跳转到匹配的catch模块。这个catch可以在当前函数,也可以在调用链中的其他函数。
- 关键特性
-
栈展开与对象销毁 :当控制权转移时,沿着调用链的函数会提前退出。同时,在
throw位置到catch位置之间,所有在调用链上创建的局部对象都会被自动销毁。 -
异常对象的拷贝 :
throw时会生成一个异常对象的拷贝 。这是因为原始异常对象可能是局部变量,在栈展开时就会被销毁。这个拷贝会存活到catch子句执行结束后才被销毁,其处理方式类似于函数传值返回。
1.3 栈展开
栈展开是异常处理机制中,从异常抛出点向上查找匹配 catch 子句的过程。
-
查找起点 :抛出异常后,程序暂停当前函数的执行,首先检查
throw语句本身是否位于try块内部。-
若在
try块内,则查找该try块关联的catch子句。 -
若找到类型匹配的
catch,则跳转到该catch处执行处理。
-
-
逐层退出 :若当前函数中没有
try/catch结构,或者虽然有try/catch但没有匹配的catch类型,则退出当前函数,销毁其局部对象,并继续在外层调用函数中重复上述查找过程。这个过程就称为栈展开。 -
未找到匹配 :如果栈展开一直进行到
main函数,仍然没有找到匹配的catch子句,程序将调用标准库的terminate函数终止运行。 -
处理完成 :一旦找到匹配的
catch子句并执行完毕后,程序会从该catch块之后继续执行(不会回到throw的位置)。
cpp
double Divide(int a, int b)
{
try
{
// 当b == 0时抛出异常
if (b == 0)
{
string s("Divide by zero condition!");
throw s;
//throw语句后面不执行
}
else
{
return ((double)a / (double)b);
}
}
catch (int errid)
{
cout << errid << endl;
}
return 0;
}
void Func()
{
int len, time;
cin >> len >> time;
try
{
cout << Divide(len, time) << endl;
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
cout << __FUNCTION__ << ":" << __LINE__ << "执行" << endl;
}
int main()
{
while (1)
{
try
{
Func();
}
catch (const string& errmsg)
{
cout << errmsg << endl;
}
catch (...)//任意类型异常
{
cout << "unknow error" << endl;
}
}
return 0;
}

1.4查找匹配的处理代码
异常匹配的核心原则是:抛出对象的类型 决定了由哪个 catch 处理。一般情况下要求类型完全匹配 ,且多个匹配时选择离抛出位置最近的那个。
但在以下几种特殊情况下,允许进行合法的类型转换:
允许的类型转换(非严格匹配)

重点:派生类向基类的转换
实际工程中,异常类通常设计为继承体系(如
std::exception作为基类),通过捕获基类类型即可统一处理所有派生异常,非常方便。
未找到匹配的处理
-
如果栈展开到
main函数仍然没有找到匹配的catch,程序会调用std::terminate()终止。 -
程序终止通常不是我们期望的结果(除非发生严重错误)。
兜底捕获:catch(...)
catch(...) 的特点:
-
可以捕获任意类型的异常
-
缺点是无法获取异常对象的内容(不知道错误是什么)
-
通常放在
catch链的最后作为兜底,防止程序意外终止
1.5 异常的重新抛出
cpp
try
{
// 可能抛出异常的代码
}
catch (const Exception& e)
{
// 部分错误需要特殊处理
if (某种条件)
{
// 特殊处理逻辑
}
else
{
throw; // 重新抛出当前捕获的异常
}
}

注意:throw; 与 throw e; 的区别
cpp
catch (const std::exception& e) {
throw; // 正确:重新抛出原始异常,保留动态类型
}
catch (const std::exception& e) {
throw e; // 错误:抛出的是 e 的静态类型(std::exception)
}
总结: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中是102号错误,⽹络不稳定,则重新发送
// 捕获异常,else中不是102号错误,则将异常重新抛出
if (e.getid() == 102)
{
// 重试三次以后否失败了,则说明⽹络太差了,重新抛出异常
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;
}
1.6 异常安全问题
一、异常导致的资源泄漏问题
问题描述:
当程序抛出异常时,throw 后面的代码将不再执行。如果在申请资源(如内存、锁、文件句柄等)和释放资源之间发生了异常,就会导致资源无法被释放,从而引发资源泄漏和安全问题。
示例场景:
cpp
void func()
{
int* ptr = new int[1000]; // 申请资源
lock.acquire(); // 获取锁
// 这里可能抛出异常
doSomething(); // 如果抛异常,后面释放资源的代码不会执行
delete[] ptr; // 可能被跳过 → 内存泄漏
lock.release(); // 可能被跳过 → 死锁
}
二、传统解决方案:捕获 → 释放 → 重新抛出
cpp
void func()
{
int* ptr = new int[1000];
lock.acquire();
try
{
doSomething(); // 可能抛异常
}
catch (...)
{
delete[] ptr; // 释放资源
lock.release();
throw; // 重新抛出,让上层继续处理
}
delete[] ptr;
lock.release();
}
缺点: 代码繁琐、容易遗漏、可维护性差。
三、更好的解决方案:RAII
RAII(Resource Acquisition Is Initialization,资源获取即初始化)是 C++ 中推荐的资源管理方式。
核心思想:
-
在构造函数中获取资源
-
在析构函数中释放资源
-
当栈展开时,局部对象的析构函数会被自动调用 ,资源自动释放

这里的内容我会单独出一篇博客来详细讲解智能指针
四、析构函数中的异常处理
4.1 问题描述
析构函数在释放多个资源时,如果释放到一半抛出异常,后续资源将无法释放,同样会导致资源泄漏。
cpp
~ResourceManager()
{
for (int i = 0; i < 10; ++i)
{
delete res[i]; // 假设第5个 delete 抛出异常
// 第6-10个资源无法释放!
}
}
4.2 核心原则(《Effective C++》条款08)不要让异常逃离析构函数
4.3 为什么析构函数不应抛出异常?
4.4 解决方案:捕获并处理
cpp
~ResourceManager()
{
for (int i = 0; i < 10; ++i)
{
try
{
delete res[i];
}
catch (const std::exception& e)
{
// 记录日志,但不向外抛出
Logger::Log(e.what());
}
catch (...)
{
Logger::Log("未知错误");
}
}
}
4.5 更好的设计:提供独立的 close() 方法
cpp
class Connection
{
public:
void close()
{
// 可能抛异常的操作
if (disconnect() == FAILED)
{
throw Exception("断开失败");
}
closed = true;
}
~Connection()
{
if (!closed)
{
try { close(); }
catch (...)
{
// 析构中只能吞掉异常或调用 terminate
}
}
}
private:
bool closed = false;
};
总结
1.7 异常规范
异常规格说明:从 throw() 到 noexcept
一、为什么需要异常规格说明?
对于用户和编译器而言,预先知道一个函数是否会抛出异常大有裨益:
-
用户 :可以决定是否需要为该函数编写
try-catch代码 -
编译器:可以进行优化(如避免生成栈展开相关的代码)
二、C++98 的 throw() 方式
在 C++98 中,通过在函数参数列表后面添加 throw() 来声明函数的异常抛出行为:
缺点:
-
语法复杂,尤其是需要列出多种异常类型时
-
实践中不好用,容易出现不一致
-
运行时检查开销较大
三、C++11 的 noexcept 方式
C++11 对其进行了简化,引入了 noexcept 关键字:
四、noexcept 的编译器行为
关键点:编译器不会在编译时强制检查 noexcept
cpp
void mayThrow()
{
throw 42;
}
void test() noexcept
{
mayThrow(); // 调用了可能抛异常的函数
throw 42; // 直接 throw
// 编译器可能只给一个警告,但会编译通过
// warning "XXX": 假定函数不引发异常,但确实发生了
}
运行时行为:
-
如果一个声明为
noexcept的函数实际抛出了异常 -
程序会调用
std::terminate()立即终止(而不是进行栈展开)
cpp
void dangerous() noexcept
{
throw std::runtime_error("error"); // 抛出异常
}//warning "dangerous": 假定函数不引发异常,但确实发生了
int main()
{
dangerous(); // 程序直接终止,不会传播到 main
return 0;
}
五、noexcept 作为运算符
noexcept(expression) 可以作为一个运算符 ,在编译时检测一个表达式是否会抛出异常:意思就是这个表达式是noexcept(无异常)的吗
-
返回
true:表达式不会抛出异常 -
返回
false:表达式可能抛出异常
示例:
cpp
void nonThrow() noexcept {}
void mayThrow() {}
int main()
{
std::cout << noexcept(1 + 2) << std::endl; // 1 (true),整型运算不抛异常
std::cout << noexcept(nonThrow()) << std::endl; // 1 (true)
std::cout << noexcept(mayThrow()) << std::endl; // 0 (false)
// 常用于模板元编程或条件编译
if constexpr (noexcept(nonThrow()))
{
// 编译时就知道不抛异常,可以做一些优化
}
return 0;
}
六、实际应用建议
总结
-
noexcept是 C++11 引入的更简洁、更实用的异常规格说明 -
它告诉编译器"这个函数不会抛异常",如果违反了承诺,程序会直接终止
-
移动语义和性能优化中应优先使用
noexcept
cpp
// C++98
// 这⾥表⽰这个函数只会抛出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
size_type size() const noexcept;
iterator begin() noexcept;
const_iterator begin() const noexcept;
double Divide(int a, int b) noexcept
{
// 当b == 0时抛出异常
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;
cout << noexcept(Divide(1, 0)) << endl;
cout << noexcept(++i) << endl;
return 0;
}
2.标准库的异常
- 参考文档:cplusplus.com/reference/exception/exception/
- C++标准库也定义了⼀套⾃⼰的⼀套异常继承体系库,基类是exception,所以我们⽇常写程序,需要在主函数捕获exception即可,要获取异常信息,调⽤what函数,what是⼀个虚函数,派⽣类可以重写。
