C++ 异常处理

本文详细解析了C++中的异常处理机制,包括throw、try和catch关键字的使用方法,异常规格声明的作用,以及如何捕获和处理各种类型的异常。同时介绍了C++标准库中提供的异常类型,并给出了具体的代码示例。
 
下面我们来看一下C++异常处理(以下称EH)的基本语法和语意。
其引入了3个关键字,分别是:
 
catch, throw, try
throw

异常由throw抛出,其格式为
throw [expression]
函数在定义时通过异常规格申明定义其会抛出什么类型的异常,其格式为:
throw([type-ID-list])
type-ID-list是一个可选项,其中包括了一个或多个类型的名字,它们之间以逗号分隔。
例如:
void func() throw(int, some_class_type)
则表明会抛出intsome_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


为什么会这样呢。因为如果异常抛出一个派生类对象,而恰好又其基类所捕获到。那么该对象会被做"切片"处理。也就是说相对于基类,派生元素会被割下。在例子中derivedvptr会被设为basevirtual table。因此虚函数what就会呈现出这种行为。而当通过引用捕获时,得到的仅仅是其地址,对象不会被做切片处理。vptr因此也就不会发生变化,所以what仍然呈现出来derived的行为。
因此,这也就提醒我们将基类处理放在最后,在实际中更有意义。因为这样可以尽可能的在前面的处理中保存信息。
 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值