天天看点

C++编程思想(卷二):异常处理

C++编程思想(卷二):异常处理

www.firnow.com    时间 : 2009-11-09  作者:匿名   编辑:小张 点击:  105 [ 评论 ]

-

-

关键字throw将导致一系列事情的发生:

首先,它将创建程序所抛出的对象的一个拷贝。

然后,包含throw表达式的函数返回了这个对象,即使该函数原先并未设计为返回这种对象类型。

另外,异常发生之前创建的局部对象被销毁(调用对象的析构函数)。

       一个异常被抛出以后,异常处理系统将按照在源代码中出现的顺序查找最近的异常处理器。一旦找到匹配的异常处理器,就认为异常已经被处理了而不再继续查找下去。

最好是通过引用而不是通过值来捕获异常。

当存在派生类时,首先捕获派生类异常,并且将基类放到最后用于捕获其他不太具体的异常。

捕获所有类型的异常:

用省略号代替异常处理器的参数列表就可以实现:

catch(...) { cout << "..." << endl; }

最好将它放在异常处理器列表的最后。省略号异常处理器不允许接受任何参数,这种catch子句经常用于清理资源并重新抛出所捕获的异常。

在一个异常处理器内部,使用不带参数的throw语句可以重新抛出异常,传递给位于更高一层语境中的异常处理器。

       如果没有任何一个层次的异常处理器能够捕获某种异常,一个特殊的库函数terminate( )(在头文件<exception>中)会被自动调用。默认情况下,terminate( )调用标准C库函数abort( )使程序执行异常终止而退出。

       在构造函数中分配资源时,如果在构造函数中发生异常,析构函数将没有机会释放这些资源,这个问题经常伴随着“悬挂”指针出现,为了防止资源泄露,必须使用下列两种方式之一来防止“不成熟的”资源分配方式:

1.在构造函数中捕获异常,用于释放资源。

2.在对象的构造函数中分配资源,并且在对象的析构函数中释放资源。

      为了实现第2种方式,可使用资源获得式(Resource Acquisition Is Initialization,RAII)技术,因为它使得对象对资源控制的时间与对象的声明周期相等。(可利用模板来封装指针,使得每个指针都被嵌入到对象中)。

       由于在一个典型的C++程序中动态分配内存是频繁使用的资源,所以C++标准中提供了一个RAII封装类(auto_ptr类),用于封装指向分配的堆内存的指针,这就使程序能自动释放创建对象时占用的堆内存。

例:

auto_ptr类模板是在头文件<memory>中定义的。

#include <memory>

#include <iostream>

#include <cstddef>

using namespace std;

class TraceHeap {

  int i;

public:

  static void* operator new(size_t siz) {

    void* p = ::operator new(siz);

    cout << "Allocating TraceHeap object on the heap "

         << "at address " << p << endl;

    return p;

  }

  static void operator delete(void* p) {

    cout << "Deleting TraceHeap object at address "

         << p << endl;

    ::operator delete(p);

  }

  TraceHeap(int i) : i(i) {}

  int getVal() const { return i; }

};

int main() {

  auto_ptr<TraceHeap> pMyObject(new TraceHeap(5));

  cout << pMyObject->getVal() << endl;  // Prints 5

}

       exception类的定义在头文件<exception>中。exception类的两个主要派生类为logic_error和runtime_error,这两个类的定义在头文件<stdexcept>中(这个头文件包含<exception>)。logic_error和runtime_error都提供一个参数类型为std::string的构造函数,这样就可以将信息保存在这两种类型的异常对象中,通过exception::what( )函数,可以从对象中得到它保存的信息。

      最好从runtime_error类或logic_error 来派生自己的异常类,而不要直接从std::exception类派生,因为它并没有提供一个参数类型为std::string的构造函数。

void f( );     意味着函数可能抛出任何类型的异常。

void f( ) throw( );  意味着函数不会抛出任何异常。

异常规格说明:

默认的unexpected( )函数会调用terminate( )函数。

set_unexpected( )函数使用一个函数指针作为参数,这个指针所指向的函数没有参数,而且其返回值类型为void。

如果unexception处理器所抛出的异常还是不符合函数的异常规格说明,下列两种情况之一将会发生:

1.如果函数的异常规格说明中包括std::bad_exception,unexpected处理器所抛出的异常会被替换成std::bad_exception对象,然后,程序恢复到这个函数被调用的位置重新开始异常匹配。

2.如果函数的异常规格说明中不包括std::bad_exception,程序会调用terminate( )函数。

在派生类函数的异常规格说明中只能指定范围更小的异常或指定为不抛出异常。(在派生类函数中抛出异常时只支持向下造型,不支持向上造型)

何时应该使用异常的最好建议是 :只有当函数不符合它的规格说明时才抛出异常。

在以下情况不应该使用异常:

1.不要在异步事件中使用异常。(异常依赖于程序运行栈上的动态函数调用链)

2.不要在处理简单错误的时候使用异常。

3.不要将异常用于程序的流程控制。

4.不要强迫自己使用异常。

5.新异常,老代码。

在下列情况下请使用异常:

1.修正错误并且重新调试产生异常的函数。

2.在重新调试中的函数外面补偿一些行为以便使程序得以继续执行。

3.在当前语境中做尽可能多的事情,并把同样类型的异常重新抛出到更高层的语境中。

4.在当前语境中做尽可能多的事情,并将一个不同类型的异常抛出到更高层的语境中。

5.终止程序。

6.将使用普通错误处理模式的函数(尤其是C库函数)封装起来,以便用异常来代替原有的错误处理模式。

7.简化。如果建立的错误处理模式使事情变得更复杂并且难以使用,那么异常可以使错误处理更加简单有效得多。

8.使建立的库和程序更安全。使用异常既是一种短期投资(为了调试方便),也是一种长期投资(为了应用系统的健壮性)。

不要在析构函数内部触发异常:

如果在析构函数中抛出异常,这个新的异常可能会在现存的异常(其他异常)到达catch子句之前被抛出,这会导致程序调用terminater( )函数。如果在析构函数中调用的函数可能会抛出异常,应该在这个析构函数中编写一个try块,并把这些函数调用放到try块中,析构函数必须自己处理所有这些异常。绝对不能有任何一个异常从析构函数中抛出。

文章出处:飞诺网(www.firnow.com):http://dev.firnow.com/course/3_program/c++/cppsl/20091109/181786.html

继续阅读