C++之异常处理

  程序中的错误分为编译时的错误和运行时的错误。编译时的错误主要是语法错误,比如:句尾没有加分号,括号不匹配,关键字错误等,这类错误比较容易修改,因为编译系统会指出错误在第几行,什么错误。而运行时的错误则不容易修改,因为其中的错误是不可预料的,或者可以预料但无法避免的,比如内存空间不够,或者在调用函数时,出现数组越界等错误。如果对于这些错误没有采取有效的防范措施,那么往往会得不到正确的运行结果,程序不正常终止或严重的会出现死机现象。我们把程序运行时的错误统称为异常,对异常处理称为异常处理。C++中所提供的异常处理机制结构清晰,在一定程度上可以保证程序的健壮性。

  C++中处理异常的过程是这样的:在执行程序发生异常,可以不在本函数中处理,而是抛出一个错误信息,把它传递给上一级的函数来解决,上一级解决不了,再传给其上一级,由其上一级处理。如此逐级上传,直到最高一级还无法处理的话,运行系统会自动调用系统函数terminate,由它调用abort终止程序。这样的异常处理方法使得异常引发和处理机制分离,而不在同一个函数中处理。这使得底层函数只需要解决实际的任务,而不必过多考虑对异常的处理,而把异常处理的任务交给上一层函数去处理。

  C++的异常处理机制有3部分组成:try(检查),throw(抛出),catch(捕获)。把需要检查的语句放在try模块中,检查语句发生错误,throw抛出异常,发出错误信息,由catch来捕获异常信息,并加以处理。一般throw抛出的异常要和catch所捕获的异常类型所匹配。异常处理的一般格式为:

  try

  {

    被检查语句

    throw 异常

  }

  catch(异常类型1)

  {

    进行异常处理的语句1

  }

  catch(异常类型2)

  {

    进行异常处理的语句2

  }

  ...

下面我们用示例演示一下异常处理:

#include "stdafx.h"
 #include <iostream>
 
 template <typename T>
 T Div(T x,T y)
 {
 if(y==0)
 throw y;//抛出异常
 return x/y;
 }
 
 int main()
 {
 int x=5,y=0;
 double x1=5.5,y1=0.0;
 try
     {
 //被检查的语句
         std::cout<<x<<"/"<<y<<"="<<Div(x,y)<<std::endl;
         std::cout<<x1<<"/"<<y1<<"="<<Div(x1,y1)<<std::endl;
     }
 catch(int)//异常类型
     {
         std::cout<<"除数为0,计算错误!"<<std::endl;//异常处理语句
     }
 catch(double)//异常类型
     {
         std::cout<<"除数为0.0,计算错误!"<<std::endl;//异常处理语句
     }
 
 return0;
 }


 

结果:

看了上述的示例代码,也许有人会问,第二个双精度类型的除法计算也应该抛出异常才对啊,在实际的运行过程中并非如此,其实该双精度类型除法函数根本没有被执行过。以上程序的执行规程为:调用函数Div(x,y)时发生异常,由函数Div中的语句"throw y"抛出异常,并不在往下执行return x/y,接着catch捕获int类型的异常并处理异常,最后直接执行"return 0"。因此函数Div(x1,y1)和catch(double){}模块根本没有被执行。如果,我们把y的值改为1,则结果就变成为:

如果在执行try语句模块时,没有发生异常,则catch语句块不起作用,流程转到其后的语句继续执行。从上述两个结果中可知第一次throw抛出的int类型所以找到处理该类型的catch,而第二次是抛出double类型所找到的是处理double类型的catch。

  下面对异常处理补充几点:(1)try和catch块中必须要用花括号括起来,即使花括号内只有一个语句也不能省略花括号;(2)try和catch必须成对出现,一个try_catch结果中只能有一个try块,但可以有多个catch块,以便与不同的异常信息匹配;(3)如果在catch块中没有指定异常信息的类型,而用删节号"...",则表示它可以捕获任何类型的异常信息;(4)如果throw不包括任何表达式,表示它把当前正在处理的异常信息再次抛出,传给其上一层的catch来处理;(5)C++中一旦抛出一个异常,如果程序没有任何的捕获,那么系统将会自动调用一个系统函数terminate,由它调用abort终止程序;

  最后还是一样,我将用一个示例来总结一下今天所讲的内容(开发工具:vs2010): 

View Code 
 #include "stdafx.h"
 #include <iostream>
 
 template <typename T>
 T Div(T x,T y)
 {
 if(y==0)
 throw y;//抛出异常
 return x/y;
 }
 
 int main()
 {
 int x=5,y=1;
 double x1=5.5,y1=0.0;
 try
     {
 //被检查的语句
         std::cout<<x<<"/"<<y<<"="<<Div(x,y)<<std::endl;
         std::cout<<x1<<"/"<<y1<<"="<<Div(x1,y1)<<std::endl;
     }
 catch(...)//捕获任意类型异常
     {
 try
         {
             std::cout<<"任意类型异常!"<<std::endl;
 throw;//抛出当前处理异常信息给上一层catch
         }
 catch(int)//异常类型
         {
             std::cout<<"除数为0,计算错误!"<<std::endl;//异常处理语句
         }
 catch(double)//异常类型
         {
             std::cout<<"除数为0.0,计算错误!"<<std::endl;//异常处理语句
         }
     
     }
     
 return0;
 }


 

结果:

 

自定义异常类型 :

// 代码清单1-2

#include "stdafx.h" 
#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; 
} 

 

 

 

异常的接口声明:

为了加强程序的可读性,使函数的用户能够方便地知道所使用的函数会抛出哪些异常,可以在函数的声明中列出这个函数可能抛出的所有异常类型,例如:

void fun() throw( A,B,C,D);

这表明函数fun()可能并且只可能抛出类型(A,B,C,D)及其子类型的异常。

如果在函数的声明中没有包括异常的接口声明,则此函数可以抛出任何类型的异常,例如:

void fun();


 

一个不会抛出任何类型异常的函数可以进行如下形式的声明:

void fun() thow();

 

异常处理中需要注意的问题:

1. 如果抛出的异常一直没有函数捕获(catch),则会一直上传到c++运行系统那里,导致整个程序的终止

2. 一般在异常抛出后资源可以正常被释放,但注意如果在类的构造函数中抛出异常,系统是不会调用它的析构函数的,处理方法是:如果在构造函数中要抛出异常,则在抛出前要记得删除申请的资源。

3. 异常处理仅仅通过类型而不是通过值来匹配的,所以catch块的参数可以没有参数名称,只需要参数类型。

4. 函数原型中的异常说明要与实现中的异常说明一致,否则容易引起异常冲突。

5. 应该在throw语句后写上异常对象时,throw先通过Copy构造函数构造一个新对象,再把该新对象传递给 catch.
那么当异常抛出后新对象如何释放?
异常处理机制保证:异常抛出的新对象并非创建在函数栈上,而是创建在专用的异常栈上,因此它才可以跨接多个函数而传递到上层,否则在栈清空的过程中就会被销毁。所有从try到throw语句之间构造起来的对象的析构函数将被自动调用。但如果一直上溯到main函数后还没有找到匹配的catch块,那么系统调用terminate()终止整个程序,这种情况下不能保证所有局部对象会被正确地销毁。

6. catch块的参数推荐采用地址传递而不是值传递,不仅可以提高效率,还可以利用对象的多态性。另外,派生类的异常扑获要放到父类异常扑获的前面,否则,派生类的异常无法被扑获。

7. 编写异常说明时,要确保派生类成员函数的异常说明和基类成员函数的异常说明一致,即派生类改写的虚函数的异常说明至少要和对应的基类虚函数的异常说明相同,甚至更加严格,更特殊。

 

转自:http://www.cnblogs.com/CaiNiaoZJ/archive/2011/08/19/2145349.html

转自:http://ticktick.blog.51cto.com/823160/191881

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值