C++之异常

news2024/11/15 6:09:14

文章目录

  • 一、C 语言传统的处理错误的方式
  • 二、C++ 异常概念
  • 三、异常的使用
    • 1.异常的抛出和捕获
    • 2.异常的重新抛出
    • 3.异常安全
    • 4.异常规范
  • 四、自定义异常体系
  • 五、C++ 标准库的异常体系
  • 六、异常的优缺点

一、C 语言传统的处理错误的方式

传统的错误处理机制:
  ① 终止程序,如 assert 。缺陷:用户难以接受。如发生内存错误或除 0 错误时就会终止程序。
  ② 返回错误码,如系统的很多库的接口函数都是通过把错误码放到 errno 中,表示错误。缺陷:需要程序员自己去查找对应的错误。

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

整体而言,C 语言处理错误的方式都不是很好。

二、C++ 异常概念

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

其实不止是 C++,很多语言都是使用异常这种处理错误的方式,比如 Java 和 Python 。

  • throw:当问题出现时,程序会使用 throw 关键字抛出一个对象。
  • try:try 块中的代码标识将被激活的特定异常,它后面通常跟着一个或多个 catch 块。
  • catch:用于捕获异常,可以有多个 catch 块进行捕获。

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

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

三、异常的使用

1.异常的抛出和捕获

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

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


我们可以通过下面的程序来认识异常的使用。
测试代码:

#include <iostream>
#include <string>
using namespace std;

double Division(int a, int b)
{
	// 当b == 0时抛出异常
	if (b == 0)
		throw "Division by zero condition!";
	else
		return ((double)a / (double)b);
}

void Func2()
{
	int len, time;
	cin >> len >> time;
	if (time != 0)
	{
		throw 3.33;
	}
	else
	{
		cout << len << " and " << time << endl;
	}
}

void Func1()
{
	try
	{
		int len, time;
		cin >> len >> time;
		cout << Division(len, time) << endl;
	}
	catch (const string& errmsg1)
	{
		cout << errmsg1 << endl;
	}
	catch (const char* errmsg2)
	{
		cout << errmsg2 << endl;
	}
	catch (int errid)
	{
		cout << errid << endl;
	}
	
	Func2();
}

int main()
{
	try 
	{
		Func1();
		
		int a;
		cin >> a;
		if (a == 2)
		{
			string str("For test");
			throw str;
		}
		else
		{
			cout << a << endl;
		}
	}
	catch (const string& errmsg)
	{
		cout << errmsg << endl;
	}
	catch (int errid)
	{
		cout << errid << endl;
	}
	catch (...) 
	{
		cout << "unkown exception" << endl;
	}

	return 0;
}

 ① 如果在 Func1() 中输入的 time == 0,则会在 Division() 中抛出一个异常对象,匹配 Func1() 中的 catch (const char* errmsg2) 语句进行异常处理,接着继续往后执行 Func2() 。
 ② 如果在 Func2() 中输入的 time != 0,则会抛出一个异常对象,匹配 main() 中的 catch (…) 语句进行异常处理。
 ③ 如果在 Func2() 中输入的 time == 0,则 Func2() 不会抛出异常,Func2() 正常结束。若在 main() 中输入的 a == 2,则会抛出一个异常对象,匹配 main() 中的 catch (const string& errmsg) 语句进行异常处理。

2.异常的重新抛出

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

测试代码:

#include <iostream>
#include <string>
using namespace std;

double Division(int a, int b)
{
	// 当b == 0时抛出异常
	if (b == 0)
	{
		throw "Division by zero condition!";
	}
	return (double)a / (double)b;
}

void Func()
{
	/* 这里可以看到如果发生除0错误抛出异常,另外下面的array没有得到释放。
	   所以这里捕获异常后并不处理异常,异常还是交给外面处理,这里捕获了再
	   重新抛出去。*/
	int* array = new int[10];

	int len, time;
	cin >> len >> time;
	
	try
	{
		cout << Division(len, time) << endl;
	}
	catch (...)  // 拦截异常,不是要处理异常
	{
		cout << "delete []" << array << endl;
		delete[] array;
		throw;  // 捕获到什么对象就重新抛出什么对象
	}

	cout << "delete []" << array << endl;
	delete[] array;
}

int main()
{
	try
	{
		Func();
	}
	catch (const char* errmsg)
	{
		cout << errmsg << endl;
	}

	return 0;
}

3.异常安全

  • 构造函数完成对象的构造和初始化,最好不要在构造函数中抛出异常,否则可能导致对象不完整或没有完全初始化。

  • 析构函数主要完成资源的清理,最好不要在析构函数内抛出异常,否则可能导致资源泄漏(内存泄漏、句柄未关闭等)。

  • C++ 中异常经常会导致资源泄漏的问题,比如在 new 语句和 delete 语句之间抛出了异常导致内存泄漏,在 lock 和 unlock 之间抛出了异常导致死锁,C++ 经常使用 RAII 来解决以上问题。

4.异常规范

异常规范说明的目的是为了让函数使用者知道该函数可能抛出的异常有哪些。

  1. 可以在函数的后面接 throw(异常类型) ,列出这个函数可能抛掷的所有异常类型。
  2. 函数的后面接 throw() ,表示该函数不抛异常。
    C++11 中新增的 noexcept ,也表示不会抛异常。
  3. 若无异常接口声明,则此函数可以抛掷任何类型的异常。
// 这里表示该函数会抛出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 (void* ptr) throw();

// C++11中新增的noexcept,表示不会抛异常
thread() noexcept;
thread (thread&& x) noexcept;

但异常规范在实际中很难执行。

四、自定义异常体系

在实际使用中,很多公司都会自定义自己的异常体系进行规范的异常管理,因为一个项目中如果大家随意抛异常,那么外层的调用者基本就没办法玩了,所以实际中都会定义一套继承的规范体系。这样大家抛出的都是继承的派生类对象,捕获一个基类就可以了。

在这里插入图片描述

#include <iostream>
#include <thread>
#include <string>
#include <time.h>
using namespace std;

// 服务器开发中通常使用的异常继承体系
class Exception
{
public:
	Exception(const string& errmsg, int id)
		:_errmsg(errmsg)
		,_id(id)
	{}

	virtual string what() const
	{
		return _errmsg;
	}

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
	{
		string str = "SqlException:";
		str += _errmsg;
		str += "->";
		str += _sql;

		return str;
	}

private:
	const string _sql;
};

class CacheException : public Exception
{
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(0));
	if (rand() % 7 == 0)
	{
		throw SqlException("权限不足", 100, "select * from exam_result where name = '张三'");
	}
}

void CacheMgr()
{
	srand(time(0));
	if (rand() % 5 == 0)
	{
		throw CacheException("权限不足", 100);
	}
	else if (rand() % 6 == 0)
	{
		throw CacheException("数据不存在", 101);
	}

	SQLMgr();
}

void HttpServer()
{
	srand(time(0));
	if (rand() % 3 == 0)
	{
		throw HttpServerException("请求资源不存在", 100, "get");
	}
	else if (rand() % 4 == 0)
	{
		throw HttpServerException("权限不足", 101, "post");
	}

	CacheMgr();
}

void ServerStart()
{
	while (1)
	{
		this_thread::sleep_for(chrono::seconds(1));

		try
		{
			HttpServer();
		}
		catch (const Exception& e)  //这里捕获父类对象就可以
		{
			// 多态
			cout << e.what() << endl;
		}
		catch (...)
		{
			cout << "Unknown Exception" << endl;
		}
	}
}


int main()
{
	ServerStart();

	return 0;
}

五、C++ 标准库的异常体系

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

在这里插入图片描述

说明: 实际中我们可以去继承exception类实现自己的异常类。但是实际中很多公司像上面一样自己定义一套异常继承体系,因为 C++ 标准库设计的不够好用。

测试代码:

#include <iostream>
#include <vector>
using namespace std;

int main()
{
	try 
	{
		vector<int> v(10, 5);
		// 这里如果系统内存不够也会抛异常
		v.reserve(1000000000);
		
		// 这里越界会抛异常
		v.at(10) = 100;  //v[10] = 100;是使用assert检查的,直接终止程序
	}
	catch (const exception& e) // 这里捕获父类对象就可以
	{
		cout << e.what() << endl;
	}
	catch (...)
	{
		cout << "Unknown Exception" << endl;
	}

	return 0;
}

六、异常的优缺点

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

C++ 异常的缺点:
 ① 异常会导致程序的执行流乱跳,而且非常的混乱,并且是运行时出错抛异常就会乱跳。这会导致我们跟踪调试以及分析程序时,比较困难。
 ② 异常会有一些性能的开销。当然在现代硬件速度很快的情况下,这个影响基本忽略不计。
 ③ C++ 没有垃圾回收机制,资源需要自己管理。有了异常非常容易导致内存泄漏、死锁等异常安全问题。这个需要使用 RAII 来处理资源的管理问题,有一定的学习成本。
 ④ C++ 标准库的异常体系定义得不好,导致大家各自定义自己的异常体系,非常的混乱。
 ⑤ 异常尽量规范使用,否则后果不堪设想,随意抛异常,外层捕获的用户苦不堪言。所以异常规范有两点:抛派生类异常对象和对接口函数声明抛异常规范。

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

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

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

相关文章

JUC(十)-线程池-ThreadPoolExecutor分析

ThreadPoolExecutor 应用 & 源码解析 文章目录ThreadPoolExecutor 应用 & 源码解析一、线程池相关介绍1.1 为什么有了JDK提供的现有的创建线程池的方法(Executors类中的方法),然而还需要自定义线程池ThreadPoolExecutor 提供的七个核心参数大致了解JDK提供的几种拒绝策…

一辆适合长途出行的电动跑车 奥迪RS e-tron GT正式上市

作为奥迪品牌电动化发展的先锋力作&#xff0c;奥迪RS e-tron GT不止是前瞻科技的呈现&#xff0c;在e-tron纯电技术的加持下&#xff0c;更传递着RS的情怀&#xff0c;承载着人们对GT豪华休旅生活的向往。 2022年12月30日&#xff0c;伴随着Audi Channel第九期直播节目盛大开播…

MySQL存储引擎介绍以及InnoDB引擎结构理解

目录存储引擎概述各个存储引擎介绍InnoDBMySIAMMemeory其他引擎引擎有关的SQL语句InnoDB引擎逻辑存储结构架构内存部分磁盘部分后台线程InnoDB三大特性存储引擎概述 数据引擎是与数据真正存储的磁盘文件打交道的&#xff0c;它的上层&#xff08;服务层&#xff09;将处理好的…

我的Python学习笔记:私有变量

一、私有变量的定义 在Python中&#xff0c;有以下几种方式来定义变量&#xff1a; xx&#xff1a;公有变量_xx&#xff1a;单前置下划线&#xff0c;私有化属性或方法&#xff0c;类对象和子类可以访问&#xff0c;from somemodule import *禁止导入__xx&#xff1a;双前置下…

掌握Python中列表生成式的五个原因

1. 引言 在Python中我们往往使用列表生成式来代替for循环&#xff0c;本文通过引入实际例子&#xff0c;来阐述这背后的原因。 闲话少说&#xff0c;我们直接开始吧&#xff01; 2. 简洁性 列表生成式允许我们在一行代码中创建一个列表并对其元素执行相应的操作&#xff0…

(十五)大白话我们每一行的实际数据在磁盘上是如何存储的?

文章目录 1、前情回顾2、真实数据是如何存储的?3、隐藏字段4、初步的把磁盘上的数据和内存里的数据给关联起来1、前情回顾 之前我们已经给大家讲过了,一行数据在磁盘文件里存储的时候,包括如下几部分: 首先会包含自己的变长字段的长度列表然后是NULL值列表接着是数据头然后…

图的概念及存储结构

文章目录图的概念图(graph)有向图(directed graph)无向图(undirected graph)加权图(weighted graph)无向完全图(undirected complete graph)有向完全图(directed complete graph)子图(subgraph)稀疏图与稠密图度路径与回路连通图与连通分量强连通图与强连通分量生成树图的存储结…

STM32H750自制开发板调试经验

​本篇只是一个记录&#xff0c;没啥可看的。 STM32H750硬件相关 STM32H750可以通过USB-OTG下载程序&#xff0c;也可以使用SWD进行调试&#xff0c;所以设计板子得时候将PA13和PA12预留出来即可&#xff0c;后续也可以用作usb虚拟串口&#xff08;CDC&#xff09;功能或者模拟…

stm32f407VET6 系统学习 day08 利用adc 模数转换 监控光敏电阻。

1. ADC 的知识 1.基本概念 &#xff1a; Analog-to-Digital Converter的缩写。指模/数转换器或者模拟/数字转换器。是指将连续变量的模拟信号转换为离散的数字信号的器件 。典型的模拟数字转换器将模拟信号转换为表示一定比例电压值的数字信号。 2.STM32F4x ADC特点 1. 可配…

git操作

删除暂存区文件&#xff1a; git rm --cached 完整文件名 git rm --cached xxx.txt这个删&#xff0c;只是把暂存区里的文件删了&#xff0c;工作区里面的没有删 把本地文件添加到暂存区 git add完整文件名 例如&#xff1a;git add xxx.txt git add xxx.txt此时xxx.txt已经…

Linux 权限理解和学习

✨个人主页&#xff1a; Yohifo &#x1f389;所属专栏&#xff1a; Linux学习之旅 &#x1f38a;每篇一句&#xff1a; 图片来源 &#x1f383;操作环境&#xff1a; CentOS 7.6 阿里云远程服务器 Don’t argue with the people of strong determination, because they may ch…

AtCoder Beginner Contest 283 Ex. Popcount Sum(类欧经典问题:数x在二进制表示下第k位的值)

题目 t(t<1e5)组样例&#xff0c;每组样例给定n,m,r(1<m<n<1e9,0<r<m) 求[1,n]这n个数中&#xff0c;所有满足i%mr的数i的二进制的1的个数之和 即&#xff1a;&#xff0c; 其中&#xff0c;__builtin_popcount(i)统计的是i的二进制表示中&#xff0c;1的…

Web APIs

文章目录一. Web API介绍1. Web APIs 和 JS 基础关联性1.1 JS 的组成1.2 JS 基础阶段以及 Web APIs 阶段2. API的概念[3.Web API的概念](https://developer.mozilla.org/zh-CN/docs/Web/API)4. API 和 Web API 总结二. DOM 介绍1. DOM 简介1.1 什么是 DOM1.2 DOM 树2. 获取元素…

Linux-6 三剑客命令

Linux-6 三剑客命令 awk&#xff08;取列&#xff09; 将系统的IP地址打印出来 [rootdestiny ~]# yum install net-tools -y #分析&#xff1a;#1.肯定是需要拿到IP地址&#xff0c;仅看某一个特定的网卡&#xff1b;ifconfig#2.先想办法过滤出数据的那一行&#xff1b; ###行#…

5)Django Admin管理工具,Form组件,Auth

目录 一 Django Admin管理工具 激活管理工具 使用管理工具 复杂模型 自定义表单 内联(Inline)显示 列表页的显示 二 django Form组件 局部钩子和全局钩子 三 Django 用户认证&#xff08;Auth&#xff09;组件 一 Django Admin管理工具 Django 提供了基于 web 的管理…

年终报告撰写小技巧,你学会了吗?

年年岁岁花相似&#xff0c;岁岁年年人不同。 临近年底&#xff0c;又到了一年一度的年终报告时段了。同事间见面最让人头疼的问候&#xff0c;莫过于&#xff0c;“你的年终报告写了吗&#xff1f;” 有的人东拼西凑、应付了事&#xff0c;汇报内容乏善可陈&#xff0c;领导…

美美的圣诞树画出来-CoCube

2022年圣诞节到来啦&#xff0c;很高兴这次我们又能一起度过~ CSDN诚邀各位技术er分享关于圣诞节的各种技术创意&#xff0c;展现你与众不同的精彩&#xff01;参与本次投稿即可获得【话题达人】勋章【圣诞快乐】定制勋章&#xff08;1年1次&#xff0c;错过要等下一年喔&#…

尚医通-上传医院接口实现(十八)

目录&#xff1a; &#xff08;1&#xff09;上传医院接口-基础类的创建 &#xff08;2&#xff09;数据接口-上传医院接口-初步实现 &#xff08;3&#xff09;上传医院接口-最终实现 &#xff08;1&#xff09;上传医院接口-基础类的创建 复制相关的工具类&#xff1a;这…

Redis Windows版安装和使用

下载地址&#xff0c;亲已测试可放心使用 https://github.com/tporadowski/redis/releases Redis安装和基本使用&#xff08;windows版&#xff09; 1.Redis简介 完全开源免费的高性能的key-value的数据库 支持数据的持久化&#xff0c;可以将内存中的数据保存在磁盘中&…

【函数】一篇文章带你看懂控制流、递归、高阶函数

目录 控制流 条件语句 迭代语句 示例&#xff1a;质因数分解 递归 示例&#xff1a;阶乘 示例&#xff1a;斐波那契数列 示例&#xff1a;判断奇偶数 高阶函数 lambda 表达式 设计函数 示例&#xff1a;累加计算 示例&#xff1a;柯里化 Lab 1: Functions, Control …