【初阶与进阶C++详解】第二十三篇:异常(异常抛出+异常捕获+异常优缺点)

个人主页:企鹅不叫的博客

专栏

  • C语言初阶和进阶
  • C项目
  • Leetcode刷题
  • 初阶数据结构与算法
  • C++初阶和进阶
  • 《深入理解计算机操作系统》
  • 《高质量C/C++编程》
  • Linux

⭐️ 博主码云gitee链接:代码仓库地址

⚡若有帮助可以【关注+点赞+收藏】,大家一起进步!

系列文章

【初阶与进阶C++详解】第一篇:C++入门知识必备

【初阶与进阶C++详解】第二篇:C&&C++互相调用(创建静态库)并保护加密源文件

【初阶与进阶C++详解】第三篇:类和对象上(类和this指针)

【初阶与进阶C++详解】第四篇:类和对象中(类的六个默认成员函数)

【初阶与进阶C++详解】第五篇:类和对象下(构造+static+友元+内部类

【初阶与进阶C++详解】第六篇:C&C++内存管理(动态内存分布+内存管理+new&delete)

【初阶与进阶C++详解】第七篇:模板初阶(泛型编程+函数模板+类模板+模板特化+模板分离编译)

【初阶与进阶C++详解】第八篇:string类(标准库string类+string类模拟实现)

【初阶与进阶C++详解】第九篇:vector(vector接口介绍+vector模拟实现+vector迭代器区间构造/拷贝构造/赋值)

【初阶与进阶C++详解】第十篇:list(list接口介绍和使用+list模拟实现+反向迭代器和迭代器适配)

【初阶与进阶C++详解】第十一篇:stack+queue+priority_queue+deque(仿函数)

【初阶与进阶C++详解】第十二篇:模板进阶(函数模板特化+类模板特化+模板分离编译)

【初阶与进阶C++详解】第十三篇:继承(菱形继承+菱形虚拟继承+组合)

【初阶与进阶C++详解】第十四篇:多态(虚函数+重写(覆盖)+抽象类+单继承和多继承)

【初阶与进阶C++详解】第十五篇:二叉树搜索树(操作+实现+应用KVL+性能+习题)

【初阶与进阶C++详解】第十六篇:AVL树-平衡搜索二叉树(定义+插入+旋转+验证)

【初阶与进阶C++详解】第十七篇:红黑树(插入+验证+查找)

【初阶与进阶C++详解】第十八篇:map_set(map_set使用+multiset_multimap使用+模拟map_set)

【初阶与进阶C++详解】第十九篇:哈希(哈希函数+哈希冲突+哈希表+哈希桶)

【初阶与进阶C++详解】第二十篇:unordered_map和unordered_set(接口使用+模拟实现)

【初阶与进阶C++详解】第二十一篇:哈希应用(位图实现应用+布隆过滤器增删查优缺点+海量数据面试题)

【初阶与进阶C++详解】第二十二篇:C++11新特性(列表初始化+变量类型推到+右值引用+新增默认成员函数+可变模板参数+lambda表达式+包装器function_bind)


文章目录

  • 系列文章
  • 一、异常概念
  • 二、异常处理
    • 1.异常的抛出
    • 2.异常的捕获
      • catch类型对应
      • catch(...)
      • 基类捕获派生类的异常(自定义异常类型)
    • 3.异常重新抛出
    • 4.异常安全
    • 5.异常规范
  • 三、C++标准库的异常体系
  • 四、异常的优缺点


一、异常概念

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

  • throw 在出现问题的地方抛出异常,可以抛出任意类型的异常
  • try 监控后续代码中出现的异常,后续需要以catch作为结尾
  • catch 用于捕获异常,同一个try可以用多个不同类型的catch进行捕获

二、异常处理

1.异常的抛出

在c++中,使用throw来抛出异常。抛出异常的语法如下:

throw "错误";

异常抛出后,接下来就要捕获异常和处理异常

2.异常的捕获

在c++中,提供了语句try…catch语句来捕获异常,其中try和catch分别用于定义异常和异常处理。

  • 定义异常时将可能产生错误的语句放在try语句块中.

  • 定义异常处理是将异常处理的语句放在语句块中,以便异常被传递来时处理,注意类型匹配.

  • try {
      可能出错的语句
    }
    catch(异常类型声明){
      异常类型处理
    }
    
    

catch类型对应

如果抛出的对象是string,那么就会找到catch中的异常类型声明为string的语句块,如果抛出的对象是int类型,那么就会找到catch中异常类型声明为int语句块,以下函数func1抛出异常被第一个catch捕获,func2抛出异常被第二个catch捕获。

需要注意的是,抛出异常后,func1和func2在抛出异常之后的位置不会执行了,会跳转到对应的catch位置,处理完异常后继续执行。

抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以生成的一个拷贝对象,这个拷贝对象catch执行语句块后就会被销毁,所以捕获列表捕获到的是异常的拷贝.

如果我们单独写一个函数,而这个函数体内有try/catch的话,那么会直接和这个最近的匹配,并不会和main函数里面的匹配

void func1()
	{
		int i = 0;
		cin >> i;
		if (i == 0)
		{
			throw 1;
		}
	}
	void func2()
	{
		int i = 0;
		cin >> i;
		if (i == 0)
		{
			throw "错误";
		}
	}
	int main()
	{
		try
		{
			func1();
			func2();
		}
  	//func1
		catch(const int x)
		{
			cout << "int 错误" << endl;
		}
 	//func2
		catch (const string str)
		{
			cout << "str 错误" << endl;
		}
	}

catch(…)

catch(…)可以捕获任意类型的异常,但是我们不知道捕获异常的类型,抛出的异常如果在当前的函数战阵中没有找到相匹配的catch语句,那么就会依次根据函数调用的路线进行返回上一层函数栈,继续查找catch语句

catch(...)
{
	cout<<"未知异常"<<endl;
}

基类捕获派生类的异常(自定义异常类型)

当我们出现异常的时候,如果throw了一个子类对象,可以用基类的引用来接接收,子类报错,父类捕获。

项目中一般都是会定义一个继承的规范异常体系,用于处理不同的异常,这样我们就只需要捕获一个基类对象,就能捕获到所有派生类的异常对象。

class A {
	int a;
};
class B : public A {
	int b;
public:
	B()
		:b(1)
	{}
};

void testab()
{
	B bt;
	throw bt;
}

int main()
{
	try {
		testab();
	}
 //基类捕获派生类异常
	catch (A& e) {
		cout << "err class A" << endl;
	}
	catch (...) {
		cout << "err" << endl;
	}

	return 0;
}

3.异常重新抛出

void testab()
{
	B bt;
	throw bt;
}

void testD()
{
	int* arr = new int[10];
	testab();
	delete[] arr;
}

以上程序会在执行testab()函数时,抛出异常,导致函数testD()函数提前终止,但是我们new开辟的空间还没有释放,此时会造成内存泄漏此时们就需要提前进行异常处理,如果出现问题,先释放我们new的资源之后,再将异常重新抛出。可以理解为是提前拦截异常

void tsetD()
{
int* arr = new int[10];
try
{
 testtab();
}
catch(...)
{
 cout<<"未知错误"<<endl;
 delete[] arr;
 throw;//重新抛出异常
}
delete[] arr;
}

4.异常安全

  • 构造函数完成对象的构造和初始化,最好不要在构造函数中抛出异常,否则可能导致对象不完整或没有完全初始化
  • 析构函数主要完成资源的清理,最好不要在析构函数内抛出异常,否则可能导致资源泄漏
  • 多线程操作中在lock与unlock之间抛出异常,导致死锁,智能指针解决

5.异常规范

  • 异常规格说明的目的是为了让函数使用者知道该函数可能抛出的异常有哪些。 可以在函数的后面接****throw(类型),****列出这个函数可能抛掷的所有异常类型。

  • 函数的后面接****throw()****,表示函数不抛异常。

  • 若无异常接口声明,则此函数可以抛掷任何类型的异常。

     void func1()throw();//表示该函数不会抛出异常
     void func2()throw(int,char,string);//表示该函数会抛出int,char,string类型的异常
    //在C++11中还新增了一个关键字noexcept来标识不会抛出异常
    void* test2(size_t sz, void* p) noexcept;
    
    

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

image-20221003162517595

标准异常类成员:

  • 在上述继承体系中,每个类都有提供了构造函数、复制构造函数、和赋值操作符重载。
  • logic_error类及其子类、runtime_error类及其子类,它们的构造函数是接受一个string类型的形参,用于异常信息的描述
  • 所有的异常类都有一个what()方法,返回const char* 类型描述异常信息
  • catch中包含exception类型,就能捕获到c++库中可能抛出的所有异常。

标准异常类的具体描述:

异常名称 描述
exception 所有标准异常类的父类
bad_alloc 当operator new and operator new[],请求分配内存失败时
bad_exception 这是个特殊的异常。如果函数的异常抛出列表里声明了bad_exception异常,而函数内部抛出了异常抛出列表中没有的异常,不论什么类型,都会被替换为bad_exception类型
bad_typeid 使用typeid操作符,操作一个NULL指针,而该指针是带有虚函数的类,这时抛出bad_typeid异常
bad_cast 使用dynamic_cast转换引用失败的时候
ios_base::failure io操作过程出现错误
logic_error 逻辑错误,可以在运行前检测的错误
runtime_error 运行时错误,仅在运行时才可以检测的错误

logic_error的子类:

异常名称 描述
length_error 试图生成一个超出该类型最大长度的对象时,例如很长的string
domain_error 参数的值域错误,主要用在数学函数中。例如使用一个负值调用只能操作非负数的函数
out_of_range 超出有效范围,vetor的at抛出了此异常
invalid_argument 参数不合适。在标准库中,当利用string对象构造bitset时,而string中的字符不是’0’或’1’的时候,抛出该异常
future_error(C++11) 此类将引发的对象类型定义为异常,以报告对未来对象或可能访问未来共享状态的库的其他元素的无效操作。

runtime_error的子类:

异常名称 描述
range_error 计算结果超出了有意义的值域范围
overflow_error 算术计算上溢
underflow_error 算术计算下溢
system_error(C++11) 运行时从操作系统或其他具有关联error_code的低级应用程序接口引发的异常

四、异常的优缺点

优点:

  • 异常对象定义完备之后,相比于错误码的方式,能让用户更加清楚的了解到自己遇到了什么类型的问题,更好定位程序的bug
  • 函数错误码若遇到,需要层层向外返回;而异常则通过catch可以直接跳到对应处理位置
  • 第三方库包含异常,我们在使用类似于boost/gtest等第三方库的时候也需要使用对应的异常处理
  • 对于T& operator[]这种操作符重载,我们没办法很好地使用返回值来标识错误(因为不同类型的返回值不一样,没办法统一处理)这时候就可以用异常来抛出越界问题

缺点:

  • 异常可能会导致程序到处乱跳(因为会跳到最近的catch位置)给观察错误情况增添了一些难度
  • 异常有一定性能开销(可忽略)
  • 异常容易导致资源泄漏等等问题
  • 异常依赖于用户编程规范,否则函数调用容易出现异常没有得到处理的问题

针对缺点,C++没有垃圾回收器,所以申请内存和释放内存要很小心,容易内存泄漏。标准库定义异常不好用,所以公司会自己定义一套


你可能感兴趣的:(#,C++初阶和进阶,c++,java,开发语言)