【C++】C++异常

news2024/11/28 8:38:57

文章目录

  • 1. C语言传统处理错误的方式
  • 2. C++异常的概念
  • 3. 异常的使用
    • 3.1 异常的抛出和捕获
    • 3.2 异常的重新抛出
    • 3.3 异常安全
    • 3.4 异常规范
  • 4. C++标准库的异常体系
  • 5. 自定义的异常体系
  • 6. 异常的优缺点

1. C语言传统处理错误的方式

C语言传统的错误处理机制有两个:

  1. 终止程序,比如assert。

    缺陷:用户难以接收。如发生内存错误,除0错误时就会终止程序。

  2. 返回错误码

    缺陷:需要程序员自己去查找对应的错误。如系统的很多库的接口函数都是通过把错误码放到errno中,表示错误

实际中C语言基本都是使用返回错误码的方式处理错误,部分情况下使用终止程序处理非常严重的错误

2. C++异常的概念

异常是一种处理错误的方式当一个函数发现自己无法处理的错误时就可以抛出异常,让函数的直接或间接的调用者处理这个错误

  • throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。
  • catch: 在您想要处理问题的地方,通过异常处理程序捕获异常.catch 关键字用于捕获异常,可以有多个catch进行捕获。
  • try: try 块中的代码标识将被激活的特定异常,它后面通常跟着一个或多个 catch 块。

如果有一个块抛出一个异常,捕获异常的方法会使用 try 和 catch 关键字。try 块中放置可能抛出异常的代码,try 块中的代码被称为保护代码。使用try/catch语句的语法如下所示:

try
{
    //保护的标识代码
}
catch(ExceptionName e1)
{
    //catch块
}
catch(ExceptionName e2)
{
    //catch块
}
catch(ExceptionName eN)
{
    //catch块
}

3. 异常的使用

3.1 异常的抛出和捕获

异常的抛出和匹配原则

  • 异常是通过抛出对象引发的,该对象的类型决定了应该激活哪个catch的处理代码
  • 选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个
  • 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会生成一个拷贝对象,这个拷贝的临时对象会在被catch以后销毁。(这里的处理类似于函数的传值返回)
  • catch(…)可以捕获任意类型的异常,问题是不知道异常错误是什么
  • 在实际抛出和捕获异常的原则有个例外,并不都是类型完全匹配,可以抛出的派生类对象,使用基类捕获,这个在实际中非常实用

在函数调用链中异常栈展开匹配原则

  • 首先检查throw本身是否在try块内部,如果是再查找匹配的catch语句。如果有匹配的,则调用到catch的地方进行处理
  • 没有匹配到catch则退出当前函数栈,继续再上层调用函数的栈中进行查找匹配catch
  • 如果到达main函数的栈,依旧没有匹配的,则终止程序。上述这个沿着调用链查找匹配的catch子句的过程称为栈展开。所以实际中我们最后都要加一个catch(…)捕获任意类型的异常,否则当有异常没捕获,程序就会直接终止
  • 找到匹配的catch子句并处理之后,会继续沿着catch子句后面继续执行

image-20230811010149061

double Division(int a, int b)
{
	// 当b == 0时抛出异常
	if (b == 0)
		throw "Division by zero condition!";
	else
		return ((double)a / (double)b);
}
void Func()
{
	int len, time;
	cin >> len >> time;
	cout << Division(len, time) << endl;
}
void Test1()
{
	try 
	{
		Func();
	}
	catch (const char* errmsg)
	{
		cout << errmsg << endl;
	}
	catch (...) 
	{
		cout << "unkown exception" << endl;
	}
}

image-20230811010453845

3.2 异常的重新抛出

有可能单个的catch不能完全处理一个异常,在进行一些校正处理以后,希望再交给更外层的调用链函数来处理,catch则可以通过重新抛出将异常传递给更上层的函数进行处理。

我们看下面一段代码:

double Division(int a, int b)
{
	// 当b == 0时抛出异常
	if (b == 0)
		throw "Division by zero condition!";
	else
		return ((double)a / (double)b);
}
void Func()
{
	// 这里可以看到如果发生除0错误抛出异常,另外下面的array没有得到释放。
  // 所以这里需要捕获所有类型的异常,然后先将array释放,然后再将捕获的异常重新抛出,交给外面处理
	int* arr = new int[10];
	try
	{
		int len, time;
		cin >> len >> time;
		cout << Division(len, time) << endl;
	}
	catch (...)
	{
		cout << "delete []" << arr << endl;
		delete[] arr;
		throw;
	}
	//...
	cout << "delete []" << arr << endl;
	delete[] arr;
}
void Test2()
{
	try
	{
		Func();
	}
	catch (const char* errmsg)
	{
		cout << errmsg << endl;
	}
	catch (...)
	{
		cout << "unkown exception" << endl;
	}
}

3.3 异常安全

C++的异常还存在一些安全问题

  • 构造函数完成对象的构造和初始化,最好不要在构造函数中抛出异常,否则可能导致对象构造或者初始化不完全
  • 析构函数主要完成资源的清理操作,最好不要在析构函数中抛出异常,否则可能会导致资源泄漏(内存泄漏或者句柄未关闭等)。
  • C++中异常经常会导致资源泄漏的问题,比如在new和delete中抛出了异常,导致内存泄 漏,在lock和unlock之间抛出了异常导致死锁,C++经常使用RAII来解决以上问题

3.4 异常规范

由于不规范使用异常会带来许多非常严重的后果,所以 C++98 引入了异常规范,异常规范中建议程序员对每个函数进行异常接口说明,其目的是让函数使用者知道该函数可能抛出的异常有哪些,如下:

  1. 在函数后面接throw(类型)列出这个函数可能抛掷的所有异常类型
  2. 函数的后面接throw(),表示函数不抛异常
  3. 若无异常接口声明,则此函数可以抛掷任何类型的异常
//这里表示func可以抛出A,B,C,D四种类型的异常
void func() throw(A,B,C,D);
//这里表示这个函数只会抛出bad_alloc一种异常
void* operator new(std::size_t size) throw (std::bad_alloc);
//这里表示这个函数不会抛出异常
void* operator delete(std::size_t size, void* ptr) throw();

但是由于 C++98 函数异常接口只是建议性做法,而不是语法硬性要求的,同时还由于写出一个函数可能抛出的所有异常比较麻烦,所以 C++98 的异常规范在实际开发中几乎没有人遵守,形同虚设;

为了让人们能够对函数进行异常接口说明,C++11 对异常接口说明进行了简化

  • 在函数后面加上关键字noexcept表示该函数不会抛出任何异常
  • 函数后面不加关键字则表示可能抛出任意类型的异常
// C++11 中新增的 noexcept,表示不会抛异常
thread() noexcept;
thread(thread&& x) noexcept;

并且 C++11 还对使用 noexcept 修饰的函数进行了检查,如果该函数被 noexcept 修饰,但是可能会抛出异常,则编译器会报一个警告,但并不影响程序的正确性:

int at(vector<int>& v, size_t pos) noexcept
{
	if (pos >= v.size())
		throw out_of_range("数组越界");
	else
		return v[pos];
}

void Test1()
{
	vector<int> v = {1, 2, 3, 4, 5};
	try {
		at(v, 5);
	}
	catch (out_of_range& errmsg)
	{
		cout << errmsg.what() << endl;
	}
	catch (...) {
		cout << "未知异常" << endl;
	}
}

请添加图片描述

4. C++标准库的异常体系

C++提供了一系列标准的异常,定义在exception中,我们可以在程序中使用这些标准的异常。它们是以父子类层次结构组织起来的,如下所示:

img

请添加图片描述

其中比较常见的错误有:bad_alloc:new空间失败时抛出此异常;runtime_error:运行时错误,比如除0错误等;out_of_range数组越界错误。

C++标准库的异常体系的使用:

void Test2()
{
	vector<int> v(10);
	try {
		v.reserve(100000000);
		v.at(20) = 5;
	}
	catch (exception& errmsg)
	{
		cout << errmsg.what() << endl;
	}
	catch (...)
	{
		cout << "unknow Exception" << endl;
	}
}

请添加图片描述

虽然我们可以直接使用 C++ 标准提供的这些异常,也可以去继承 exception 类来实现自己的异常类,但在实际开发中很多企业都会自己定义一套单独的异常继承体系,因为C++标准库设计的不够好用。再加上我们平时自己写代码基本不会使用异常,所以对于 C++ 标准异常我们作为了解内容即可。

5. 自定义的异常体系

实际上,很多公司都会对自己定义的异常体系进行规范化管理,因为如果在一个项目中,大家随意的抛出异常,那么最外层的调用者就没法玩了,所以在实际中都会定义一套继承的规范体系:定义一个最基础的基类,所有人抛出的异常对象都是继承与该异常类的派生类对象,所以异常语法可以用基类捕获抛出的派生类对象,所以最外成值需要捕获基类就行了

img

下面我们看一下服务器开发中通常使用的异常继承体系

class Exception//这里定义了一个异常的基类,后面对于不同类型的异常,将进行不同的继承
{
public:
    Exception(const string& errmsg, int id)
        :_errmsg(errmsg)
        ,_id(id)
    {}
    virtual string what() const//这里的what使用virtual修饰,以达到多态使用,返回不同的错误信息
    {
        return _errmsg;
    }
protected:
    string _errmsg;
    int _id;
};
class SqlException : public Exception//sql类型的异常
{
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 += "->";
        str += _sql;
        return str;
    }
private:
    const string _sql;//保存出现错误的sql语句
};
class CacheException : public Exception//Cache类型异常
{
public:
    CacheException(const string& errmsg, int id)
        :Exception(errmsg, id)
    {}
    virtual string what() const//虚函数重写
    {
        string str = "CacheException:";
        str += _errmsg;
        return str;
    }
};
class HttpServerException : public Exception//网络类型异常
{
public:
    HttpServerException(const string& errmsg, int id, const string& type)
        :Exception(errmsg, id)
        , _type(type)
    {}
    virtual string what() const
    {
        string str = "HttpServerException:";
        str += _type;
        str += ":";
        str += _errmsg;
        return str;
    }
private:
    const string _type;
};
void SQLMgr()
{
    srand(time(nullptr));
    if (rand() % 4 == 0)
    {
        throw SqlException("权限不足", 100, "select * from name = '张三'");
    }
    //throw "xxxxxx";
}
void CacheMgr()
{
    srand(time(0));
    if (rand() % 5 == 0)
    {
        throw CacheException("权限不足", 100);
    }
    else if (rand() % 6 == 0)
    {
        throw CacheException("数据不存在", 101);
    }
    SQLMgr();//如果没有抛出异常就调用SQL层操作
}
void HttpServer()
{
    // ...
    srand(time(0));
    if (rand() % 3 == 0)
    {
        throw HttpServerException("请求资源不存在", 100, "get");
    }
    else if (rand() % 4 == 0)
    {
        throw HttpServerException("权限不足", 101, "post");
    }
    CacheMgr();//如果网络服务没有问题,在这里调用内存操作
}
void Test3()
{
    while(1)
    {
        this_thread::sleep_for(chrono::seconds(1));
        try{
            HttpServer();//这里调用网络服务
        }
        catch (const Exception& e) // 这里捕获父类对象就可以
        {
            // 多态
            cout << e.what() << endl;
        }
        catch (...)
        {
            cout << "Unkown Exception" << endl;
        }
    }
}

请添加图片描述

6. 异常的优缺点

C++异常的优点

  1. 异常对象定义好了,相比错误码的方式可以清晰准确的展示出错误的各种信息,甚至可以包含堆栈调用的信息,这样可以帮助更好的定位程序的bug
  2. 返回错误码的传统方式有个很大的问题就是,在函数调用链中,深层的函数返回了错误,那么我们得层层返回错误,最外层才能拿到错误;
  3. 很多的第三方库都包含异常,比如boost、gtest、gmock等等常用的库,那么我们使用它们也需要使用异常;
  4. 部分函数使用异常更好处理,比如构造函数没有返回值,不方便使用错误码方式处理。比如 T& operator这样的函数,如果pos越界了只能使用异常或者终止程序处理,没办法通过返回值表示错误

C++异常的缺点

  1. 异常会导致程序的执行流乱跳,并且非常的混乱,并且是运行时出错抛异常就会乱跳。这会导致我们跟踪调试时以及分析程序时,比较困难。
  2. 异常会有一些性能的开销。当然在现代硬件速度很快的情况下,这个影响基本忽略不计。
  3. C++没有垃圾回收机制,资源需要自己管理。有了异常非常容易导致内存泄漏、死锁等异常安全问题。这个需要使用RAII来处理资源的管理问题。学习成本较高。
  4. C++标准库的异常体系定义得不好,导致大家各自定义各自的异常体系,非常的混乱。
  5. 异常尽量规范使用,否则后果不堪设想,随意抛异常,外层捕获的用户苦不堪言。所以异常规范有两点:
    1. 抛出异常类型都继承自一个基类;
    2. 函数是否抛异常、抛什么异常,都使用 func() throw();的方式(或者C++11的noexcept)规范化。

总结:异常总体而言,利大于弊,所以工程中我们还是鼓励使用异常的。另外OO的语言基本都是用异常处理错误,这也可以看出这是大势所趋


本节完…

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/866727.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

IoTDB 小白“踩坑”心得:入门安装部署篇

小伙伴介绍&#xff01; 大家好&#xff0c;我是 zai&#xff0c;一个基本功不那么扎实、没有太多经验的大学生。我刚刚加入社区&#xff0c;接触 IoTDB&#xff0c;目前仍处于学习阶段&#xff0c;所以我会跟大家分享我学习过程中踩过的一些雷&#xff0c;以及对应的解决办法&…

【百度翻译api】中文自动翻译为英文

欸&#xff0c;最近想做一些nlp的项目&#xff0c;做完了中文的想做做英文的&#xff0c;但是呢&#xff0c;国内爬虫爬取的肯定都是中文 &#xff0c;爬取外网的技术我没有尝试过&#xff0c;没有把握。所以我决定启用翻译&#xff0c;在这期间chatGPT给了我非常多的方法&…

文件上传以及yml的配置

目录 一、存储本地 二、存储到阿里云 三、配置文件信息 一、存储本地 MultipartFile 常见方法&#xff1a; String getOriginalFilename(); //获取原始文件名 void transferTo(File dest); //将接收的文件转存到磁盘文件中 long getSize(); //获取文件的大小&#xff0c;单…

Vue2-绑定样式、条件渲染、列表渲染、列表过滤、模糊查询、Vue监测数据原理

&#x1f954;&#xff1a;想只有苦难&#xff0c;做才有答案 更多Vue知识请点击——Vue.js VUE2-Day3 绑定样式1、class绑定2、绑定style样式 条件渲染1、v-show2、v-if条件渲染案例 列表渲染1、v-for2、key的作用与原理&#xff08;重要&#xff09;面试题&#xff1a;react、…

Android 高手进阶教程(二)之----Android 数据库SQLiteDatabase的使用!!

直接进入主题~ Android 提供了三种数据存储方式&#xff0c;第一种是文件存储;第二种是SharedPreferences 存储;第三种就是数据库SQLiteDatabase 存储。 文件存储我就不用多说了&#xff0c;而SharedPreferences 可以存取简单的数据(int,double,float.etc)&#xff0c;它经常…

数字孪生有哪些应用场景?

数字孪生技术正在越来越普遍。根据艾瑞咨询2023年调查&#xff0c;2022年中国数字孪生市场规模为104亿&#xff0c;同比增长35.0%。随着各行业数字化转型的推进&#xff0c;国内未来数字孪生市场规模将继续增长&#xff0c;预计2023年国内市场规模将达到375亿元。 数字孪生是指…

图片预览插件vue-photo-preview的使用

移动端项目中需要图片预览的功能&#xff0c;但本身使用mintui&#xff0c;vantui中虽然也有&#xff0c;但是为了一个组件安装这个有点儿多余&#xff0c;就选用了vue-photo-preview插件实现&#xff08;其实偷懒也不想自己写&#xff09;。 1、安装 npm i vue-photo-preview…

学以致用:python面向对象和PyEcharts的完美混合技

文章目录 学习目标数据案例分析数据内容需求分析参考代码data_define.pyfile_define.pymain.py 学习目标 使用面向对象思想完成数据读取和处理基于面向对象思想重新认知第三方库使用(PyEcharts) 数据案例分析 数据内容 1月份数据是普通文本&#xff0c;使用逗号分割数据记录&…

生态系统服务(InVEST模型)

第一天&#xff1a; 1. 生态系统服务理论联系实践案例讲解 2. InVEST模型的开发历程、不同版本的差异及对数据需求的讲解 3. InVEST所需数据的要求&#xff08;分辨率、格式、投影系统等&#xff09;、获取及标准化预处理讲解 4. InVEST运行常见问题及处理解决方法讲解 5.…

基于 CentOS 7 构建 LVS-DR 群集 配置nginx负载均衡

环境配置&#xff1a; RHCE客户机192.168.100.146node1lvs192.168.100.145node2RS192.168.100.147node3RS192.168.100.148 配置ipvsadm httpd&#xff1a; [rootnode1 ~]# yum install ipvsadm.x86_64 [rootnode2 ~]# yum install http -y [rootnode2 ~]# systemctl …

Ubuntu18.04版本安装ROS及出现错误的处理方法

前面的文章是在已安装的ROS基础上做的一些应用&#xff0c;这里我们从零开始安装ROS机器人操作系统。 机器人操作系统(Robot Operating System,ROS)是一个开发机器人软件的框架&#xff0c;里面包含了一系列的工具&#xff0c;库和惯例&#xff0c;目的在于简化在大量不同种类机…

从一道面试题来学习前台进程和后台进程、孤儿进程和僵尸进程

1、面试题介绍 以前面试&#xff0c;面试官问了一个问题&#xff0c;大意是&#xff1a; 我们在终端中&#xff0c;通过执行 python main.py 命令&#xff0c;会启动一台前台进程直到程序结束。现在我还是想通过执行 python main.py &#xff0c;启动一个后台进程&#xff0c;…

k8s的yaml文件管理

声明式管理方法&#xff1a; 1.适合于对资源的修改操作2.声明式资源管理方法依赖于资源配置清单文件对资源进行管理资源配置清单文件有两种格式&#xff1a;yaml&#xff08;人性化&#xff0c;易读&#xff09;&#xff0c;json&#xff08;易于api接口解析&#xff09;3.对资…

【实操】2023年npm组件库的创建发布流程

2022年的实践为基础&#xff0c;2023年我再建一个组件库【ZUI】。步骤回顾&#xff1a; 2022年的npm组件包的发布删除教程_npm i ant-design/pro-components 怎么删除_啥咕啦呛的博客-CSDN博客 1.在gitee上创建一个项目,相信你是会的 2.创建初始化项目&#xff0c;看吧&#…

SpringBoot使用自定义事件监听器的demo

记录一下SpringBoot自定义事件监听器的使用方法 案例源码:SpringBoot使用自定义事件监听器的demo 使用的SpringBoot2.0.x版本 <parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><…

【Opencv入门到项目实战】(十):项目实战|文档扫描|OCR识别

所有订阅专栏的同学可以私信博主获取源码文件 文章目录 1.引言1.1 什么是光学字符识别 (OCR)1.2 应用领域 2.项目背景介绍3.边缘检测3.1 原始图像读取3.2 预处理3.3 结果展示 3.轮廓检测4.透视变换5.OCR识别5.1 tesseract安装5.2 字符识别 1.引言 今天我们来看一个OCR相关的文…

开发工具IDEA的下载与初步使用【各种快捷键的设置,使你的开发事半功倍】

&#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 接下来看看由辉辉所写的关于IDEA的相关操作吧 目录 &#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 一.IDEA的简介以及优势 二.IDEA的下载 1.下…

释放马氏距离的力量:用 Python 探索多元数据分析

一、说明 马哈拉诺比斯距离&#xff08;Mahalanobis Distance&#xff09;是一种测量两个概率分布之间距离的方法。它是基于样本协方差矩阵的函数&#xff0c;用于评估两个向量之间的相似程度。Mahalanobis Distance考虑了数据集中各个特征之间的协方差&#xff0c;因此比欧氏距…

skynet 网络模块解析

文章目录 前言环境准备sneak peek线程数据结构会话对象&#xff1a;持有基础套接字&#xff0c;封装了套接字的基础操作。会话管理器&#xff1a;持有并管理会话池&#xff0c;给外部模块提供网络接口。 网络模块管理会话管理器的生命周期管理工作模式 总结技术点原子数据管道描…

漫话拥塞控制:BBR 是个单流模型

概要(便于检索主题)&#xff1a;单流&#xff0c;多流收敛&#xff0c;probe buffer 挤压带宽&#xff0c;maxbw-filter wnd。 我曾经经常说 BBR 是个单流模型&#xff0c;而不是多流收敛模型&#xff0c;也做过不少评论&#xff0c;最近在复听 IETF 的大会&#xff0c;在 IET…