当前位置: 首页 > news >正文

C++修炼:异常

         Hello大家好!很高兴我们又见面啦!给生活添点passion,开始今天的编程之路!

我的博客:<但凡.

我的专栏:《编程之路》、《数据结构与算法之美》、《题海拾贝》、《C++修炼之路》

欢迎点赞,关注!

        异常可以说是一种非常厉害的错误处理方式,不仅C++在用,java,python等也都在用。那么这期我们就深入学习一下异常。

目录

1、异常的概念

        1.1、什么是异常

        1.2、异常的抛出与捕获

        1.3、查找匹配的处理代码

基类与派生类异常定义

异常触发函数

异常捕获场景

关键点说明

扩展测试场景

        1.4、异常重新抛出       

        1.5、异常安全问题

        1.6、异常规范

2、标准库的异常

标准异常类

使用示例

自定义异常

异常处理建议


 

1、异常的概念

        1.1、什么是异常

        在C语言中,我们主要是通过错误码的形式处理错误。也就是说对各种错误进行编号。在C++中我们不用这么麻烦的方法去处理错误了,我们出现错误时,也就是异常时直接抛出一个对象,这个对象可以包含各种错误信息。

        1.2、异常的抛出与捕获

        当出现错误是,我们通过throw这个关键字来抛出一个异常对象,同时呢我们可以用catch关键字进行捕获。try也是异常处理机制的关键字,他一般和catch搭配使用。

        话不多说我们直接上代码:

#include<iostream>
using namespace std;
int division(int x, int y)
{if (y == 0){string s("Division by zero!");throw(s);}else{return x / y;}return 0;
}
int func1(int x,int y)
{try{cout << division(x, y) << endl;}catch (const char* ch){cout << ch << endl;cout << "Successful catch!" << endl;}return 0;
}
int func2(int x, int y)
{try{func1(x,y);}catch (string& s){cout << s << endl;cout << "Successful catch!" << endl;}return 0;
}
int main()
{func2(10, 5);func2(10, 0);return 0;
}

        在上面这个逻辑中,当除数为0是会发生错误,此时应该会抛出异常。 

        抛出异常后,会暂定当前程序,开始寻常与之匹配的catch子句。对于try catch语句,如果没有捕获到异常,就会继续在外层调用函数链中查找,也就是返回上一层函数。一直查找,直到异常被捕获。那么这个查找的过程就叫做栈展开。

        如果某个异常一直没有被捕获,当他找不到任何能够和他匹配的捕捉时就会报错终止程序。

终止程序是通过标准库函数terminate来实现的。

        上述的栈展开的过程都是在编译时进行的。

        下面我们拿func2(10,0)来举例看看栈展开的过程是什么样的。

        当捕获之后,后续的代码正常执行。比如说我们上面的代码在func2中被捕获了,那么继续执行func2中catch之后的代码,也就是return 0。 

        一个try可以匹配多个catch:

	try{cout << division(x, y) << endl;}catch (const char* ch){cout << ch << endl;cout << "Successful catch!" << endl;}catch (string& s){cout << s << endl;cout << "Successful catch!" << endl;}

        当有多个catch与异常匹配的时候,距离抛出异常最近的catch优先捕获。 并且还有两点需要注意:1、沿着调用链中的其他函数可能提前退出。2、一旦程序开始处理异常,沿着调用链创建的对象都将销毁。

        抛出异常对象后,会生成一个异常对象的拷贝,因为异常对象可能是局部对象。这个拷贝对象在catch子句之后销毁。

        1.3、查找匹配的处理代码

        一般情况下catch是和抛出对象完全匹配的。但是也有例外,捕获允许权限缩小(常量向非常量转换)、数组向指向数组元素类型的指针,函数向指向函数的指针,但最重要的是允许派生类向基类转换。

        我们在捕获异常时,可以设置一个基类,再设置几个典型的异常去继承这个基类,那么对于捕获来说,我们直接捕获基类,这个基类的所有子类就都被捕获了。如果我们不需要捕获这个基类的异常,可以直接把这个基类定义成抽象类。

        以下是一个展示C++异常捕获子类的经典场景示例,通过基类异常和派生类异常的分层处理,体现异常捕获的多态性和优先级机制。

基类与派生类异常定义

#include <iostream>
#include <stdexcept>// 基类异常
class BaseException : public std::runtime_error {
public:BaseException(const std::string& msg) : std::runtime_error(msg) {}
};// 派生类异常
class DerivedException : public BaseException {
public:DerivedException(const std::string& msg) : BaseException(msg) {}
};

异常触发函数

void simulateError(int code) {if (code == 1) {throw DerivedException("Derived Exception Occurred");} else if (code == 2) {throw BaseException("Base Exception Occurred");} else {throw std::runtime_error("Generic Error");}
}

异常捕获场景

int main() {try {simulateError(1); // 触发派生类异常} catch (const DerivedException& e) {std::cerr << "Caught DerivedException: " << e.what() << std::endl;}catch (const BaseException& e) {std::cerr << "Caught BaseException: " << e.what() << std::endl;}catch (const std::exception& e) {std::cerr << "Caught Generic Exception: " << e.what() << std::endl;}return 0;
}

关键点说明

  • 捕获顺序:派生类异常(DerivedException)的catch块必须放在基类(BaseException)之前,否则派生类异常会被基类catch块截获。
  • 多态性:所有异常类型最终继承自std::exception,可通过基类引用捕获任意派生类异常。
  • 输出示例
    当调用simulateError(1)时,输出结果为:
    Caught DerivedException: Derived Exception Occurred
    

扩展测试场景

修改main()中的simulateError参数,测试不同异常类型:

simulateError(2); // 触发基类异常
simulateError(3); // 触发通用异常

        我们为了防止到main函数中无法捕获异常而终止程序,一般会在main函数中使用catch(...)捕获,可以捕捉到任意类型的异常。但是我们无法判断异常错误是什么。

        1.4、异常重新抛出       

        捕获异常后,可以直接在 catch 块中重新抛出该异常。

        我们在后面介绍标准库的异常之后再实现一下异常重新抛出的场景。

        1.5、异常安全问题

        在异常抛出之后,当前程序之后的代码就不会执行了。倘若在抛出异常之前向内存申请了一些资源,这部分资源不会被释放,此时会造成内存泄漏。我们可以通过智能指针来解决这个问题,我们下一期会介绍智能指针。其实智能指针就是让每个申请的内存在出作用域时自己释放资源(通过析构函数实现)。

#include <iostream>
using namespace std;void test()
{int* arr1 = new int[10];int* arr2 = new int[10];int* arr3 = new int[10];throw 1;delete[] arr1;//内存泄漏delete[] arr2;delete[] arr3;
}int main() {try{test();}catch (...){cout << "successful catch" << endl;}return 0;
}

         在上面这个场景中会造成内存泄漏。而且不仅如此,还有一个更严重的问题,就是new本身也会抛异常。但是如果我们对于每次new都进行异常捕获并且重新抛出的话,对于连续多个new的话写起来太麻烦了。这个也是可以使用智能指针来解决。

        另外就是析构函数中对抛异常也要进行谨慎处理,否则也可以会导致资源释放不完全而内存泄漏。

        1.6、异常规范

        在C++11之后如果某个函数不会抛异常,我们只需要在函数列表之后加noexcpt。但是抛异常的函数也可以不加noexcept,并不会报错。但是如果某个函数标明了noexcept,但是却抛出了异常,编译器会调用terminate终止程序,因为他会认为这个异常没有被捕获:

#include <iostream>
using namespace std;void test()noexcept//程序终止
{int* arr1 = new int[10];int* arr2 = new int[10];int* arr3 = new int[10];throw 1;delete[] arr1;//内存泄漏delete[] arr2;delete[] arr3;
}int main() {try{test();}catch (...){cout << "successful catch" << endl;}return 0;
}

        还可以用noexcept()来判断一个表达式是否会抛异常。可能抛异常返回false,否则返回true。

#include <iostream>
using namespace std;void test()//程序终止
{int* arr1 = new int[10];int* arr2 = new int[10];int* arr3 = new int[10];throw 1;delete[] arr1;//内存泄漏delete[] arr2;delete[] arr3;
}int main() {cout << noexcept(test()) << endl;//输出0return 0;
}

         需要注意的是,上面的代码中noexcept只是检测test会不会抛出异常,但是并没有真正执行test函数,所以也不会终止程序。

2、标准库的异常

        C++标准库中的异常主要分为两类:标准异常类和自定义异常类。标准异常类均继承自std::exception,提供统一的错误处理接口。

标准异常类

  1. 逻辑错误(std::logic_error
    表示程序逻辑错误,通常在编写代码时可避免。常见子类:

    • std::invalid_argument:参数无效
    • std::out_of_range:超出有效范围
    • std::length_error:超出容器最大长度
  2. 运行时错误(std::runtime_error
    表示运行时无法避免的错误。常见子类:

    • std::overflow_error:算术溢出
    • std::underflow_error:算术下溢
    • std::system_error:系统调用错误
  3. 其他异常

    • std::bad_alloc:内存分配失败(如new操作)
    • std::bad_cast:动态类型转换失败(dynamic_cast

使用示例

#include <iostream>
#include <stdexcept>
#include <vector>int main() {try {std::vector<int> v(5);v.at(10) = 1; // 抛出 std::out_of_range} catch (const std::out_of_range& e) {std::cerr << "Error: " << e.what() << std::endl;} catch (const std::exception& e) {std::cerr << "General error: " << e.what() << std::endl;}
}

自定义异常

继承std::exception或其子类可自定义异常:

class MyException : public std::runtime_error {
public:MyException(const std::string& msg) : std::runtime_error(msg) {}
};try {throw MyException("Custom error");
} catch (const MyException& e) {std::cerr << e.what() << std::endl;
}

        接下来我们补一下之前提到的异常重新抛出场景:

#include <iostream>
#include <stdexcept>void riskyOperation() {throw std::runtime_error("Original error occurred");
}void intermediateFunction() {try {riskyOperation();}catch (...) {std::cout << "Logging the error before rethrow" << std::endl;throw;  // 重新抛出}
}int main() {try {intermediateFunction();}catch (const std::exception& e) {std::cout << "Caught in main: " << e.what() << std::endl;}
}

        当然在实践中我们一般是捕获到特定类型的异常再重新抛出。以上只是一个最基本的异常重新抛出场景。

        好了,今天的内容就分享到这,我们下期再见! 

 

http://www.lryc.cn/news/574476.html

相关文章:

  • 解码成都芯谷金融中心文化科技产业园:文化+科技双轮驱动
  • Qt 中使用 gtest 做单元测试
  • 一文读懂微观测量:光学3D轮廓仪与共聚焦显微成像的结合应用
  • cherry-pick除了使用命令,有没有什么工具可以使用,或者更高效的方法
  • Linux 文件 I/O 与标准 I/O 缓冲机制详解
  • Java面试中被深挖过的线程问题
  • 对手机屏中断路和短路的单元进行切割或熔接,实现液晶线路激光修复原理
  • Luckysheet Excel xlsx 导入导出互相转换
  • 02-Linux内核源码编译
  • CentOS 7 编译安装Nginx 1.27.5完整指南及负载均衡配置
  • MinIO中视频转换为HLS协议并进行AES加密
  • Python Polars库详解:高性能数据处理的新标杆
  • pyqt多界面
  • LangChain网页自动化PlayWrightBrowserToolkit
  • gRPC 静态库链接到 DLL 的风险与潜在问题
  • 鸿蒙开发深入解析:Service Ability(后台任务)全面指南
  • 深度解析|智能汽车操作系统技术突破:从架构演进到产业重构
  • 比翼双飞,影像的迁徙之旅
  • 基于目标驱动的分布式敏捷开发
  • GPPT(Graph Pre-training and Prompt Tuning)项目复现
  • 生成FUCK代币,并用程序进行转账的过程
  • C++字符串的行输入
  • 查询sqlserver数据库中,数据占的空间和索引占的空间
  • 鸿蒙HarmonyOS 5 开发实践:LazyForEach在通讯录应用中的高效渲染(附:代码)
  • 前端vue2每三十秒被动接受后端服务器发送过来得数据
  • 前端react使用 UmiJS 构建框架 在每次打包时候记录打包时间并在指定页面显示
  • Linux 启动过程流程图
  • PDF全能转换工具,支持图片转PDF,多图合并转PDF,word转PDF,PDF转WORD,PDF转图片
  • TouchDIVER Pro触觉手套:虚拟现实中的多模态交互新选择
  • Flask(五) 表单处理 request.form