[ZZ] c++中的错误处理

[ZZ] c++中的错误处理
c++中的错误处理
原创:monkeyfu 2003年6月19日

处理在程序的运行时刻发生的错误,对于任何一个程序设计者来讲都是不陌生的。对于错误的处理,我们有很多方法,本篇着重介绍的是C++中的错误异常处理。

在介绍C++中的错误异常处理之前,我们先来看一下常用的错误处理方式。

1.返回值 可以说这是最常用的错误处理方式之一,但其存在着一个致命的问题。就是返回值的检查与否是由调用者主动控制的。如果调用者不检查返回值,那也没有任何 机制能够强迫他这么做。再一个,考虑在C++中参数表相同而返回值不同的重载情况。在这种情况下,如果调用者不检查返回值的话,编译器根本不清楚应该调用哪个函数。
2.全局状态标示符 这种办法同返回值一样,也是需要调用者主动检查的。并且由于其是全局的,因此在多线程程序中,还必须保证它的线程安全性,必须要让检查者知道这是谁的返回值。
3.setjmp()/longjmp() 你完全可以将longjmp()当成远程的goto语句进行调用(goto语句只能左右于本地函数里)。但这个函数却存在着很大甚至是致命的危险。暂且放下该函数会破坏结构化程序设计风格不说。其一,longjmp()只能处理int型的异常。其二,也就是最致命的一点就是,longjmp()不会调用析构函数,而C++的 异常处理机制却会完成这个事情。因此,在C++中,千万不要使用setjmp()longjmp()函数。
4.断言 对于断言(Assert),其仅仅是在Debug版本中起作用,在Release中其是不存在的。另外断言与我们通常所说的错误处理方式不同,他是用来处理我们可能会发生这个错误,并能够避免的这种情况。

在介绍过上面那些存在问题的错误处理方式后,现在让我们来看看C++中的异常机制是如何处理错误的。首先说,C++的异常处理不会像上面提到的那些方法一样,必须是调用着主动检查。因为在C++中,一旦抛出(throw)一个异常,而程序不捕获(catch)的话,那么最终的结果就是abort()函数被调用,使得程序被终止。

下面我们来看一下C++异常处理(以下称EH)的基本语法和语意。
其引入了3个关键字,分别是:

catch
, throw, try

throw

异常由throw抛出,其格式为

throw [expression]

函数在定义时通过异常规格申明定义其会抛出什么类型的异常,其格式为:

throw([type-ID-list])

type-ID-list是一个可选项,其中包括了一个或多个类型的名字,它们之间以逗号分隔。

例如:

void func() throw(int, some_class_type)

则表明会抛出int和some_class_type类型异常。

对于一个空的异常规格申明,表示不抛出任何异常。
如:

void func() throw(...)

而如果函数没有异常规格申明,则表示会抛出任何类型的异常。
不过这里存在一种情况,例如:

void func() throw(int) //指明抛出int型异常
{
...
subfunc(); //但可能从这里抛出非int型异常
...
}

try -- catch

try块中的异常处理函数对异常进行捕获。其可以包含一个或多个处理函数,其形式如下:

catch (exception-declaration) compound-statement

处理函数的异常申明指明了其要捕获什么类型的异常。
对于异常申明其可以是无名的,例如:catch(char *),其表明会捕获一个char *类型异常,但由于是无名的,因此不能对其进行操作。另外异常申明也可以存在如下形式:catch(...),其表明会捕获任何类型的异常。

举例:

void func() throw(int, some_class_type)
{
    int i;
    ........
    throw i;
    ........
}

int main()
{
    try
    {
        func();
    }
    catch(int e)
    {
        //处理int型异常
    }
    catch(some_class_type)
    {
        //处理some_class_type型异常
    }
    .......
    return 0;
}

从上面的例子可以看出,当函数抛出异常时,throw后面要带一个抛出的对象。但这并不是必须的,例如:

catch(int e)
{
    .......
    throw;
}

throw后面没有接任何对象,这表明throw会再次抛出已存在的异常对象,因此其必须位于catch块中。

下面介绍一些C++提供的标准异常

namespace std
{
//exception派生
class logic_error; //逻辑错误,在程序运行前可以检测出来

//logic_error派生
class domain_error; //违反了前置条件
class invalid_argument; //指出函数的一个无效参数
class length_error; //指出有一个超过类型size_t的最大可表现值长度的对象的企图
class out_of_range; //参数越界
class bad_cast; //在运行时类型识别中有一个无效的dynamic_cast表达式
class bad_typeid; //报告在表达试typeid(*p)中有一个空指针p

//exception派生
class runtime_error; //运行时错误,仅在程序运行中检测到

//runtime_error派生
class range_error; //违反后置条件
class overflow_error; //报告一个算术溢出
class bad_alloc; //存储分配错误
}

在C++标准库头文件<exception>申明了几个EH类型和函数,它们是:

namespace std
{
//EH类型
class bad_exception;
class exception;

typedef void (*terminate_handler)();
typedef void (*unexpected_handler)();

// 函数
terminate_handler set_terminate(terminate_handler) throw();
unexpected_handler set_unexpected(unexpected_handler) throw();

void terminate();
void unexpected();

bool uncaught_exception();
}


exception 是所有标准库抛出的异常的基类。
uncaught_exception() 函数在异常被抛出却没有被捕获时返回true,其它情况返回false
terminate() 在异常处理陷入了不可恢复状态,如:重入时被调用。
unexpected() 在函数抛出一个没有在“异常规格申明”中申明的异常时被调用。

运行库提供了缺省terminate_handler()unexpected_handler()函数处理对应的情况。你可以通过set_terminate()set_unexpected()函数替换库的默认版本。这两个函数,其可以获取不带输入输出参数的函数,并且该函数会返回原terminate或者unexpected函数的地址指针。以便在使用中调用或者以后的恢复。另外,在terminate ()中。其必须不返回或者抛出异常。

在介绍了EH的基本知识后让我们来看看EH是如何工作的。

一般来说当发生函数调用的时候,都会进行诸如,保存寄存器值,参数压栈,创建被调函数堆栈等保护现场的工作,而在函数返回的时候则会进行与此相反的恢复现场的工作。

这样,当一个异常发生时,程序会在异常点处停止,然后开始搜索异常处理函数,其过程同函数返回相同,延调用栈向上搜索,直到找到一个与异常对象类型像匹配的异常申明,并进行相应的异常处理函数,在异常处理结束后,程序跳到异常处理函数所在try快最接近的下面一条语句开始执行。如果没有找到合适的异常申明,则最终会调用std :: unexpected(),并在其中调用std:terminate()直到abort(),程序被终止。

这也就意味着C++对于异常处理的模式始终是终止的。

例如:

#include <iostream.h>
static void func(int n)
 {
  if (n)
  throw 100;
 }

extern int main()
 {
  try
   {
    func(1);
    cout<<"程序不会执行到这里"<<endl;
   }
  catch(int)
   {
    cout<<"捕获一个int型异常"<<endl;
   }
  catch(...)
   {
    cout<<"捕获任意类型异常"<<endl;
   }

  cout<<"继续执行"<<endl;

return 0;
}

该程序在运行时会打印如下信息:

捕获一个int型异常
捕获任意类型异常


至于异常处理的另一种模式恢复模式。可以通过循环检测直到结果满意为止。但在实际中,往往产生异常的地方与异常处理函数距离可能会比较远,在这种情况下恢复模式就不那么可行了。

虽然,在异常处理延调用栈向上走的过程中回析构所有栈上的对象,但其并不会对堆中的对象进行处理,这样将会引起严重的资源泄露问题。

例如:

void func()
{
    testclass *p = new testclass();
    ...
    test(p); //这里会抛出异常
    ...
    delete p; //在抛出异常后,这里不会被执行,因此会导致内存泄露问题。
}

为了解决这个问题,C++提供了std::auto_ptr模板。其原理就是,将指针用一个栈上的模版实例保护起来,当发生异常的时候,模版会被析构,在析构函数中指针也就被delete了。

例如:

void func()
{
    std::auto_ptr<testclass> p(new testclass());
    ...
    test(p.get());
    ...
}

另外,在构造函数中抛出异常并不会引发析构函数。这一点要十分注意。因为这也会产生资源泄露问题。

例如:

class test
 {
  public:
  test() { c = new char[10]; throw -1;}
  ~test() {delete c;}

  private:
  char *c;
 };

void proc()
 {
  try{
     test t;
    }
  catch(int)
    {
    .......
     }
 }


由于异常是在test的构造函数中产生的,因此其不会引发其析构函数的调用。于是就如程序所示,产生了内存泄露问题。对于这种问题,最好的解决办法还是使用auto_ptr

对于析构函数,则不要在其中抛出异常。其原因在于析构函数会在其他异常抛出时被调用,这样就会引发异常的重入问题,进而导致terminate()被调用。如果在析构函数中真要抛出异常,如:析构函数调用的函数会抛出异常等,则必须在该析构函数内将其捕获。

前面说到要“找到一个与异常对象类型像匹配的异常申明”。事实上,这种匹配并不要求的十分准确。
考虑如下例子:

#include <iostream.h>

class base
  {
   public:
   virtual void what()
    {
     cout << "base" << endl;
    }
  };

class derived: public base
  {
   public:
   void what()
    {
     cout << "derived" << endl;
    }
  };

void f()
  {
   throw derived();
  }

main()
  {
   try
    {
     f();
    }
   catch(base b)
    {
     b.what();
    }

   try
    {
     f();
    }
   catch(base& b)
    {
     b.what();
    }
}

其显示结果为:

base
derived

为什么会这样呢。因为如果异常抛出一个派生类对象,而恰好又其基类所捕获到。那么该对象会被做"切片"处理。也就是说相对于基类,派生元素会被割下。在例子中derived的vptr会被设为base的virtual table。因此虚函数what就会呈现出这种行为。而当通过引用捕获时,得到的仅仅是其地址,对象不会被做切片处理。vptr因此也就不会发生变化,所以what仍然呈现出来derived的行为。

因此,这也就提醒我们将基类处理放在最后,在实际中更有意义。因为这样可以尽可能的在前面的处理中保存信息。

你可能感兴趣的:([ZZ] c++中的错误处理)