📝前言:
这篇文章我们来讲讲C++异常:
🎬个人简介:努力学习ing
📋个人专栏:C++学习笔记
🎀CSDN主页 愚润求学
🌄其他专栏:C语言入门基础,python入门基础,python刷题专栏,Linux
一,异常处理
异常处理是C++中处理运行时错误的重要机制,它允许程序在遇到错误时能够优雅地恢复或终止,而不是直接崩溃。
1.1 基本语法
C++异常处理使用三个关键字:try
, catch
, 和 throw
。
try {
// 可能抛出异常的代码
if (error_occurred) {
throw exception_object;
}
} catch (exception_type1& e) {
// 处理exception_type1类型的异常
} catch (exception_type2& e) {
// 处理exception_type2类型的异常
} catch (...) {
// 处理所有其他类型的异常
}
1.1 抛出异常
使用throw
关键字抛出异常:
double divide(double a, double b) {
if (b == 0.0) {
throw "Division by zero!"; // 抛出字符串字面量
}
return a / b;
}
1.2 捕获异常
使用catch
块捕获并处理异常:
try {
double result = divide(10.0, 0.0);
} catch (const char* msg) {
cerr << "Error: " << msg << endl;
}
- 程序先执行
try
里面的语句,如果try
里面throw
了异常,throw
后⾯的语句将不再被执行。程序的执行从throw
位置跳到与之匹配的catch
模块 - 找
catch
的规则:找调用链中与该对象类型匹配且离抛出异常位置最近的那⼀个 - 从
throw
到catch
:1,沿着调用链的函数可能提早退出。2、沿着调用链创建的对象都将销毁 - 抛出异常对象后,会⽣成⼀个异常对象的拷⻉,因为抛出的异常对象可能是⼀个局部对象,所以会⽣成⼀个拷⻉对象,这个拷⻉的对象会在catch⼦句后销毁。(这⾥的处理类似于函数的传值返
回)
二,具体的找catch的栈展开过程
- 如果
throw
在try
里面,则找对应的catch
- 如果没找到 / 类型不匹配,则提前退出当前函数,到外层函数里面(函数调用链)找
- 如果找到
main
函数都没找到,则程序会调⽤标准库的terminate
函数终止程序【main
函数兜底】 - 如果这个过程中有一步找到了,则跳去执行
catch
,执行完以后,不会回到原来的位置,而是继续执行catch
后面的语句,相当于throw
改变了执行流程
三,catch 的类型匹配
除了严格的完全对应的类型匹配,以下几种也可以匹配:
- 允许从⾮常量向常量的类型转换,也就是权限缩⼩
- 允许数组转换成指向数组元素类型的指针,函数被转换成指向函数的指针
- 允许从派⽣类向基类类型的转换(实际中继承体系基本都是⽤这个⽅式设计)
catch(...)
:...
表示匹配任意类型(但是不知道异常错误是什么),main
函数的兜底就是这个
四, 异常的重新抛出
在catch块中可以重新抛出当前异常:
try {
// ...
} catch (const std::exception& e) {
// 部分处理
throw; // 重新抛出相同的异常
}
- 重新抛出的是原始异常对象(不是拷贝),并且保留原始的异常类型和内容。
- 不会被同一个
catch
块再次捕获,而是会跳出当前try-catch
块,继续向外层传播,由外层的try-catch
块捕获。
五,异常安全问题
- 异常抛出后,后⾯的代码就不再执行,如果前面申请了资源,但是释放代码在
catch
后面,而catch
中又重新抛出了异常,就会没有释放,引发资源泄漏,产⽣安全性的问题。解决方法:1,捕获异常以后,要先释放资源,再重新抛出;2,用智能指针的RAII
方式解决这种问题是更好的。 - 其次,析构函数中,如果抛出异常也要谨慎处理,比如析构函数要释放10个资源,释放到第5个时抛
出异常,则也需要捕获处理,否则后⾯的5个资源就没释放,也资源泄漏了。
示例(解决方法1):
void process() {
int* resource = new int(100); // 申请资源(假设是动态内存)
try {
some_operation_that_may_throw(); // 可能抛出异常
}
catch (...) {
delete resource; // 手动释放资源
throw; // 重新抛出异常
}
delete resource; // 正常情况下的释放
}
但是这种解决方法不好,后面用智能指针 +RAII
更好!
六,异常的规范
- 对于用户和编译器而言,预先知道某个函数是否会抛出异常有助于简化调用函数的代码
noexcept
关键字:函数参数列表后⾯加noexcept
表示不会抛出异常,啥都不加表示可能会抛出异常- 编译器并不会在编译时检查
noexcept
修饰的函数。即使里面还是有throw
- 但是,如果⼀个声明了
noexcept
的函数抛出了异常(里面有throw
,并且运行时抛出了),程序会调用terminate
终止程序 noexcept
还可以作为⼀个运算符去检测⼀个表达式是否会抛出异常,如果保证不会抛出异常就返回true
,可能会就返回false
示例(有noexcept
但是还是会throw
):
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;
}
编译时通过,运行时,输入:1 0
,直接干没了
不带noexcept
:
double Divide(int a, int b)
输入2 2
运行结果:
七,标准异常
官方文档:https://legacy.cplusplus.com/reference/exception/exception/
- C++标准库也定义了⼀套自己的⼀套异常继承体系库,基类是
exception
,所以我们⽇常写程序,需要在主函数捕获exception
即可,要获取异常信息,调⽤what
函数,what
是⼀个虚函数,派生类可以重写。 - 标准异常头文件;
<stdexcept>
标准库常见异常类:
异常类 | 用途 |
---|---|
std::logic_error | 程序逻辑错误(如无效参数) |
std::runtime_error | 运行时错误(如文件不存在) |
std::bad_alloc | 内存分配失败(new 抛出) |
std::out_of_range | 越界访问(如 vector::at) |
示例:
int main() {
try {
// 尝试执行的代码块
std::vector<int> v(1000000000); // 可能抛出 std::bad_alloc
throw std::runtime_error("自定义运行时错误"); // 构造并抛出标准异常类:runtime_error异常,并且用"自定义运行时错误"构造它,把异常信息设置成这句话。后续.what()的时候,就可以获取异常信息
}
catch (const std::exception& e) {
// 异常处理块
std::cerr << "捕获异常: " << e.what() << std::endl;
}
return 0;
}
执行流程
-
try 块中的代码执行:
-
首先尝试创建一个超大
vector v(1000000000)
-
如果系统内存不足,会抛出
std::bad_alloc
异常 -
如果创建成功,继续执行下一行
-
-
然后执行
throw std::runtime_error("自定义运行时错误")
- 这行代码会主动抛出一个运行时错误异常
-
-
catch 块捕获异常:
-
无论抛出的是
std::bad_alloc
还是std::runtime_error
-
因为它们都继承自
std::exception
,所以都会被这个catch
块捕获 -
捕获后执行异常处理代码,打印错误信息
-
运行示例(内存足够时):
把vector
的大小再加大(内存不足时):
🌈我的分享也就到此结束啦🌈
要是我的分享也能对你的学习起到帮助,那简直是太酷啦!
若有不足,还请大家多多指正,我们一起学习交流!
📢公主,王子:点赞👍→收藏⭐→关注🔍
感谢大家的观看和支持!祝大家都能得偿所愿,天天开心!!!