版权声明:此文章转载自infocool
原文链接:http://www.infocool.net/kb/CPlus/201607/163175.html
如需转载请联系听云College团队成员小尹 邮箱:yinhy#tingyun.com
引言
在程序设计过程中,我们总是希望自己设计的程序是天衣无缝的,但这几乎又是不可能的。即使程序编译通过,同时也实现了所需要的功能,也并不代表程序就已经完美无缺了,因为运行程序时还可能会遇到异常,例如当我们设计一个为用户计算除法的程序时,用户很有可能会将除数输入为零,又例如当我们需要打开一个文件的时候确发现该文件已经被删除了……类似的这种情况很有很多,针对这些特殊的情况,不加以防范是不行的。
我们通常希望自己编写的程序能够在异常的情况下也能作出相应的处理,而不至于程序莫名其妙地中断或者中止运行了。在设计程序时应充分考虑各种异常情况,并加以处理。
在C++中,一个函数能够检测出异常并且将异常返回,这种机制称为抛出异常。当抛出异常后,函数调用者捕获到该异常,并对该异常进行处理,我们称之为异常捕获。
C++新增throw关键字用于抛出异常,新增catch关键字用于捕获异常,新增try关键字尝试捕获异常。通常将尝试捕获的语句放在 try{ } 程序块中,而将异常处理语句置于 catch{ } 语句块中。
异常处理机制
C++提供的异常处理机制可以允许程序员自己对异常进行捕获,根据捕获到的异常类型,自己进行处理,这样使得程序员对于自己的程序具有更大的控制权限。
异常处理需要以下三个关键字:try、throw、catch。
基本的异常处理程序框架
try { //可能出现异常的代码块 } catch(类型名1 [形参名]) //这里形参名可以不出现 { //捕获到 类型名1 的异常时的异常处理程序 } catch(类型名1 [形参名]) { //捕获到 类型名2 的异常时的异常处理程序 } ... catch(...) { //三个点 表示可以捕获任何异常 }
首先一个异常的抛出使用 throw,语法为:
throw 表达式
使用的时候,将可能会抛出异常的语句块包含在try{}语句块中,如果try{}语句块中的程序发现了异常并且抛出此异常的话,那么这个异常可以被catch{} 语句进行捕获并处理,捕获和处理的条件是被抛弃的异常的类型与catch语句的异常类型相匹配。由于C++使用数据类型来区分不同的异常,因此在判断异常时,throw语句中的表达式的值就没有实际意义,而表达式的类型就特别重要。
抛出内置类型
C++可以抛出普通内置类型的异常
#include<iostream.h> //包含头文件 #include<stdlib.h> double fuc(double x, double y) //定义函数 { if(y==0) { throw y; //除数为0,抛出异常 } return x/y; //否则返回两个数的商 } void main() { double res; try //定义异常 { res=fuc(2,3); cout<<"The result of x/y is : "<<res<<endl; res=fuc(4,0); 出现异常,函数内部会抛出异常 } catch(double) //捕获并处理异常 { cerr<<"error of dividing zero.\n"; exit(1); //异常退出程序 } }
上面的程序当除数为0的时候,就抛出一个类型为double的异常,然后下面的catch{}捕获异常之后进行处理。
抛出自定义异常类型
异常类型可以使自定义的异常类,并且在程序中抛出该异常类。
#include<stdlib.h> #include<crtdbg.h> #include <iostream> // 内存泄露检测机制 #define _CRTDBG_MAP_ALLOC #ifdef _DEBUG #define new new(_NORMAL_BLOCK, __FILE__, __LINE__) #endif // 自定义异常类 class MyExcepction { public: // 构造函数,参数为错误代码 MyExcepction(int errorId) { // 输出构造函数被调用信息 std::cout << "MyExcepction is called" << std::endl; m_errorId = errorId; } // 拷贝构造函数 MyExcepction( MyExcepction& myExp) { // 输出拷贝构造函数被调用信息 std::cout << "copy construct is called" << std::endl; this->m_errorId = myExp.m_errorId; } ~MyExcepction() { // 输出析构函数被调用信息 std::cout << "~MyExcepction is called" << std::endl; } // 获取错误码 int getErrorId() { return m_errorId; } private: // 错误码 int m_errorId; }; int main(int argc, char* argv[]) { // 内存泄露检测机制 _CrtSetDbgFlag( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF ); // 可以改变错误码,以便抛出不同的异常进行测试 int throwErrorCode = 110; std::cout << " input test code :" << std::endl; std::cin >> throwErrorCode; try { if ( throwErrorCode == 110) { MyExcepction myStru(110); // 抛出对象的地址 -> 由catch( MyExcepction* pMyExcepction) 捕获 // 这里该对象的地址抛出给catch语句,不会调用对象的拷贝构造函数 // 传地址是提倡的做法,不会频繁地调用该对象的构造函数或拷贝构造函数 // catch语句执行结束后,myStru会被析构掉 throw &myStru; } else if ( throwErrorCode == 119 ) { MyExcepction myStru(119); // 抛出对象,这里会通过拷贝构造函数创建一个临时的对象传出给catch // 由catch( MyExcepction myExcepction) 捕获 // 在catch语句中会再次调用通过拷贝构造函数创建临时对象复制这里传过去的对象 // throw结束后myStru会被析构掉 throw myStru; } else if ( throwErrorCode == 120 ) { // 不提倡这样的抛出方法 // 这样做的话,如果catch( MyExcepction* pMyExcepction)中不执行delete操作则会发生内存泄露 // 由catch( MyExcepction* pMyExcepction) 捕获 MyExcepction * pMyStru = new MyExcepction(120); throw pMyStru; } else { // 直接创建新对象抛出 // 相当于创建了临时的对象传递给了catch语句 // 由catch接收时通过拷贝构造函数再次创建临时对象接收传递过去的对象 // throw结束后两次创建的临时对象会被析构掉 throw MyExcepction(throwErrorCode); } } catch( MyExcepction* pMyExcepction) { // 输出本语句被执行信息 std::cout << "执行了 catch( MyExcepction* pMyExcepction) " << std::endl; // 输出错误信息 std::cout << "error Code : " << pMyExcepction->getErrorId()<< std::endl; // 异常抛出的新对象并非创建在函数栈上,而是创建在专用的异常栈上,不需要进行delete //delete pMyExcepction; } catch ( MyExcepction myExcepction) { // 输出本语句被执行信息 std::cout << "执行了 catch ( MyExcepction myExcepction) " << std::endl; // 输出错误信息 std::cout << "error Code : " << myExcepction.getErrorId()<< std::endl; } catch(...) { // 输出本语句被执行信息 std::cout << "执行了 catch(...) " << std::endl; // 处理不了,重新抛出给上级 throw ; } // 暂停 int temp; std::cin >> temp; return 0; }
下面的例子也能很好的说明
class NumberParseException {}; //判断字符串是否为数字 bool isNumber(char * str) { using namespace std; if (str == NULL) return false; int len = strlen(str); if (len == 0) return false; bool isaNumber = false; char ch; for (int i = 0; i < len; i++) { if (i == 0 && (str[i] == '-' || str[i] == '+')) continue; if (isdigit(str[i])) { isaNumber = true; } else { isaNumber = false; break; } } return isaNumber; } //不是数字的话就抛出异常 int parseNumber(char * str) throw(NumberParseException) { if (!isNumber(str)) throw NumberParseException(); return atoi(str); }
异常的接口类型
为了加强程序的可读性,使函数的用户能够方便地知道所使用的函数会抛出哪些异常,在接口声明的时候就需要将throw的异常标注出来。
列出可能抛出的所有异常
void fun() throw(A, B, C, D);
这表明函数fun()可能并且只可能抛出类型(A, B, C, D)及其子类型的异常。
抛出任何类型的异常
void fun();
这表明该函数可以抛出任何类型的异常
不会抛出任何类型异常
void fun() thow();
这表明该函数不会抛出任何类型的异常
捕获异常
捕获异常的代码一般如下:
try { throw E(); //在try中抛出了类型为E的异常 } catch (H h) { //何时我们可以能到这里呢 }
1.如果H和E是相同的类型
2.如果H是E的基类
3.如果H和E都是指针类型,而且1或者2对它们所引用的类型成立
4.如果H和E都是引用类型,而且1或者2对H所引用的类型成立
从原则上来说,异常在抛出时被复制,我们最后捕获的异常只是原始异常的一个副本,所以我们不应该抛出一个不允许抛出一个不允许复制的异常。
此外,我们可以在用于捕获异常的类型加上const,就像我们可以给函数加上const一样,限制我们,不能去修改捕捉到的那个异常。
还有,捕获异常时如果H和E不是引用类型或者指针类型,而且H是E的基类,那么h对象其实就是H h = E(),最后捕获的异常对象h会丢失E的附加携带信息。
将异常重新抛出
当我们捕获了一个异常,却发现无法处理,这种情况下,我们会做完局部能够做的事情,然后再一次抛出这个异常,让这个异常在最合适的地方地方处理。例如:
void downloadFileFromServer() { try { connect_to_server(); //... } catch (NetworkException) { if (can_handle_it_completely) { //处理网络异常,例如重连 } else { throw; } } }
这个函数是从远程服务器下载文件,内部调用连接到远程服务器的函数,但是可能存在着网络异常,如果多次重连无法成功,就把这个网络异常抛出,让上层处理。
重新抛出是采用不带运算对象的throw表示,但是如果重新抛出,又没有异常可以重新抛出,就会调用terminate();
假设NetworkException有两个派生异常叫FtpConnectException和HttpConnectException,调用connect_to_server时是抛出HttpConnectException,那么调用downloadFileFromServer仍然能捕捉到异常HttpConnectException。
标准异常
到了这里,你已经基本会使用异常了,可是如果你是函数开发者,并需要把函数给别人使用,在使用异常时,会涉及到自定义异常类,但是C++标准已经定义了一部分标准异常,请尽可能复用这些异常,标准异常参考http://www.cplusplus.com/reference/std/stdexcept/
虽然C++标准异常比较少,但是作为函数开发者,尽可能还是复用c++标准异常,作为函数调用者就可以少花时间去了解的你自定义的异常类,更好的去调用你开发的函数。