异常处理

调用abort()

    abort()函数的原型位于头文件cstdlib(或stdlib.h)中,其典型实现是向标准错误流(即cerr使用的错误流)发送消息abnormal program termination(程序异常终止),然后终止程序。它还返回一个随实现而异的值,告诉操作系统(如果程序是由另一个程序调用的,则告诉父进程),处理失败。
    abort()是否刷新文件缓冲区(用于存储写到文件中的数据的内存区域)取决于实现。也可以使用exit(),该函数刷新文件缓冲区,但不显示消息。

 

返回错误码
    一种比异常终止更灵活的方法是,使用函数的返回值来指出问题。例如,ostream类的get(void)成员通常返回下一个输入字符的ASCII码,但到达文件尾时,将返回特殊值EOF。istream 族重载的>>操作符也使用了这种技术的变体,它返回bool值,告知调用程序是成功还是失败。

 

异常机制

异常提供了将控制权从程序的一个部分传递到另一个部分的途径。对异常的处理有3个组成部分:

引发异常;
捕获有处理程序的异常;
使用try块。

 

    程序在出现问题时将引发异常。程序使用异常处理程序(exceptiong handler)来捕获异常,异常处理程序位于要处理问题的程序中。


    catch关键字表示捕获异常。处理程序以关键字catch开头,随后是位于括号中的类型声明,它指出了异常处理程序要响应的异常类型。然后是一个用花括号括起的代码块,指出要采取的措施。catch关键字和异常类型用作标签,指出当异常被引发时,程序应跳到这个位置执行。异常处理程序也被称为catch块。


    try块标识其中特定的异常可能被激活的代码块,它后面跟一个或多个catch块。try块是由关键字try指示的,关键字try的后面是一个由花括号括起的代码块,表明需要注意这些代码引发的异常。


    throw语句实际上是跳转,即命令程序跳到另一条语句。throw关键字表示引发异常,紧随其后的值(例如字符串或对象)指出了异常的特征。

 

double hmean(double a,double b);

int main()
{ double x,y,z;
  while(std::cin >> x >> y)
  {
    try
    {                 // start of try block
      z = hmean(x,y);
    }                 // end of try block
    catch(const char * s)      // start of exceptiong handler
    {
      std::cout << s << std::endl;
      continue;
    }                                   // end of handler
  }
   ...
}

 

double hmean(double a,double b)
{
   if (a == -b)
      throw "abnormal program";
   return 2.0 * a * b / (a + b);
}

 

   如果try{...}块中的某条语句导致异常被引发,则后面的catch块将对异常进行处理。如果程序在try块的外面调用hmean(),将无法处理异常。

 

   执行throw语句类似于执行返回语句,因为它也将终止函数的执行。但throw不是将控制权返回给调用程序,而是导致程序沿函数调用序列后退,直到找到包含try块的函数。上面例子中,throw将程序控制权返回给main()。程序将在main()中寻找与引发的异常类型匹配的异常处理程序(位于try块的后面)。

 

   关键字catch表明这是一个处理程序,而括号中的类型(char*s)则表明该处理程序与字符串异常匹配。这里的字符串指针s与函数参数定义及其类似,因为匹配的引发将被赋给s。

 

   如果try块中没有引发任何异常,则程序跳过后面的catch块。

(如果函数引发了异常,而没有try块或没有匹配的处理程序时,默认情况下程序最终将调用abort()函数)

 

将对象用作异常类型

   使用对象作为异常类型参数,可以使用不同的异常类型,来区分不同的函数在不同情况下引发的异常。另外,对象可以携带信息,程序员可以根据这些信息来确定引发异常的原因。同时,catch块可以根据这些信息来决定采取什么样的措施。

 

例如,针对上面hmean()范例引发的异常而提供的一种设计:
class bad_hmean
{
  private:
     double v1; double v2;
  public:
     bad_hmean(int a=0,int b=0):v1(a),v2(b){}
     void mesg();
};

 

inline void bad_hmean::mesg()
{ std::cout<<"hmean("<<v1<<", "<<v2<<"): "
       <<"invalid arguments: a= -b/n"; }

 

那么hmean()函数便可以使用如下代码:
if (a == -b)
      throw bad_hmean(a,b);

上述代码调用构造函数bad_hmean(),以初始化对象,使其存储参数值。

 

异常规范

      可以使用异常规范对函数定义进行限定,指出它将引发那些类型的异常。可在函数定义的后面加上关键字throw和异常类型类表组成,异常类型类表被括在括号中,并用逗号分隔:
double hmean(double a,double b) throw(bad_hmean) {...}
double hmean(double a,double b) throw(const char *, double) {...}

     上述代码告诉编译器,该函数引发那些类型的异常。如果以后该函数引发了其他类型的异常,程序(最终)将调用abort()函数,对这种越权作出反应。

 

如果异常规范中的括号内为空,则表明该函数不会引发异常:
double hmean(double a,double b) throw() {...}

 

    原则上,异常规范应包含函数调用的其他函数引发的异常。例如,如果Argh()调用了Duh()函数,而后者可能引发retort对象异常,则Argh()和Duh()的异常规范中都应包含retort。但往往很难正确完成这项工作。

 

堆栈解退(unwinding the stack)

   C++通常将调用函数的指令的地址(返回地址)放到堆栈中。当被调用的函数执行完毕后,程序将使用该地址来确定从哪里开始继续执行。另外,函数调用将函数参数放到堆栈中。如果被调用的函数创建了新的自动变量,也将被添加到堆栈中。如果被调用的函数调用了另一个函数,则后者的信息将被添加到堆栈中,依此类推。


   当函数结束时,程序流程将跳到该函数被调用时存储的地址处,同时释放堆栈顶端的元素。同时每个函数都在结束时释放其自动变量,如果是类对象,则将调用析构函数(如果有的话)。

 

   假设函数由于出现异常(而不是由于返回)而终止,则程序也将释放堆栈中的内存,但不会在释放堆栈的第一个返回地址后停止,而是继续释放堆栈,直到找到一个位于try块中的返回地址。随后,控制权将转到块尾的异常处理程序,而不是函数调用后的第一条语句。这个过程被称为堆栈解退

 

   异常处理机制和函数返回一样,对于堆栈中的自动类对象,类的析构函数将被调用。不过,函数返回仅仅处理该函数放在堆栈中的对象,而throw语句则处理try块和throw之间整个函数调用序列放在堆栈中的对象。这是异常处理机制一个非常重要的特性。

 

重新抛出异常

   在内层catck块中使用不带异常类型的关键字throw,异常类型仍旧为内层相应catck块所带的类型。并将会把异常向上抛给第一个包含能够捕获相应异常的try-catch组合。

 

其他异常特性

   throw语句将控制权向上返回到第一个包含能够捕获相应异常的try-catch组合。另外,引发异常时编译器总是创建一个临时拷贝,即使异常规范和catch块中指定的是引用。

 


   由于基类引用可以执行派生类对象。那么,如果有一个异常类继承层次结构,应该这样排列catch块:将捕获位于层次最下面的异常类的catch语句放在最前面,将捕获基类异常的catch语句放在最后面。


   catch语句中使用基类对象引用时,将捕获所有的派生类对象,但派生特性将被剥去,因此该使用虚方法的基类版本。

 

当不确定异常类型时,可以使用省略号来捕获任何异常:
catch (...) { // statements // catches any type exception

(如果知道一些可能引发的异常,可以将捕获所有异常的catch块放在最后面,这有点类似于switch语句中的default)

 

exception类
   exception头文件(以前为exception.h或except.h)定义了exception类,C++可以把它用作其他异常类的基类。代码可以引发exception异常,也可以将exception类用作基类。类中有一个名为what()的虚拟成员函数,它返回一个字符串,该字符串的特征随实现而异。由于这是一个虚方法,因此可以在从exception派生而来的类中重新定义它:

#include<exception>
class bad_hmean : public std::exception
{ public:
     const char * what() { return "bad arguments to hmean()"; }
  ... };

 

   C++库定义了很多基于exception的异常类型。头文件exception提供了bad_exception,供函数unexpected()使用。

 

1. stdexcept 异常类
   头文件stdexcept定义了其他几个异常类。首先,该文件定义了logic_error(逻辑错误)和runtime_error(运行时错误)类,它们都是以公有方式从exception派生而来的:

class logic_error : public exception
{ public:
     explicit logic_error(const string & what_arg);
  ... };

 

class domain_error : public logic_error
{ public:
     explicit domain_error(const string & what_arg);
  ... };

 

注意,这些类的构造函数接受一个string对象作为参数,该参数提供了方法what()以C-风格字符串方式返回的字符数据。

 

logic_error类派生了如下几个类,描述了典型的逻辑错误:

 

domain_error;
invalid_argument;
length_error;
out_of_bounds。

以上每个类独有一个类似于logic_error的构造函数,让使用者能够提供一个供方法what()返回的字符串。

 

     数学函数有定义域(domain)和值域(range)。定义域由参数的可能取值组成,值域由函数可能的返回值组成。那么,可以编写一个函数,该函数将一个参数传递给函数sin(),则可以让该函数在参数不在定义域之内时引发domain_error异常

 

异常invalid_argument指出给函数传递了一个意外的值。

 

异常length_error用于指出没有足够的空间来执行所需的操作。例如,string类的append()方法在合并得到的字符串长度超过最大允许长度时,将引发该异常。

 

异常out_of_bounds通常用于指示索引错误。例如,定义一个类似于数组的类,其operator()[]在使用的索引无效时引发该异常。

 

runtime_error异常系列,描述了可能在运行期间发生但难以预计和防范的错误

 

range_error;
overflow_error;
underflow_error。


同样,以上每个类独有一个类似于runtime_error的构造函数,让使用者能够提供一个供方法what()返回的字符串。

 

underflow_error下溢错误在浮点数计算中。一般而言,存在浮点类型可以表示的最小非零值,计算结果比这个值还小时将导致该错误。

 

overflow_error上溢错误,整型和浮点型都可能发生,当计算结果超过了某种类型能够表示的最大数量级时,将导致该错误。

 

当计算结果可能不在函数允许的范围之内,但又没有发生上溢或下溢错误,这时可以使用range_error异常

 

    (如果上述库类不能满足需求,应该从logic_error或runtime_error派生一个异常类,以确保使用的异常类可以归入同一继承层次结构中)

 

2. bad_alloc 异常和 new
    处理使用new时可能出现的内存分配问题。第一种方式,让new在无法满足内存请求时返回一个空指针;第二种方式,让new引发bad_alloc异常。头文件new(以前名为new.h)中包含了bad_alloc类的声明,它是从exception类公有派生而来的。

 

范例:
#include <new>
...
struct Big { double stuff[90000]; };

 

int main()
{ Big * pb;

 

  try { pb = new double[10000]; }
  catch (bad_alloc & ba)
  { cout << ba.what() << endl;
    exit(EXIT_FAILURE); }

 

  if (pb = NULL)
    cout << " pb is null pointer/n";
  else
  { pb[0].stuff[0] = 4;
    cout << pb[0].stuff[0] << endl; }

 

  delete [] pb;
  return 0;
}

 

   异常、类和继承以三种方式相互关联。首先,可以像标准C++库所作的那样,从一个异常类派生出另一个;其次,可以在类定义中嵌套异常类声明来组合异常;第三,这种嵌套声明本身可被继承,还可用作基类。

 

异常何时会迷失方向

   异常引发后,如果它是在带异常规范的函数中引发的,则必须与规范列表中的某种异常匹配(在继承层次结构中,类类型与这个类及其派生类的对象匹配),否则称为意外异常(unexpected exception)。在默认情况下,这将导致程序异常终止。

 

   如果异常不是在函数中引发的(或者函数没有异常规范),则它必须被捕获。如果没被捕获(在没有try块或没有匹配的catch块时,将出现这种情况),则异常被称为未捕获异常(uncaught exception)。在默认情况下,这将导致程序异常终止。

 

未捕获异常(uncaught exception)
   未捕获异常不会导致程序立刻异常终止。相反,程序将首先调用函数terminate()。默认情况下,terminate()调用abort()函数。可以使用set_terminate()函数来指定terminate()应调用的函数,从而修改默认行为。

 

terminate()和set_terminate()都在头文件exception中声明:


typedef void (*terminate_handler) ();
terminate_handler set_terminate(terminate_handler f) throw();
void terminate();

 

其中typedef使terminate_handler成为这样一种类型的名称:指向没有参数和返回值的函数的指针。

 

set_terminate()函数将不带任何参数且返回类型为void的函数的名称(地址)作为参数,并返回该函数的地址。

 

如果调用了set_terminate()函数多次,则terminate()将调用最后一次set_terminate()调用设置的函数。

 

首先,设计一个这样的异常处理函数:
void myQuit() {...}

 


然后,在程序的开头,将终止操作指定为调用该函数:
set_terminate(myQuit);

 

现在,如果发生了一个异常且没有被捕获,程序将调用terminate(),而后者将调用myQuit()。

 

意外异常(unexpected exception)
    如果发生意外异常,程序将调用unexpected()函数。这个函数将调用terminate()。可以使用set_unexpected()函数来指定unexpected()应调用的函数,从而修改默认行为。

 

这两个函数也是在头文件exception中声明的:


typedef void (*unexpected_handler) ();
unexpected_handler set_unexpected(unexpected_handler f) throw();
void unexpected();

 

提供给set_unexpected()的函数的行为受到严格的限制,unexpected_handler函数可以为:
  通过调用terminate() (默认行为)、abort()或exit()来终止程序。
  引发异常。

 

其中,引发异常(第二种选择)的结果取决于unexpected_handler函数所引发的异常以及引发意外异常的函数的异常规范:

(1) 如果新引发的异常与原来的异常规范匹配,则程序将从那里开始进行正常处理,即寻找与新引发的异常匹配的catch块。基本上,这种方法将用预期的异常取代意外异常。

 

(2) 如果新引发的异常与原来的异常规范不匹配,且异常规范中没有包括std::bad_exception类型,则程序将调用terminate()。bad_exception是从exception派生而来的,其声明位于头文件exception中。

 

(3) 如果新引发的异常与原来的异常规范不匹配,且原来的异常规范中包含了std::bad_exception类型,则不匹配的异常将被std::bad_exception异常所取代。

 

捕获所有的异常,不管是预期的异常还是意外异常,则可以这样做:


首先确保异常头文件的声明可用:
#include <exception>
using namespace std;


然后,设计一个替代函数,将意外异常转换为bad_exception异常:
void myUnexpected()
{ throw std::bad_exception();
// or just throw }
(仅使用throw,而不指定异常将导致重新引发原来的异常。不过,如果异常规范中包含了这种类型,则该异常将被bad_exception对象所取代)

 

接下来在程序的开始位置,将意外异常操作指定为调用该函数:
set_unexpected(myUnexpected);

 

最后,将bad_exception类型包含在异常规范中,并添加如下catch块序列:
double Argh(double,double) throw(out_of_bounds,bad_exception);
...
try { x = Argh(a,b); }
catch (out_of_bounds & ex) { ... }
catch (bad_exception & ex) { ... }

 

异常的注意事项
    应在设计程序时就加入异常处理功能,而不是以后再添加。异常规范不适用于模板,因为模板函数引发的异常可能随特定的具体化而异。异常和动态内存分配不能总是协同工作。例如:

 

void test (int n)
{ double * ar = new double[n];
  ...
  if(oh_no)
    throw exception();
  ...
  delete [] ar;
  return;
}

 

    当解退堆栈时,将删除堆栈中的变量ar。但函数过早的终止意味着函数末尾的delete[]语句被忽略。指针消失了,但它指向的内存块未被释放,并且不可访问。这些内存被泄露了。

 

解决方法之一,在catch块中包含一些清理代码,然后重新引发异常:

void test (int n)
{ double * ar = new double[n];
  ...
  try {
       if(oh_no)
          throw exception();
  }
  catch(exception & ex)
  {
       delete [] ar;
       throw;
  }
  ...
  delete [] ar;
  return;
}

这将增加疏忽和产生其他错误的机会。另一种解决方法是使用<<auto_ptr模板类 (智能指针)>>。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值