C++的throw抛出异常机制

关于C++的“抛出异常”机制:

关键点:
(1)throw是将抛出的表达式的值拷贝到“异常对象”中,catch则是根据异常对象进行参数匹配并处理异常;
(2)throw可一次性跳出多层函数调用,直到最近一层的try语句,称为“栈展开”;
(3)catch捕获时是将异常对象与catch参数的进行 类型比较,而不是值比较,所以只要类型相同,就可以进入catch中处理。(例如throw抛出一个int类型的值,catch(int &i)就可以对其进行处理;或者throw抛出一个类对象,catch(Base& b)也可成功匹配)

所谓 “try”,就是 “尝试着执行一下”,如果有异常,则通过throw向外抛出,随后在外部通过catch捕获并处理异常。

0. 概述:

异常处理 将 问题的 “检测” 与 “解决” 过程分离开来。
程序的一部分负责检测问题的出现,然后解决该问题的任务传递给程序的另一部分。

检测环节无须知道处理模块的所有细节,处理模块也无须知道检测模块的细节。

这种机制允许在大型程序开发中 两个独立开发的部分能够在运行时 就出现的问题进行通信并做出相应的处理。

要想有效的使用异常处理,必须首先了解 当抛出异常时发生了什么、捕获异常时发生了什么,以及用来传递错误的对象的意义。

1. 抛出异常:

C++通过 “throw” 关键字 抛出一条表达式来触发一个异常。

throw的用法类似于return:
它通常作为 条件语句的一部分 或者 作为某个函数的最后一条语句,当throw执行时,跟在throw后面的语句将不再被执行,程序的控制权从throw转移到与之匹配的catch(catch可能是同一个函数中的局部catch,也可能位于调用链上的其他函数)。

try {
	throw expression;
}
catch(case 1) {

}
catch(case 2) {

}
catch(case 3) {

}
...

1.1 throw的处理过程:(栈展开)

throw语句一般位于try语句块内,当throw抛出一个异常时,程序暂停当前函数的执行过程,并寻找与try语句块关联的catch语句(类似 switch…case…),
如果这一步没找到匹配的catch,且这一层的try语句外部又包含着另一层try,则在外层try中继续寻找匹配的catch,如果找不到,则退出当前函数,在当前函数的外层函数中继续寻找匹配的try与catch。

上述过程被称为“栈展开”(stack unwinding)过程。

栈展开 过程沿着嵌套函数的调用链不断查找,直到找到匹配的catch 子句为止;
或者一直没有找到匹配的catch,则退出主函数终止查找过程(调用标准库函数terminate)。

如果找到了一个匹配的catch子句,则程序进入该子句并执行其中的代码,执行完成后回到到这个 try…catch… 的最后一个catch之后的位置继续向下执行。

1.2 析构函数 与 异常:

当异常发生调用throw,后面的语句将不会被执行,退出作用域时,作用域的局部对象都将会被释放,对于类对象,退出作用域时将自动调用它的析构函数。

因此,如果析构函数中有抛出异常的流程,应该要在析构函数内部try捕获,并在析构函数内部得到处理。

1.3 异常对象:

在编译器的管理空间中,会维护一种“异常对象”,专门用于抛出异常时使用。
当发生异常时,编译器会用throw 抛出的表达式的值 对 “异常对象” 进行拷贝初始化,当异常处理完毕后,编译器会将“异常对象”销毁。

所以,基于 异常对象 的这种处理机制,对抛出异常的处理有几点限制:
① 如果throw抛出的表达式是类类型,则此类必须要有可访问的 拷贝构造函数和 析构函数;(因为对 异常对象 进行拷贝初始化 以及 释放 异常对象的时候需要调用)
② throw抛出的异常对象 不能是指向局部对象的指针;(因为throw退出作用域后,局部对象随之被释放掉,抛出指针到外层后将无法访问所指向的局部对象)
③ throw抛出的表达式 为 此表达式的 静态编译类型,如果抛出的是一个指向类对象的基类指针,则派生类部分将被截断,只有基类部分被抛出。

2. 捕获异常:

使用 catch子句 捕获异常。

2.1 catch的参数 与 函数参数 类似:

① 如果catch参数是非引用类型,则该参数是“异常对象”的一个副本,在catch语句内修改该参数实际上改变的是局部副本的值而非异常对象本身;
② 如果catch参数是引用类型,则该参数就是“异常对象”的一个别名。此引用同样适用于继承体系下的对象传递:基类类型引用可用于绑定派生类对象。

2.2 catch的匹配规则:

按照其出现顺序逐一匹配,但 允许类型转换、允许派生类向基类的类型转换、允许非常量向常量的类型转换,所以越是专门的catch越应该置于判断列表的前端。
(如果多个catch语句之间存在继承关系,应该将派生类放在前面,基类放在后面(继承链最低端的类放在前面,最顶端的类放在后面))

2.3 重新抛出:

在catch捕获异常并开始处理后,如果一个单独的catch不能完整的处理某个异常,则当前catch会 向调用链更上一层 重新抛出异常(rethrowing),这次是一个 空的 throw语句
(空throw语句只能出现在catch语句中)

catch (my_error &eObj) {
	throw ;
}

如果catch的参数是引用类型,则 catch可将异常的内容修改后再向上层抛出

2.4 捕获所有异常:

catch(...) 表示捕获所有类型的异常:

try {

}
catch(...) {

}

3. 函数try语句块 与 构造函数:

如果想要处理 构造函数初始阶段(初始值列表中发生的错误)的异常,写法是:

Bob::Bob(string i1) try : data(i1) {

} catch(const bad_alloc &e) { handle_out_of_memory(e); }

注意 try 和 catch 的位置。

4. nonexcept异常说明:

明确指出某个函数不可能抛出异常,有助于简化调用该函数的代码。
如果我们能够提前知道这个函数没有throw机制,就不需要在调用该函数时编写任何异常处理的代码,否则如果有抛出异常而却没有进行捕获,则有可能造成程序调用 terminate 终止。

使用 nonexcept 表示函数不会抛出异常:

void recoup(int) nonexcept;	//不会抛出异常
void alloc(int);			//可能抛出异常

5. 异常类层次:

throw 可抛出 ① 自定义的异常对象,也可以抛出 ② 标准异常中的类型。

关于标准异常:

C++的throw抛出异常机制_第1张图片

exception是所有异常的父类。

bad_cast		: 通过dynamic_cast 抛出;
runtime_error	: 运行时异常,包括 3个自子类:
	overflow_error / underflow_error / range_error
logic_error		: 逻辑错误,包括 4个子类:
	domain_error / invalid_argument / out_of_range / length_error
bad_alloc		: new失败时,会抛出bad_alloc;

exception 标准异常有个比较好用的功能是可以通过 e.what() 打印出出错的位置(获取一个标识异常的字符串),例如:

#include 
#include 

int main() {

	std::deque<int> mydeq;
	mydeq.push_back(1);
	mydeq.push_back(2);

	try {
		mydeq.at(2);	//访问越界,deque内部实现了throw抛出异常
	}
	catch(const std::exception &e) {
		std::cerr << "exception: " << e.what() << std::endl;
	}

	return 0;
}

输出结果:

exception: deque

std::cout(标准输出) 与 std::cerr(标准错误输出) 的区别:

二者都是默认向屏幕输出,不同点在于:
stdout是输出到磁盘文件,默认情况下stdout是行缓冲的,它的输出会先放在一个buffer里面,只有到换行的时候才会输出到屏幕上;
而stderr是无缓冲的,直接输出到屏幕上。

6. C++抛出异常 相比于 “return error”的好处:

https://bbs.csdn.net/topics/391839431

简言之:在多层函数调用时,throw使代码更简洁、异常处理更可靠:
① throw可以一下子跳出多层的函数调用,内层异常,本层次不是需要处处捕获;
② return error 只能返回一层,需要层层传递,或者使用全局变量记录错误码,如果一层忽略,就失去了处理的机会。

7. 实际开发中的抛出异常应用举例:

void CHttpQuery::_QueryCreateGroup(const string& strAppKey, Json::Value& post_json_obj, CHttpConn *pHttpConn) {

	try {

	}
	catch(std::runtime_error msg) {
		
	}
}


void CImConn::OnRead() {

	try {

	}
	catch (CPduException& ex) {
		log_error();
		if(pPdu) {
			delete pPdu;
			pPdu = nullptr;
		}
		OnClose();
	}
}

简单使用举例:

#include 
using namespace std;

int main() {
	int 	ival  = 5;
	char 	cchar = 'c';
	char 	dchar = 'd';

	try {
		throw cchar;
	}
	catch(int) {
		cout << "int excpetion" << endl;
	}
	catch(char &c) {
		if(c == 'c') {
			cout << "cchar excpetion" << endl;
		}
		else if(c == 'd') {
			cout << "dchar excpetion" << endl;
		}
	}
	return 0;
}

输出结果:

cchar excpetion

你可能感兴趣的:(C/C++)