大家好,又见面了,我是全栈君,祝每个程序员都可以多学几门语言。
用于大型程序的工具
—异常处理
引言:
C++语言包括的一些特征在问题比較复杂,非个人所能管理时最为实用。如:异常处理、命名空间和多重继承。
相对于小的程序猿团队所能开发的系统需求而言,大规模编程[往往涉及数千万行代码]对程序设计语言的要求更高。大规模应用程序往往具有下列特殊要求:
1.更严格的正常运转时间以及更健壮的错误检測和错误处理。错误处理常常必须跨越独立开发的多个子系统进行[异常处理]。
2.能够用各种库(可能包括独立开发的库)构造程序[命名空间]。
3.能够处理更复杂的应用概念[多重继承&虚继承]。
异常处理
使用异常处理,程序中独立开发的各部分就能够就程序运行期间出现的问题相互通信,并处理这些问题。程序的一个部分能够检測出本部分无法解决的问题,这个问题检測部分就能够将问题传递给准备处理问题的其它部分。
【注解】
通过异常我们能够将问题的检測和问题的解决分离,这样程序的问题检測部分能够不必了解怎样处理问题。
C++的异常处理中,须要由问题检測部分抛出一个对象给处理代码,通过这个对象的类型和内容,两个部分就能够就出现了什么错误进行通信。
如:前面以前介绍过的一个样例:
Sales_itemoperator+(const Sales_item &lsh,const Sales_item &rhs){ if (!lsh.same_isbn(rhs)) { throw runtime_error("Data must refer to same ISBN"); } Sales_item ret(lsh); ret += rhs; return ret;}
程序中将Sales_item对象相加的部分能够使用一个try块,以便在异常发生时捕获异常:
Sales_item item1,item2,sum; while (cin >> item1 >> item2) { try { sum = item1 + item2; } catch(const runtime_error &e) { cerr << e.what() << " Try again.\n" << endl; } }
一、抛出类类型的异常
异常是通过抛出对象而引发的。该对象的类型决定应该激活哪个处理代码。被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置近期的那个。
异常以相似于将实參传递给函数的方式抛出和捕获。异常能够是可传给非引用形參的随意类型的对象,这意味着必须能够复制该类型的对象。
不存在数组或函数类型的异常。相反,假设抛出一个数组,被抛出的对象转换为指向数组首元素的指针,相似的,假设抛出一个函数,函数转换为指向该函数的指针。
运行throw时,不会运行跟在throw后面的语句,而是将控制从throw转移到匹配的catch,该catch能够是同一函数中局部的catch,也能够在直接或间接基类调用发生异常的函数的还有一个函数中。控制从一个地方传到还有一地方,这有两个重要含义:
1)沿着调用链的函数提早退出。
2)一般而言,在处理异常的时候,抛出异常的块中的局部存储不存在了。
由于在处理异常的时候会释放局部存储,所以被抛出的对象就不能在局部存储,而是用throw表达式初始化一个称为异常对象的特殊对象。异常对象由编译器管理,并且保证驻留在可能被激活的随意catch都能够訪问的空间。这个对象由throw创建,并被初始化为被抛出的表达式的副本。异常对象将传给相应的catch,并且在全然处理了异常之后撤销。
【小心地雷】
异常对象通过复制被抛出表达式的结果创建,该结果必须是能够复制的类型。
1、异常对象与继承
当抛出一个表达式时,被抛出对象的静态编译时类型将决定异常对象的类型。
通常,使用静态类型抛出对象不成问题。当抛出一个异常的时候,通常在抛出点构造将抛出的对象,该对象表示出了什么问题,所以我们知道确切的异常类型。
2、异常与指针
假设指针指向继承层次中的一种类型,指针所指对象的类型就有可能与指针的类型不同。不管对象的实际类型是什么,异常对象的类型都与指针的静态类型相匹配。假设该指针是一个指向派生类对象的基类类型指针,则那个对象将被切割,仅仅抛出基类部分。
谨记:抛出指向局部对象的指针总是错误的,因此,在抛出指针的时候,必须确定进入处理代码时指针所指向的对象存在。
【小心地雷】
抛出指针一般是个坏主意:抛出指针要求在相应处理代码存在的随意地方存在指针所指向的对象。
//P582 习题17.1 range_error r("error"); throw r; //异常对象类型为 range_error exception *p = &r; throw *p; //被异常对象是对指针p进行解引用的结果,其类型与p的静态类型相匹配,为exception
二、栈展开
抛出异常的时候,将暂停当前函数的运行。首先检查throw本身是否在try块内部,假设是,则检查与该try相关的catch子句,看是否当中之中的一个与被抛出对象相匹配。假设找到匹配的catch,就处理异常;假设找不到,就退出当前函数(释放当前函数的内存并撤销局部对象),并且继续在调用函数中查找。
假设对抛出异常的函数的调用是在try块中,则检查与该try相关的catch子句。假设找到匹配的catch,就处理异常;假设找不到匹配的catch,调用函数也退出,并且继续在调用这个函数的函数中查找。
这个过程,称之为栈展开,沿嵌套函数调用继续向上,直至为异常找到一个catch子句。仅仅要找到能够处理异常的catch子句,就进入该catch子句,并在该处理代码中继续运行。当catch结束的时候,在紧接在与该try块相关的最后一个catch子句之后的点继续运行。
1、为局部对象调用析构函数
栈展开期间,提早退出包括throw的函数和调用链中可能的其它函数。在释放内存之前,撤销在异常发生之前所创建的全部对象。假设局部对象是类类型的,就自己主动调用该对象的析构函数。通常,编译器不撤销内置类型的对象。
【小心地雷】
栈展开期间,释放局部对象所用的内存并运行类类型局部对象的析构函数。
假设一个块直接分配资源,并且在释放资源之前发生异常,在栈展开期间将不会释放该资源。比如,一个块能够通过调用new动态分配内存,假设该块因异常而退出,编译器不会删除该指针,已分配的内在将不会释放。
由类类型对象分配的资源通常会被适当地释放。运行局部对象的析构函数,由类类型对象分配的资源通常由它们的析构函数释放。
2、析构函数应该从不抛出异常
在为某个异常进行栈展开的时候,析构函数假设又抛出自己的未经处理的还有一个异常,将会导致调用标准库terminate函数。一般而言,terminate函数将调用abort函数,强制从整个程序非正常退出。
由于terminate函数结束程序,所以析构函数做不论什么可能导致异常的事情通常都是很糟糕的主意。在实践中,由于析构函数释放资源,所以它不太可能抛出异常。标准库类型都保证它们的析构函数不会引发异常。
3、异常与构造函数
构造函数内部所作的事情常常会抛出异常。在构造函数内部,即使对象仅仅是部分被构造了,也要保证将会适当的撤销已构造的成员。
相似地,在初始化数组或其它容器类型的元素的时候,也可能发生异常,相同,也要保证将会适当地撤销已构造的元素。
4、未捕获的异常终止程序
不能不处理异常。异常是足够重要的、使程序不能继续正常运行的事件。假设找不到匹配的catch,程序就调用库函数terminate[你懂得。。。]!
三、捕获异常
catch子句中的异常说明符看起来像仅仅包括一个形參的形參表,异常说明符是在其后跟一个(可选)形參名的类型名。
说明符的类型决定了处理代码能够捕捉的异常种类。类型必须是全然类型,即必须是内置类型或者是已经定义了的程序猿自己定义的类型。类型的前向声明不行。
当catch为了处理异常仅仅须要了解异常的类型的时候,异常说明符能够省略形參名;假设处理代码须要已发生异常的类型之外的信息,则异常说明符就包括形參名,catch使用这个名字訪问异常对象。
1、查找匹配的处理代码
在查找匹配的catch期间,找到的catch不必是与异常最匹配的那个,相反,将选中第一个找到的能够处理该异常的catch。因此,在catch子句列表中,最特殊的catch必须最先出现。
异常与catch异常说明符匹配:大多数转换都不同意 —除以下几种可能的差别之外,异常的类型与catch说 明符的类型必须全然匹配:
1)同意从非const到const的转换。也就是说,非const对象的 throw能够与指定接受const引用的 catch匹配。
2)同意从派生类型型到基类类型的转换。
3)将数组转换为指向数组类型的指针,将函数转换为指向函数类型的适当指针。
在查找匹配catch的时候,不同意其它转换。详细而言:既不同意标准算术转换,也不同意为类类型定义的转换[好绝情%>_<%]。
2、异常说明符
进入catch的时候,用异常对象初始化catch的形參。像函数形參一样,异常说明符类型能够是引用。异常对象本身是被抛出对象的副本。是否再次将异常对象拷贝到catch位置取决于异常说明符类型。
假设说明符不是引用,就将异常对象拷贝到catch形參中,对形參所做的不论什么改变都仅仅作用于副本,不会作用于异常对象本身。假设说明符是引用,则像引用形參一样,不存在单独的catch对象,
catch形參仅仅是异常对象的还有一名字。对catch形參所做的改变作用于异常对象。
3、异常说明符与继承
像形參声明一样,基类的异常说明符能够用于捕获派生类型的异常对象,并且,异常说明符的静态类型决定catch子句能够运行的动作。假设被抛出的异常对象是派生类类型的,但由接受基类类型的catch处理,那么,catch不能使用派生类特有的不论什么成员。
【最佳实践】
通常,假设catch子句处理因继承而相关的类型的异常,它就应该将自己的形參定义为引用。此时catch对象的静态类型能够与catch对象所引用的异常对象的动态类型不同。
假设catch对象是基类类型对象而异常对象是派生类型的,就将异常对象切割为它的基类子对象。
对象(相对于引用)不是多态的。对象的静态类型和动态类型相同,函数是虚函数也一样。仅仅有通过引用或指针调用时才发生动态绑定,通过对象调用不进行动态绑定。
4、catch子句的次序必须反映类型层次
将异常类型组织成类层次的时候,用户能够选择应用程序处理异常的粒度级别。比如,仅仅希望清除并退出的应用程序能够定义一个try块,该try块包围main函数中带有例如以下catch代码:
catch(exception &e) { cerr << "Exiting: " << e.what() << endl; size_t status_indicator = 42; return(status_indicator); }
有更严格实时需求的程序可能须要更好的异常控制,这种应用程序将清除导致异常的一切并继续运行。
由于catch子句按出现次序匹配,所以使用来自继承层次的异常的程序将它们的catch子句排序,以便派生类型的处理代码出如今其基类类型的catch之前。
【注解】
带有因继承而相关的类型的多个catch子句,必须从最低层派生类型到最高派生类型排序。例如以下题:
//P585 习题17.3 try { //... } catch(overflow_error eobj) { //... } catch(const runtime_error &re) { //... } catch(exception) { }
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/118441.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...