C++ STL——异常
注:原创不易,转载请务必注明原作者和出处,感谢支持!
注:内容来自某培训课程,不一定完全正确!
一 C++异常机制概述
什么是异常处理?一句话,异常处理就是处理程序中的错误。
为什么需要异常处理以及异常处理的基本思想?
C++之父Bjarne Stroustrup在《The C++ Programming Language》中讲到:一个库的作者可以检测出发生了运行时错误,但一般不知道怎样去处理它们(因为和用户具体的应用有关);另一方面,库的用户知道怎样处理这些错误,但却无法检查它们何时发生(如果能检测,就可以在用户的代码里处理了,不用留给库去发现)。
Bjane Stroustrup说:提供异常的基本目的就是为了处理上面的问题。基本思想是:让一个函数在发现了自己无法处理的错误时抛出(throw)一个异常,然后它的(直接或间接)调用者能够处理这个问题。也就是说,C++将问题的检测与问题的处理相分离。
在异常处理机制出现之前的错误处理方式?在C语言中,对错误的处理总是围绕着两种方法:一是使用整型的返回值标识错误;二是使用errno宏(可以简单理解为一个全局整型变量)去记录错误。当然C++中仍然可以使用这两种方法。但是这两种方法最大的缺陷就是会出现不一致问题。例如有些函数返回1表示成功,返回0表示出错;而有些函数返回0表示成功,返回非0表示出错。还有一个缺点是一个函数的返回值只有一个,你通过函数的返回值标识错误代码,那么函数就不能返回其他的值。当然,你也可以通过指针或者C++的引用来返回另外的值,但这可能会令你的程序略微晦涩难懂。
异常的优点在哪里?
(1)函数的返回值可以忽略,但异常不可以忽略。如果程序出现异常,但是没有被捕获,程序就会终止,这多少回促使程序员开发出来的程序更健壮一点。而如果使用C语言的errno宏或者函数返回值,调用者都有可能忘记检查,从而没有对错误进行处理,结果造成程序莫名其妙地终止或者出现错误的结果。
(2)整型返回值没有任何语义信息。而异常却包含语义信息,有时你从类名就能够体现出来。
(3)整型返回值缺乏相关的上下文信息。异常作为一个类,可以拥有自己的成员,这些成员就可以传递足够的信息。
(4)异常处理可以在调用时跳级。这是一个代码编写时的问题:假设在有多个函数的调用栈中出现了某个错误,使用整型返回码要求你在每一级函数中都要进行处理。而使用异常处理的栈展开机制,只需在一处进行处理就可以了,不需要每级函数都处理。
下面是一个异常的基本语法实例。如果除数y为0,则抛出y的值。在Test1()中尝试捕获异常。
// 异常的基本语法
int Divide(int x, int y)
{
	if (y == 0)
	{
		// 抛出异常
		throw y;
	}
	return x / y;
}
void Test1(void)
{
	// 试着捕获异常
	try
	{
		Divide(10, 0);
	}
	// 异常是根据类型进行匹配的
	catch (int e)
	{
		cout << "Error : 除数为" << e << endl;
	}
}
异常可以跳级处理。比如在下面的这个例子中,函数的调用顺序为Test2() --> CallDivide() --> Divide()。异常在Divide()函数中被抛出,但异常的捕获却没有在CallDivide()中进行,而是放到了Test2()中进行。
int Divide(int x, int y)
{
	if (y == 0)
	{
		// 抛出异常
		throw y;
	}
	return x / y;
}
// CallDivide()并未对异常进行处理
void CallDivide(int x, int y)
{
	Divide(x, y);
}
// Divide()所抛出的异常在函数调用顶层Test2()中被捕获
// 如果Test2()中仍然没有捕获到Divide()所抛出的异常,则
// 异常会被抛到函数调用的最顶层main()函数中,如果在main()
// 中异常还没有被捕获并处理,则程序会终止执行!
void Test2(void)
{
	try
	{
		CallDivide(10, 0);
	}
	catch (int e)
	{
		cout << "错误:除数为" << e << endl;
	}
}
二 栈解旋(unwinding)
栈解旋是指当异常被抛出后,从进入try块起,到异常被抛出前,这期间在栈上构造的所有对象,都会被自动析构,析构的顺序与构造的顺序相反。这一过程被称为栈的解旋(unwinding)。
比如下面的例子,进入try语句块之后,先调用的是CallDivide(),在CallDivide()当中先是构建了Person对象p3,然后调用Divide(),在Divide()当中构建了对象p1和p2,直到异常的发生。
class Person
{
public:
	Person(string nm)
	{
		name = nm;
		cout << "Person对象" << name << "构建" << endl;
	}
	~Person()
	{
		cout << "Person对象" << name << "析构" << endl;
	}
private:
	string name;
};
int Divide(int x, int y)
{
	Person p1("p1");
	Person p2("p2");
	if (y == 0)
		throw y;
	return x / y;
}
void CallDivide()
{
	Person p3("p3");
	Divide(10, 0);
}
void Test01()
{
	try
	{
		CallDivide();
	}
	catch (int e)
	{
		cout << "有异常发生!" << endl;
	}
}
上面程序的输出如下,可以看到构建的顺序和析构的顺序刚好相反。
Person对象p3构建
Person对象p1构建
Person对象p2构建
Person对象p2析构
Person对象p1析构
Person对象p3析构
有异常发生!
三 异常接口的声明
(1)为了加强程序的可读性,可以在函数声明中列出可能抛出异常的所有类型,例如:void func() throw (A, B, C),这个函数func能够且只能抛出类型A,B,C及其子类型的异常。
(2)如果在函数声明中没有包含异常接口声明,则此函数可以抛出任何类型的异常
(3)一个不抛出任何异常的函数可以声明为:void func() throw ()
(4)如果一个函数抛出了它的异常接口声明所不允许抛出的异常,则unexcepted函数会被调用,该函数默认行为调用terminate()函数中断程序。
下面是一个异常接口声明的实例。
// 这个函数只能抛出int float char三种类型的异常,抛出其他异常就报错
void func1() throw (int, float, char)
{
	// 抛出char *会导致程序异常退出
	throw string("abc");
}
// 这个函数不能抛出任何异常
void func2() throw ()
{
	// 抛出异常会导致程序异常退出
	throw 1;
}
// 这个函数可以抛出任何类型异常
void func3()
{
	;
}
四 异常类型和异常变量的生命周期
throw的异常是有类型的,可以是数字、字符串、和类对象等。catch需要严格地匹配异常类型。
void fun1()
{
	throw 1;
}
void fun2()
{
	throw "exception";
}
class MyException
{
public:
	MyException(string msg)
	{
		error = msg;
	}
	void what()
	{
		cout << error << endl;
	}
private:
	string error;
};
void fun3()
{
	// 抛出匿名对象
	throw MyException("我刚写的异常!");
}
void Test1()
{
	try
	{
		fun1();
	}
	catch (int e)
	{
		cout << "int型异常!" << endl;
	}
	try
	{
		fun2();
	}
	catch (char *e)
	{
		cout << "char *型异常!" << endl;
	}
	try
	{
		fun3();
	}
	catch (MyException e)
	{
		// 对象MyException里封装了异常的相关信息
		e.what();
	}
}
下面进行异常变量生命周期分析。首先是使用普通的匿名对象去接抛出异常的情况。
class MyException
{
public:
	MyException()
	{
		cout << "MyException构造函数被调用" << endl;
	}
	MyException(const MyException & ex)
	{
		cout << "MyException拷贝构造函数被调用" << endl;
	}
	~MyException()
	{
		cout << "MyException析构函数被调用" << endl;
	}
};
void fun()
{
	// 抛出匿名异常对象
	throw MyException();
}
void Test()
{
	try
	{
		fun();
	}
	// 使用普通对象去接抛出的异常
	catch (MyException e)
	{
		cout << "异常捕获!" << endl;
	}
}
此时,程序输出结果如下。程序首先是在fun()中调用了MyException的构造函数创建了一个匿名对象,然后将该匿名对象抛出。然后在使用普通元素e接收抛出的异常对象时,调用了拷贝构造函数将匿名对象拷贝至e当中。然后是捕获异常,捕获之后,在catch语句中进行处理。当catch中处理完了异常之后再将对象e和匿名对象析构,所以输出两次“MyException析构函数被调用”。
MyException构造函数被调用
MyException拷贝构造函数被调用
异常捕获!
MyException析构函数被调用
MyException析构函数被调用
接下来是使用引用去接抛出的异常对象的情况。
void Test()
{
	try
	{
		fun();
	}
	// 使用引用去接抛出的异常
	catch (const MyException &e)
	{
		cout << "异常捕获!" << endl;
	}
}
此时程序输出如下。和使用普通元素e接收抛出异常对象相比,使用引用去接收抛出对象少了调用拷贝构造函数的步骤。因为,在catch时直接引用了匿名对象,所以从始至终,只要匿名对象被创建和析构。
我比较菜,我有疑问:参考下面的用指针去接收抛出的异常部分,为什么这里可以在catch语句块里去引用匿名对象?这个匿名对象不是在fun()里创建的吗?随着fun()调用完毕,退栈之后,匿名对象还存在吗?如果不存在,那你还怎么能够引用呢?如果存在,那说明这个匿名异常对象肯定不在栈内存中,不在栈内存中,那它在哪里?在堆区吗?
MyException构造函数被调用
异常捕获!
MyException析构函数被调用
下面是使用指针去接收抛出异常对象的错误示范。
void fun()
{
	// 抛出匿名对象的地址
	throw &(MyException());
}
void Test()
{
	try
	{
		fun();
	}
	// 使用指针去接抛出的异常
	catch (const MyException *e)
	{
		cout << "异常捕获!" << endl;
	}
}
此时,程序输出结果如下。可以看到,在catch块中进行异常处理之前,匿名对象就已经被析构了!此时指针e成了一个野指针,你再也无法通过e来获取e中封装的异常信息了!
MyException构造函数被调用
MyException析构函数被调用
异常捕获!
下面是使用指针去接收抛出异常对象的正确示范。为了防止匿名对象在进行异常处理之前被析构,你需要在堆中创建匿名对象,这也就意味着你需要手动地管理内存!
void fun()
{
	// 抛出匿名对象的地址,匿名对象在堆中创建
	throw new MyException();
}
void Test()
{
	try
	{
		fun();
	}
	// 使用指针去接抛出的异常
	catch (const MyException *e)
	{
		cout << "异常捕获!" << endl;
		// 千万别忘记手动释放匿名对象的内存!
		delete e;
	}
}
此时,程序终于输出正确了!
MyException构造函数被调用
异常捕获!
MyException析构函数被调用
五 C++标准异常库
C++标准异常库的成员

(1)在上述的继承体系中,每个类都提供了构造函数、拷贝构造函数和赋值操作符重载
(2)logic_error类及其子类、runtime_error类及其子类,它们的构造函数是接受一个string类型的形式参数,用于异常信息的描述
(3)所有的异常类都有一个what()方法,返回const char *类型(C风格字符串)的值,描述异常信息。
标准异常类的具体描述
| 异常名称 | 描述 | 
|---|---|
| exception | 所有标准异常类的父类 | 
| bad_alloc | 当operator new and operator new[]请求分配失败时 | 
| bad_exception | 这是个特殊的异常类,如果函数的异常抛出列表里声明了bad_exception异常,当函数内部抛出了异常列表中没有的异常,这时调用的unexcepted函数中若抛出异常,不论什么类型,都会被替换为bad_exception类型 | 
| bad_type | 使用typeid操作符,操作一个NULL指针,而该指针是带有虚函数的类,这时抛出bad_typeid异常 | 
| bad_cast | 使用dynamic_cast转换引用失败时 | 
| ios_base::failure | io操作过程中出现错误 | 
| logic_error | 逻辑错误,可以在运行前检测的错误 | 
| runtime_error | 运行时错误,仅在运行时才可以检测的错误 | 
logic_error的子类:
| 异常名称 | 描述 | 
|---|---|
| length_error | 试图生成一个超出该类型最大长度的对象时,例如vector的resize操作 | 
| domain_error | 参数的值域错误,主要用在数学函数中。例如使用一个负值调用只能操作非负值的函数 | 
| out_of_range | 超出有效范围 | 
| invalid_argument | 参数不合适。在标准库中,当利用string对象构造bitset时,而string中的字符不是'0'或者'1'的时候,抛出该异常 | 
runtime_error的子类:
| 异常名称 | 描述 | 
|---|---|
| range_error | 计算结果超出了有意义的值域范围 | 
| overflow_error | 算数计算上溢 | 
| underflow_error | 算数计算下溢 | 
| invalid_argument | 参数不合适。在标准库中,当利用string对象构造bitset时,而string中的字符不是'0'或者'1'的时候,抛出该异常 | 
编写自己的异常类
为什么要编写自己的异常类?
(1)标准库中的异常类是有限的
(2)在自己的异常类中,可以添加自己的信息(标准库中的异常类值允许设置一个用来描述异常的字符串)
如何编写自己的异常类?
(1)建议自己的异常类要继承标准异常类。因为C++中可以抛出任何类型的异常,所以我们的异常类可以不继承自标准异常,但是这样可能会导致程序混乱,尤其是当我们多人协同开发时
(2)当继承标准异常类时,应该重载父类的what()函数和虚析构函数
(3)因为栈展开的过程中,要复制异常类型,那么要根据你在类中添加的成员考虑是否提供自己的复制构造函数。
下面是一个C++标准异常库的应用实例
class Person
{
public:
	Person()
	{
		mAge = 0;
	}
	void setAge(int age)
	{
		if (age < 0 || age > 100)
			throw out_of_range("年龄应该在0到100之间");
		this->mAge = age;
	}
private:
	int mAge;
};
void Test()
{
	Person p;
	try
	{
		p.setAge(1000);
	}
	catch (const exception &e)
	{
		cout << e.what() << endl;
	}
}
下面是自己手写的异常类的应用实例。
class MyOutOfRangeException : public exception
{
public:
	MyOutOfRangeException(char *error)
	{
		pError = new char[strlen(error) + 1];
		strcpy_s(pError, strlen(error) + 1, error);
	}
	~MyOutOfRangeException()
	{
		if (pError != nullptr)
		{
			delete[] pError;
		}
	}
	virtual const char *what() const
	{
		return pError;
	}
private:
	char *pError;
};
void Call(void)
{
	throw MyOutOfRangeException("我自己的异常类!");
}
void Test()
{
	try
	{
		Call();
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
}
六 异常的继承
下面是一个自己手写异常继承的应用案例。
// 异常基类
class BaseMyException
{
public:
	virtual void what() = 0;
	virtual ~BaseMyException() {}
};
// 继承
class TargetSpaceNullException : public BaseMyException
{
public:
	virtual void what()
	{
		cout << "目标空间为空!" << endl;
	}
	~TargetSpaceNullException() {}
};
// 继承
class SourceSpaceNullException : public BaseMyException
{
public:
	virtual void what()
	{
		cout << "源空间为空!" << endl;
	}
	~SourceSpaceNullException() {}
};
void copy_str(char *target, char *source)
{
	if (target == nullptr)
	{
		throw TargetSpaceNullException();
	}
	if (source == nullptr)
	{
		throw SourceSpaceNullException();
	}
	while (*source != '\0')
	{
		*target = *source;
		target++;
		source++;
	}
	*target = '\0';
}
int main(int argc, char **argv)
{
	char *source = "abcdefg";
	char buf[1024] = { 0 };
	try
	{
		copy_str(nullptr, source);
	}
	catch (BaseMyException &e)
	{
		e.what();
	}
	cout << buf << endl;
	getchar();
	return 0;
}
												
											C++ STL——异常的更多相关文章
- NeHe OpenGL教程 第四十三课:FreeType库
		
转自[翻译]NeHe OpenGL 教程 前言 声明,此 NeHe OpenGL教程系列文章由51博客yarin翻译(2010-08-19),本博客为转载并稍加整理与修改.对NeHe的OpenGL管线 ...
 - SWIG 3 中文手册——9. SWIG 库
		
目录 9 SWIG 库 9.1 %include 指令与库搜索路径 9.2 C 数组与指针 9.2.1 cpointer.i 9.2.2 carrays.i 9.2.3 cmalloc.i 9.2.4 ...
 - <学习opencv>opencv数据类型
		
目录 Opencv数据类型: 基础类型概述 固定向量类class cv::Vec<> 固定矩阵类cv::Matx<> 点类 Point class cv::Scalar 深入了 ...
 - STL 跨模块 调用 异常 解决
		
本文为转载别人的,以作收藏之用 百度了一天,现在把结论放上边: 1.不要用STL(std::string属于STL)来跨模块传输数据,例如:dll(so)之间,dll(so)和exe(elf)之间. ...
 - STL容器能力一览表和各个容器操作函数异常保证
		
STL容器能力一览表 Vector Deque List Set Multiset map Multimap 典型内部 结构 dynamic array Array of arrays Doubly ...
 - 时间序列分析  异常分析 stl
		
https://blog.csdn.net/snowdroptulip/article/details/79125912 https://www.cnblogs.com/runner-ljt/p/52 ...
 - 【转】三十分钟掌握STL
		
转自http://net.pku.edu.cn/~yhf/UsingSTL.htm 三十分钟掌握STL 这是本小人书.原名是<using stl>,不知道是谁写的.不过我倒觉得很有趣,所以 ...
 - 容器使用的12条军规——《Effective+STL中文版》试读
		
容器使用的12条军规——<Effective+STL中文版>试读 还 记的自己早年在学校学习c++的时候,老师根本就没有讲STL,导致了自己后来跟人说 起会C++的时候总是被鄙视, ...
 - [c++] STL = Standard Template Library
		
How many people give up, because of YOU. Continue... 先实践,最后需要总结. 1. 数据流中的数据按照一定的格式<T>提取 ------ ...
 
随机推荐
- R 读取xls/xlsx文件
			
包readxl install.packages('readxl',repois='https://mirrors.utsc.edu.cn/CRAN/) library(readxl) # read_ ...
 - asp.net mvc5 DataBase First下model校验问题(MetadataType使用)
			
最近学习asp.net mvc5,使用 asp.net mvc5+EF6+AutoFac做个小Demo,其中是先设计的数据库表,就直接选择了EF的DataBase First(三种开发模式分别是c ...
 - vue父组件传值和子组件触发父组件方法
			
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.js"></script> <scr ...
 - 【JAVA各版本特性】JAVA 1.0 - JAVA 12
			
make JDK Version 1.01996-01-23 Oak(橡树) 初代版本,伟大的一个里程碑,但是是纯解释运行,使用外挂JIT,性能比较差,运行速度慢. JDK Version 1.119 ...
 - scrapy 增量采集
			
在做新闻或者其它文章采集到时候,只想采集最新发布的信息,之前采集过得就不要再采集了,从而达到增量采集到需求 scrapy-deltafetch,是一个用于解决爬虫去重问题的第三方插件. scrapy- ...
 - Centos7虚拟机根分区扩展
			
线上的kvm虚拟机,原来只规划了8G,后来发现硬盘动不动就被日志塞满了,需要进行扩容. 扩容步骤如下: 1.先把kvm虚拟机关机 2.在宿主机上进行kvm虚拟机的磁盘扩容 qemu-img resiz ...
 - 设计模式之Template Method
			
1.设计模式的使用场景 模板方法模式(Template Method) 解释一下模板方法模式,就是指:一个抽象类中,有一个主方法,再定义1…n个方法,可以是抽象的,也可以是实际的方法,定义一个类,继承 ...
 - java_八大数据类型
			
一.整型 1.byte 1个字节(8位--一个字节占8位)-128~127 2.short 2个字节 -32768~32767 3.int 4个字节(常用) 4.long 8个字节 ...
 - 【CEOI1999】Sightseeing trip
			
Description https://loj.ac/problem/10072 Solution 现在我连普及组题都不会了?(bushi) 懒得讲了,看这吧.
 - java与JSON
			
XML 格式数据极其的冗长.因为每个离散的数据片段需要大量的 XML 结构,所有有效 的数据的比例非常低.XML 语法还有轻微的模糊.还有,解析 XML 是非常占程序员的精力的.你需要提前了解详细的结 ...