新普京网站-澳门新普京 > 前端 > 异常详解,异常处理

异常详解,异常处理

2019/12/29 22:05

写在前面

所谓异常处理,即让一个程序运行时遇到自己无法处理的错误时抛出一个异常,希望调用者可以发现处理问题.

异常处理的基本思想是简化程序的错误代码,为程序键壮性提供一个标准检测机制.

也许我们已经使用过异常,但是你习惯使用异常了吗?

现在很多软件都是n*365*24小时运行,软件的健壮性至关重要.

一、什么是异常处理

        一句话:异常处理就是处理程序中的错误。

内容导读

本文包括2个大的异常实现概念:C++的标准异常和SEH异常.

C++标准异常:

也许你很高兴看到错误之后的Heap/Stack中对象被释放,可是如果没有呢?

又或者试想一下一个能解决的错误,需要我们把整个程序Kill掉吗?

在《C++标准异常》中我向你推荐这几章:

<使用异常规格编程> <构造和析构中的异常抛出> <使用析构函数防止资源泄漏>,以及深入一点的<抛出一个异常的行为>.

SEH异常:

我要问你你是一个WIN32程序员吗?如果不是,那么也许你真的不需要看.

SEH是Windows的结构化异常,每一个WIN32程序员都应该要掌握它.

SEH功能强大,包括Termination handling和Exception handling两大部分.

强有力的维护了代码的健壮,虽然要以部分系统性能做牺牲(其实可以避免).

在SEH中有大量的代码,已经在Win平台上测试过了.

这里要提一下:在__finally处理中编译器参与了绝大多数的工作,而Exception则是OS接管了几乎所有的工作,也许我没有提到的是:

对__finally来说当遇到ExitThread/ExitProcess/abort等函数时,finally块不会被执行.

另:<使用析构函数防止资源泄漏>这个节点引用了More effective C++的条款9.

用2个列子,讲述了我们一般都会犯下的错误,往往这种错误是我们没有意识到的但确实是会给我们的软件带来致命的Leak/Crash,但这是有解决的方法的,那就是使用“灵巧指针”.

如果对照<More effective C++>的37条条款,关于异常的高级使用,有以下内容是没有完成的:

  • 使用构造函数防止资源Leak(More effective C++ #10)
  • 禁止异常信息传递到析构Function外 (More effective C++ #11)
  • 通过引用捕获异常 (More effective C++ #13)
  • 谨慎使用异常规格  (More effective C++ #14)
  • 了解异常处理造成的系统开销 (More effective C++ #15)
  • 限制对象数量 (More effective C++ #26)
  • 灵巧指针 (More effective C++ #28)

C++异常

C++引入异常的原因:

例如使用未经处理的pointer变的很危险,Memory/Resource Leak变的更有可能了.

写出一个具有你希望的行为的构造函数和析构函数也变的困难(不可预测),当然最危险的也许是我们写出的东东狗屁了,或者是速度变慢了.

大多数的程序员知道Howto use exception 来处理我们的代码,可是很多人并不是很重视异常的处理(国外的很多Code倒是处理的很好,Java的Exception机制很不错).

异常处理机制是解决某些问题的上佳办法,但同时它也引入了许多隐藏的控制流程;有时候,要正确无误的使用它并不容易.

在异常被throw后,没有一个方法能够做到使软件的行为具有可预测性和可靠性

对C程序来说,使用Error Code就可以了,为什么还要引入异常?因为异常不能被忽略.

如果一个函数通过设置一个状态变量或返回错误代码来表示一个异常状态,没有办法保证函数调用者将一定检测变量或测试错误代码.

结果程序会从它遇到的异常状态继续运行,异常没有被捕获,程序立即会终止执行.

在C程序中,我们可以用int setjmp( jmp_buf env );和 void longjmp( jmp_buf env, int value );

这2个函数来完成和异常处理相识的功能,但是MSDN中介绍了在C++中使用longjmp来调整stack时不能够对局部的对象调用析构函数,

但是对C++程序来说,析构函数是重要的(我就一般都把对象的Delete放在析构函数中).

所以我们需要一个方法:

  • 能够通知异常状态,又不能忽略这个通知.
  • 并且Searching the stack以便找到异常代码时.
  • 还要确保局部对象的析构函数被Call.

而C++的异常处理刚好就是来解决这些问题的.

有的地方只有用异常才能解决问题,比如说,在当前上下文环境中,无法捕捉或确定的错误类型,我们就得用一个异常抛出到更大的上下文环境当中去.

还有,异常处理的使用呢,可以使出错处理程序与“通常”代码分离开来,使代码更简洁更灵活.

另外就是程序必不可少的健壮性了,异常处理往往在其中扮演着重要的角色.

C++使用throw关键字来产生异常,try关键字用来检测的程序块,catch关键字用来填写异常处理的代码.

异常可以由一个确定类或派生类的对象产生。C++能释放堆栈,并可清除堆栈中所有的对象.

C++的异常和pascal不同,是要程序员自己去实现的,编译器不会做过多的动作.

throw异常类编程,抛出异常用throw, 如:

throw ExceptionClass(“my throw“);

例句中,ExceptionClass是一个类,它的构造函数以一个字符串做为参数.

也就是说,在throw的时候,C++的编译器先构造一个ExceptionClass的对象,让它作为throw的值抛出去,同时,程序返回,调用析构.

看下面这个程序:

#include <iostream.h>
class ExceptionClass
{
     char* name;
public:
     ExceptionClass(const char* name="default name")
     {
           cout<<"Construct "<<name<<endl;
           this->name=name;
     }
     ~ExceptionClass()
     {
           cout<<"Destruct "<<name<<endl;
     }
     void mythrow()
     {
           throw ExceptionClass("my throw");
     }
}

void main()
{
     ExceptionClass e("Test");
     try
     {
           e.mythrow();
     }
     catch(...)
     {
           cout<<”*********”<<endl;
     }
}

这是输出信息:

Construct Test
Construct my throw
Destruct my throw
****************
Destruct my throw   (这里是异常处理空间中对异常类的拷贝的析构)
Destruct Test
======================================

不过一般来说我们可能更习惯于把会产生异常的语句和要throw的异常类分成不同的类来写,下面的代码可以是我们更愿意书写的.

class ExceptionClass
{
public:
       ExceptionClass(const char* name="Exception Default Class")
     {
             cout<<"Exception Class Construct String"<<endl;
     }
        ~ExceptionClass()
     {
               cout<<"Exception Class Destruct String"<<endl;
     }
        void ReportError()
     {
               cout<<"Exception Class:: This is Report Error Message"<<endl;
     }
};

class ArguClass
{
        char* name;
public:
        ArguClass(char* name="default name")
     {
               cout<<"Construct String::"<<name<<endl;
               this->name=name;
     }
     ~ArguClass()
     {
               cout<<"Destruct String::"<<name<<endl;
     }
     void mythrow()
     {
               throw ExceptionClass("my throw");
     }
};

_tmain()
{
     ArguClass e("haha");
        try
     {
                 e.mythrow();
     }
     catch(int)
     {
               cout<<"If This is Message display screen, This is a Error!!"<<endl;
     }
     catch(ExceptionClass pTest)
     {
               pTest.ReportError();
     }
     catch(...)
     {
               cout<<"***************"<<endl;
     }
}

输出Message:

Construct String::haha
Exception Class Construct String
Exception Class Destruct String
Exception Class:: This is Report Error Message
Exception Class Destruct String
Destruct String::haha

二、为什么需要异常处理,以及异常处理的基本思想

        C++之父Bjarne Stroustrup在《The C++ Programming Language》中讲到:一个库的作者可以检测出发生了运行时错误,但一般不知道怎样去处理它们(因为和用户具体的应用有关);另一方面,库的用户知道怎样处理这些错误,但却无法检查它们何时发生(如果能检测,就可以再用户的代码里处理了,不用留给库去发现)。

        Bjarne Stroustrup说:提供异常基本目的就是为了处理上面的问题。基本思想是:让一个函数在发现了自己无法处理的错误时抛出(throw)一个异常,然后它的(直接或者间接)调用者能够处理这个问题。
The fundamental idea is that a function that finds a problem it cannot cope with throws an exception, hoping that its (direct or indirect) caller can handle the problem.

        也就是《C++ primer》中说的:将问题检测问题处理相分离。
Exceptions let us separate problem detection from problem resolution

        一种思想:在所有支持异常处理的编程语言中(例如java),要认识到的一个思想:在异常处理过程中,由问题检测代码可以抛出一个对象给问题处理代码,通过这个对象的类型和内容,实际上完成了两个部分的通信,通信的内容是“出现了什么错误”。当然,各种语言对异常的具体实现有着或多或少的区别,但是这个通信的思想是不变的。

使用异常规格编程

如果我们调用别人的函数,里面有异常抛出,用去查看它的源代码去看看都有什么异常抛出吗?这样就会很烦琐.

比较好的解决办法,是编写带有异常抛出的函数时,采用异常规格说明,使我们看到函数声明就知道有哪些异常出现。

异常规格说明大体上为以下格式:

void ExceptionFunction(argument…)
throw(ExceptionClass1, ExceptionClass2, ….)

所有异常类都在函数末尾的throw()的括号中得以说明了,这样,对于函数调用者来说,是一清二楚的。

注意下面一种形式:

void ExceptionFunction(argument…) throw()

表明没有任何异常抛出.

而正常的void ExceptionFunction(argument…)则表示:可能抛出任何一种异常,当然,也可能没有异常,意义是最广泛的.

异常捕获之后,可以再次抛出,就用一个不带任何参数的throw语句就可以了.

三、异常出现之前处理错误的方式

        在C语言的世界中,对错误的处理总是围绕着两种方法:一是使用整型的返回值标识错误;二是使用errno宏(可以简单的理解为一个全局整型变量)去记录错误。当然C++中仍然是可以用这两种方法的。

        这两种方法最大的缺陷就是会出现不一致问题。例如有些函数返回1表示成功,返回0表示出错;而有些函数返回0表示成功,返回非0表示出错。

        还有一个缺点就是函数的返回值只有一个,你通过函数的返回值表示错误代码,那么函数就不能返回其他的值。当然,你也可以通过指针或者C++的引用来返回另外的值,但是这样可能会令你的程序略微晦涩难懂。

构造和析构中的异常抛出

这是异常处理中最要注意的地方了

先看个程序,假如我在构造函数的地方抛出异常,这个类的析构会被调用吗?可如果不调用,那类里的东西岂不是不能被释放了?

#include <iostream.h>
#include <stdlib.h>

class ExceptionClass1
{
     char* s;
public:
     ExceptionClass1()
     {
           cout<<"ExceptionClass1()"<<endl;
           s=new char[4];
           cout<<"throw a exception"<<endl;
           throw 18;
     }
     ~ExceptionClass1()
     {
           cout<<"~ExceptionClass1()"<<endl;
           delete[] s;
     }
};

void main()
{
     try
     {
           ExceptionClass1 e;
     }
     catch(...)
     {}
}

结果为:

ExceptionClass1()
throw a exception

在这两句输出之间,我们已经给S分配了内存,但内存没有被释放(因为它是在析构函数中释放的).

应该说这符合实际现象,因为对象没有完整构造.

为了避免这种情况,我想你也许会说:应避免对象通过本身的构造函数涉及到异常抛出.

即:既不在构造函数中出现异常抛出,也不应在构造函数调用的一切东西中出现异常抛出.

但是在C++中可以在构造函数中抛出异常,经典的解决方案是使用STL的标准类auto_ptr.

其实我们也可以这样做来实现:

在类中增加一个 Init()以及 UnInit();成员函数用于进行容易产生错误的资源分配工作,而真正的构造函数中先将所有成员置为NULL,然后调用 Init();

并判断其返回值/或者捕捉 Init()抛出的异常,如果Init();失败了,则在构造函数中调用 UnInit(); 并设置一个标志位表明构造失败.

UnInit()中按照成员是否为NULL进行资源的释放工作.

那么,在析构函数中的情况呢?

我们已经知道,异常抛出之后,就要调用本身的析构函数,如果这析构函数中还有异常抛出的话,则已存在的异常尚未被捕获,会导致异常捕捉不到.

四、异常为什么好

    在如果使用异常处理的优点有以下几点:

        1. 函数的返回值可以忽略,但异常不可忽略。如果程序出现异常,但是没有被捕获,程序就会终止,这多少会促使程序员开发出来的程序更健壮一点。而如果使用C语言的error宏或者函数返回值,调用者都有可能忘记检查,从而没有对错误进行处理,结果造成程序莫名其面的终止或出现错误的结果。

        2. 整型返回值没有任何语义信息。而异常却包含语义信息,有时你从类名就能够体现出来。

        3. 整型返回值缺乏相关的上下文信息。异常作为一个类,可以拥有自己的成员,这些成员就可以传递足够的信息。

        4. 异常处理可以在调用跳级。这是一个代码编写时的问题:假设在有多个函数的调用栈中出现了某个错误,使用整型返回码要求你在每一级函数中都要进行处理。而使用异常处理的栈展开机制,只需要在一处进行处理就可以了,不需要每级函数都处理。

标准C++异常类

C++有自己的标准的异常类.

一个基类

exception 是所有C++异常的基类.

class exception {
public:
   exception() throw();
   exception(const exception& rhs) throw();
   exception& operator=(const exception& rhs) throw();
   virtual ~exception() throw();
   virtual const char *what() const throw();
};

派生了两个异常类

  • logic_erro       报告程序的逻辑错误,可在程序执行前被检测到.
  • runtime_erro     报告程序运行时的错误,只有在运行的时候才能检测到.

以上两个又分别有自己的派生类:

  • 由logic_erro派生的异常类

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

    range_error     报告违反了后置条件
    overflow_error   报告一个算术溢出
    bad_alloc          报告一个存储分配错误
    

使用析构函数防止资源泄漏

这部分是一个经典和很平常就会遇到的实际情况,下面的内容大部分都是从More Effective C++条款中得到的.

假设,你正在为一个小动物收容所编写软件,小动物收容所是一个帮助小狗小猫寻找主人的组织.

每天收容所建立一个文件,包含当天它所管理的收容动物的资料信息,你的工作是写一个程序读出这些文件然后对每个收容动物进行适当的处理(appropriate processing).

完成这个程序一个合理的方法是定义一个抽象类,ALA(”Adorable Little Animal”),然后为小狗和小猫建立派生类.

一个虚拟函数processAdoption分别对各个种类的动物进行处理:

class ALA {
public:
 virtual void processAdoption() = 0;
 ...
};
class Puppy: public ALA {
public:
 virtual void processAdoption();
 ...
};
class Kitten: public ALA {
public:
 virtual void processAdoption();
 ...
};

你需要一个函数从文件中读信息,然后根据文件中的信息产生一个puppy(小狗)对象或者kitten(小猫)对象.

这个工作非常适合于虚拟构造器(virtual constructor),在条款25详细描述了这种函数.

为了完成我们的目标,我们这样声明函数:

// 从s中读动物信息, 然后返回一个指针
// 指向新建立的某种类型对象
ALA * readALA(istream& s);

你的程序的关键部分就是这个函数,如下所示:

void processAdoptions(istream& dataSource)
{
     while(dataSource)
     {
           ALA *pa = readALA(dataSource); //得到下一个动物
           pa->processAdoption(); //处理收容动物
           delete pa; //删除readALA返回的对象
     }                 
}

这个函数循环遍历dataSource内的信息,处理它所遇到的每个项目.

唯一要记住的一点是在每次循环结尾处删除ps.

这是必须的,因为每次调用readALA都建立一个堆对象.如果不删除对象,循环将产生资源泄漏。

现在考虑一下,如果pa->processAdoption抛出了一个异常,将会发生什么?

processAdoptions没有捕获异常,所以异常将传递给processAdoptions的调用者.

转递中,processAdoptions函数中的调用pa->processAdoption语句后的所有语句都被跳过,这就是说pa没有被删除.

结果,任何时候pa->processAdoption抛出一个异常都会导致processAdoptions内存泄漏,很容易堵塞泄漏.

void processAdoptions(istream& dataSource)
{
     while(dataSource)
     {
           ALA *pa = readALA(dataSource);
           try
           {
                 pa->processAdoption();
           }
           catch(...)
           {
                        // 捕获所有异常
                 delete pa;         // 避免内存泄漏
                                // 当异常抛出时
                 throw;           // 传送异常给调用者
           }
           delete pa;          // 避免资源泄漏
     }              // 当没有异常抛出时
}

但是你必须用try和catch对你的代码进行小改动.

更重要的是你必须写双份清除代码,一个为正常的运行准备,一个为异常发生时准备.

在这种情况下,必须写两个delete代码.

象其它重复代码一样,这种代码写起来令人心烦又难于维护,而且它看上去好像存在着问题.

不论我们是让processAdoptions正常返回还是抛出异常,我们都需要删除pa,所以为什么我们必须要在多个地方编写删除代码呢?

我们可以把总被执行的清除代码放入processAdoptions函数内的局部对象的析构函数里,这样可以避免重复书写清除代码.

因为当函数返回时局部对象总是被释放,无论函数是如何退出的.

(仅有一种例外就是当你调用longjmp时。Longjmp的这个缺点是C++率先支持异常处理的主要原因)

具体方法是用一个对象代替指针pa,这个对象的行为与指针相似。当pointer-like(类指针)对象被释放时,我们能让它的析构函数调用delete.

替代指针的对象被称为smart pointers(灵巧指针),下面有解释,你能使得pointer-like对象非常灵巧.

在这里,我们用不着这么聪明的指针,我们只需要一个pointer-lik对象,当它离开生存空间时知道删除它指向的对象.

写出这样一个类并不困难,但是我们不需要自己去写。标准C++库函数包含一个类模板,叫做auto_ptr,这正是我们想要的.

每一个auto_ptr类的构造函数里,让一个指针指向一个堆对象(heap object),并且在它的析构函数里删除这个对象.

下面所示的是auto_ptr类的一些重要的部分:

template<class T>
class auto_ptr
{
public:
      auto_ptr(T *p = 0): ptr(p) {}    // 保存ptr,指向对象
      ~auto_ptr() { delete ptr; }     // 删除ptr指向的对象
private:
      T *ptr;            // raw ptr to object
};

auto_ptr类的完整代码是非常有趣的,上述简化的代码实现不能在实际中应用.

(我们至少必须加上拷贝构造函数,赋值operator以及下面将要讲到的pointer-emulating函数)

但是它背后所蕴含的原理应该是清楚的:用auto_ptr对象代替raw指针,你将不再为堆对象不能被删除而担心,即使在抛出异常时,对象也能被及时删除.

(因为auto_ptr的析构函数使用的是单对象形式的delete,所以auto_ptr不能用于指向对象数组的指针.

如果想让auto_ptr类似于一个数组模板,你必须自己写一个。在这种情况下,用vector代替array可能更好)

auto_ptr
template<class T>
class auto_ptr
{
public:
     typedef T element_type;
     explicit auto_ptr(T *p = 0) throw();
     auto_ptr(const auto_ptr<T>& rhs) throw();
     auto_ptr<T>& operator=(auto_ptr<T>& rhs) throw();
     ~auto_ptr();
     T& operator*() const throw();
     T *operator->() const throw();
     T *get() const throw();
     T *release() const throw();
};

使用auto_ptr对象代替raw指针,processAdoptions如下所示:

void processAdoptions(istream& dataSource)
{
     while(dataSource)
     {
           auto_ptr<ALA> pa(readALA(dataSource));
           pa->processAdoption();
     }
}

这个版本的processAdoptions在两个方面区别于原来的processAdoptions函数:

  • pa被声明为一个auto_ptr<ALA>对象,而不是一个raw ALA*指针.
  • 在循环的结尾没有delete语句.

其余部分都一样,因为除了析构的方式,auto_ptr对象的行为就象一个普通的指针。是不是很容易.

隐藏在auto_ptr后的思想是:

用一个对象存储需要被自动释放的资源,然后依靠对象的析构函数来释放资源,这种思想不只是可以运用在指针上,还能用在其它资源的分配和释放上.

想一下这样一个在GUI程序中的函数,它需要建立一个window来显式一些信息:

// 这个函数会发生资源泄漏,如果一个异常抛出
void displayInfo(const Information& info)
{
      WINDOW_HANDLE w(createWindow());//在w对应的window中显式信息
      destroyWindow(w);
}

很多window系统有C-like接口,使用象like createWindow 和 destroyWindow函数来获取和释放window资源.

如果在w对应的window中显示信息时,一个异常被抛出,w所对应的window将被丢失,就象其它动态分配的资源一样.

解决方法与前面所述的一样,建立一个类,让它的构造函数与析构函数来获取和释放资源:

//一个类,获取和释放一个window 句柄
class WindowHandle
{
public:
       WindowHandle(WINDOW_HANDLE handle): w(handle) {}
      ~WindowHandle() { destroyWindow(w); }
       operator WINDOW_HANDLE() { return w; }    // see below
private:
      WINDOW_HANDLE w;
      // 下面的函数被声明为私有,防止建立多个WINDOW_HANDLE拷贝
     //有关一个更灵活的方法的讨论请参见下面的灵巧指针
      WindowHandle(const WindowHandle&);
      WindowHandle& operator=(const WindowHandle&);
};

这看上去有些象auto_ptr,只是赋值操作与拷贝构造被显式地禁止(参见More effective C++条款27),有一个隐含的转换操作能把WindowHandle转换为WINDOW_HANDLE.

这个能力对于使用WindowHandle对象非常重要,因为这意味着你能在任何地方象使用raw WINDOW_HANDLE一样来使用WindowHandle.

(参见More effective C++条款5 ,了解为什么你应该谨慎使用隐式类型转换操作)

通过给出的WindowHandle类,我们能够重写displayInfo函数,如下所示:

// 如果一个异常被抛出,这个函数能避免资源泄漏
void displayInfo(const Information& info)
{
     WindowHandle w(createWindow());
     //在w对应的window中显式信息;
}

即使一个异常在displayInfo内被抛出,被createWindow 建立的window也能被释放.

资源应该被封装在一个对象里,遵循这个规则,你通常就能避免在存在异常环境里发生资源泄漏.

但是如果你正在分配资源时一个异常被抛出,会发生什么情况呢?

例如当你正处于resource-acquiring类的构造函数中.

还有如果这样的资源正在被释放时,一个异常被抛出,又会发生什么情况呢?

构造函数和析构函数需要特殊的技术.

你能在More effective C++条款10和More effective C++条款11中获取有关的知识.

五、C++中使用异常时应注意的问题

    任何事情都是两面性的,异常有好处就有坏处。如果你是C++程序员,并且希望在你的代码中使用异常,那么下面的问题是你要注意的。

        1. 性能问题。这个一般不会成为瓶颈,但是如果你编写的是高性能或者实时性要求比较强的软件,就需要考虑了。

(如果你像我一样,曾经是java程序员,那么下面的事情可能会让你一时迷糊,但是没办法,谁叫你现在学的是C++呢。)

       2. 指针和动态分配导致的内存回收问题:在C++中,不会自动回收动态分配的内存,如果遇到异常就需要考虑是否正确的回收了内存。在java中,就基本不需要考虑这个,有垃圾回收机制真好!

        3. 函数的异常抛出列表:java中是如果一个函数没有在异常抛出列表中显式指定要抛出的异常,就不允许抛出;可是在C++中是如果你没有在函数的异常抛出列表指定要抛出的异常,意味着你可以抛出任何异常

        4. C++中编译时不会检查函数的异常抛出列表。这意味着你在编写C++程序时,如果在函数中抛出了没有在异常抛出列表中声明的异常,编译时是不会报错的。而在java中,eclipse的提示功能真的好强大啊!

        5. 在java中,抛出的异常都要是一个异常类;但是在C++中,你可以抛出任何类型,你甚至可以抛出一个整型。(当然,在C++中如果你catch中接收时使用的是对象,而不是引用的话,那么你抛出的对象必须要是能够复制的。这是语言的要求,不是异常处理的要求)。

        6. 在C++中是没有finally关键字的。而java和python中都是有finally关键字的。

抛出一个异常的行为

个人认为接下来的这部分其实说的很经典,对我们理解异常行为/异常拷贝是很有帮助的.

条款12:理解“抛出一个异常”与“传递一个参数”或“调用一个虚函数”间的差异

从语法上看,在函数里声明参数与在catch子句中声明参数几乎没有什么差别:

class Widget { ... };                 //一个类,具体是什么类在这里并不重要
void f1(Widget w);                    // 一些函数,其参数分别为
void f2(Widget& w);                   // Widget, Widget&,或
void f3(const Widget& w);             // Widget* 类型
void f4(Widget *pw);
void f5(const Widget *pw);
catch(Widget w) ...                  //一些catch 子句,用来
catch(Widget& w)   ...               //捕获异常,异常的类型为
catch(const Widget& w) ...            // Widget, Widget&, 或
catch(Widget *pw) ...                 // Widget*
catch(const Widget *pw) ...

你因此可能会认为用throw抛出一个异常到catch子句中与通过函数调用传递一个参数两者基本相同.

这里面确有一些相同点,但是他们也存在着巨大的差异.

让我们先从相同点谈起.

你传递函数参数与异常的途径可以是传值、传递引用或传递指针,这是相同的.

但是当你传递参数和异常时,系统所要完成的操作过程则是完全不同的.

产生这个差异的原因是:你调用函数时,程序的控制权最终还会返回到函数的调用处,但是当你抛出一个异常时,控制权永远不会回到抛出异常的地方。

有这样一个函数,参数类型是Widget,并抛出一个Widget类型的异常:

// 一个函数,从流中读值到Widget中
istream operator>>(istream& s, Widget& w);
void passAndThrowWidget()
{
     Widget localWidget;
     cin >> localWidget;          //传递localWidget到 operator>>
     throw localWidget;           // 抛出localWidget异常
}

当传递localWidget到函数operator>>里,不用进行拷贝操作,而是把operator>>内的引用类型变量w指向localWidget,任何对w的操作实际上都施加到localWidget上.

这与抛出localWidget异常有很大不同.

不论通过传值捕获异常还是通过引用捕获(不能通过指针捕获这个异常,因为类型不匹配)都将进行lcalWidget的拷贝操作,也就说传递到catch子句中的是localWidget的拷贝.

必须这么做,因为当localWidget离开了生存空间后,其析构函数将被调用.

如果把localWidget本身(而不是它的拷贝)传递给catch子句,这个子句接收到的只是一个被析构了的Widget,一个Widget的“尸体”.

这是无法使用的。因此C++规范要求被做为异常抛出的对象必须被复制.

即使被抛出的对象不会被释放,也会进行拷贝操作.

例如如果passAndThrowWidget函数声明localWidget为静态变量(static):

void passAndThrowWidget()
{
     static Widget localWidget;        // 现在是静态变量(static) 一直存在至程序结束
     cin >> localWidget;               // 象以前那样运行
     throw localWidget;                // 仍将对localWidget进行拷贝操作
}

当抛出异常时仍将复制出localWidget的一个拷贝.

这表示即使通过引用来捕获异常,也不能在catch块中修改localWidget;仅仅能修改localWidget的拷贝.

对异常对象进行强制复制拷贝,这个限制有助于我们理解参数传递与抛出异常的第二个差异:抛出异常运行速度比参数传递要慢.

当异常对象被拷贝时,拷贝操作是由对象的拷贝构造函数完成的.

该拷贝构造函数是对象的静态类型(static type)所对应类的拷贝构造函数,而不是对象的动态类型(dynamic type)对应类的拷贝构造函数.

比如以下这经过少许修改的passAndThrowWidget:

class Widget { ... };
class SpecialWidget: public Widget { ... };
void passAndThrowWidget()
{
     SpecialWidget localSpecialWidget;
     ...
     Widget& rw = localSpecialWidget;      // rw 引用SpecialWidget
     throw rw;                             //它抛出一个类型为Widget的异常
}

这里抛出的异常对象是Widget,即使rw引用的是一个SpecialWidget.

因为rw的静态类型(static type)是Widget,而不是SpecialWidget.

你的编译器根本没有主要到rw引用的是一个SpecialWidget。编译器所注意的是rw的静态类型(static type).

这种行为可能与你所期待的不一样,但是这与在其他情况下C++中拷贝构造函数的行为是一致的.

(不过有一种技术可以让你根据对象的动态类型dynamic type进行拷贝,参见条款25)

异常是其它对象的拷贝,这个事实影响到你如何在catch块中再抛出一个异常.

比如下面这两个catch块,乍一看好像一样:

catch(Widget& w)                  // 捕获Widget异常
{
     ...                             // 处理异常
     throw;                          // 重新抛出异常,让它
}                                 // 继续传递
catch(Widget& w)                  // 捕获Widget异常
{
     ...                             // 处理异常
     throw w;                        // 传递被捕获异常的
}                                 // 拷贝

这两个catch块的差别在于第一个catch块中重新抛出的是当前捕获的异常,而第二个catch块中重新抛出的是当前捕获异常的一个新的拷贝.

如果忽略生成额外拷贝的系统开销,这两种方法还有差异么?
当然有。第一个块中重新抛出的是当前异常(current exception),无论它是什么类型.

特别是如果这个异常开始就是做为SpecialWidget类型抛出的,那么第一个块中传递出去的还是SpecialWidget异常,即使w的静态类型(static type)是Widget.

这是因为重新抛出异常时没有进行拷贝操作.

第二个catch块重新抛出的是新异常,类型总是Widget,因为w的静态类型(static type)是Widget.

一般来说,你应该用throw来重新抛出当前的异常,因为这样不会改变被传递出去的异常类型,而且更有效率,因为不用生成一个新拷贝.

(顺便说一句,异常生成的拷贝是一个临时对象.

正如条款19解释的,临时对象能让编译器优化它的生存期(optimize it out of existence),

不过我想你的编译器很难这么做,因为程序中很少发生异常,所以编译器厂商不会在这方面花大量的精力)

让我们测试一下下面这三种用来捕获Widget异常的catch子句,异常是做为passAndThrowWidgetp抛出的:

catch (Widget w) ...                // 通过传值捕获异常
catch (Widget& w) ...               // 通过传递引用捕获异常
catch (const Widget& w) ...         //通过传递指向const的引用捕获异常

我们立刻注意到了传递参数与传递异常的另一个差异.

一个被异常抛出的对象(刚才解释过,总是一个临时对象)可以通过普通的引用捕获.

它不需要通过指向const对象的引用(reference-to-const)捕获.

在函数调用中不允许转递一个临时对象到一个非const引用类型的参数里(参见条款19),但是在异常中却被允许.

让我们先不管这个差异,回到异常对象拷贝的测试上来.

我们知道当用传值的方式传递函数的参数,我们制造了被传递对象的一个拷贝(参见Effective C++ 条款22),并把这个拷贝存储到函数的参数里.

同样我们通过传值的方式传递一个异常时,也是这么做的。当我们这样声明一个catch子句时:

catch (Widget w) ...                // 通过传值捕获

会建立两个被抛出对象的拷贝,一个是所有异常都必须建立的临时对象,第二个是把临时对象拷贝进w中.

同样,当我们通过引用捕获异常时:

catch (Widget& w) ...               // 通过引用捕获
catch (const Widget& w) ...         file://也通过引用捕获

这仍旧会建立一个被抛出对象的拷贝:拷贝是一个临时对象.

相反当我们通过引用传递函数参数时,没有进行对象拷贝.

当抛出一个异常时,系统构造的(以后会析构掉)被抛出对象的拷贝数比以相同对象做为参数传递给函数时构造的拷贝数要多一个.

我们还没有讨论通过指针抛出异常的情况,不过通过指针抛出异常与通过指针传递参数是相同的.

不论哪种方法都是一个指针的拷贝被传递.

你不能认为抛出的指针是一个指向局部对象的指针,因为当异常离开局部变量的生存空间时,该局部变量已经被释放.

Catch子句将获得一个指向已经不存在的对象的指针。这种行为在设计时应该予以避免.

对象从函数的调用处传递到函数参数里与从异常抛出点传递到catch子句里所采用的方法不同,

这只是参数传递与异常传递的区别的一个方面,第二个差异是在函数调用者或抛出异常者与被调用者或异常捕获者之间的类型匹配的过程不同.

比如在标准数学库(the standard math library)中sqrt函数:

double sqrt(double);      // from <cmath> or <math.h>

我们能这样计算一个整数的平方根,如下所示:

int i;
double sqrtOfi = sqrt(i);

毫无疑问,C++允许进行从int到double的隐式类型转换,所以在sqrt的调用中,i 被悄悄地转变为double类型,并且其返回值也是double.

(有关隐式类型转换的详细讨论参见条款5)一般来说,catch子句匹配异常类型时不会进行这样的转换.

见下面的代码:

void f(int value)
{
     try
     {
           if(someFunction())         // 如果 someFunction()返回
           {
                 throw value;             //真,抛出一个整形值
                 ...
           }
     }
     catch(double d)              // 只处理double类型的异常
     {
           ...
     }
     ...
}

在try块中抛出的int异常不会被处理double异常的catch子句捕获.

该子句只能捕获真真正正为double类型的异常;不进行类型转换.

因此如果要想捕获int异常,必须使用带有int或int&参数的catch子句.

不过在catch子句中进行异常匹配时可以进行两种类型转换.

第一种是继承类与基类间的转换.

一个用来捕获基类的catch子句也可以处理派生类类型的异常.

例如在标准C++库(STL)定义的异常类层次中的诊断部分(diagnostics portion )(参见Effective C++ 条款49).

捕获runtime_errors异常的Catch子句可以捕获range_error类型和overflow_error类型的异常,

可以接收根类exception异常的catch子句能捕获其任意派生类异常.

这种派生类与基类(inheritance_based)间的异常类型转换可以作用于数值、引用以及指针上:

catch (runtime_error) ...               // can catch errors of type
catch (runtime_error&) ...              // runtime_error,
catch (const runtime_error&) ...        // range_error, or overflow_error
catch (runtime_error*) ...              // can catch errors of type
catch (const runtime_error*) ...        // runtime_error*,range_error*, oroverflow_error*

第二种是允许从一个类型化指针(typed pointer)转变成无类型指针(untyped pointer),

所以带有const void* 指针的catch子句能捕获任何类型的指针类型异常:

catch (const void*) …                 file://捕获任何指针类型异常

传递参数和传递异常间最后一点差别是catch子句匹配顺序总是取决于它们在程序中出现的顺序.

因此一个派生类异常可能被处理其基类异常的catch子句捕获,即使同时存在有能处理该派生类异常的catch子句,与相同的try块相对应.

例如:

try
{
     ...
}
catch(logic_error& ex)                 // 这个catch块 将捕获
{
     ...                                  // 所有的logic_error
}                                      // 异常, 包括它的派生类
catch(invalid_argument& ex)            // 这个块永远不会被执行
{
     ...                                    //因为所有的invalid_argument异常 都被上面的catch子句捕获
}

与上面这种行为相反,当你调用一个虚拟函数时,被调用的函数位于与发出函数调用的对象的动态类型(dynamic type)最相近的类里.

你可以这样说虚拟函数采用最优适合法,而异常处理采用的是最先适合法.

如果一个处理派生类异常的catch子句位于处理基类异常的catch子句前面,编译器会发出警告.

(因为这样的代码在C++里通常是不合法的)

不过你最好做好预先防范:不要把处理基类异常的catch子句放在处理派生类异常的catch子句的前面.

上面那个例子,应该这样去写:

try
{
     ...
}
catch(invalid_argument& ex)             // 处理 invalid_argument
{
     ...
}
catch(logic_error& ex)                  // 处理所有其它的
{
     ...                                   // logic_errors异常
}

综上所述,把一个对象传递给函数或一个对象调用虚拟函数与把一个对象做为异常抛出,这之间有三个主要区别.

第一、异常对象在传递时总被进行拷贝;当通过传值方式捕获时,异常对象被拷贝了两次.

对象做为参数传递给函数时不需要被拷贝.

第二、对象做为异常被抛出与做为参数传递给函数相比,前者类型转换比后者要少(前者只有两种转换形式).

最后一点,catch子句进行异常类型匹配的顺序是它们在源代码中出现的顺序,第一个类型匹配成功的catch将被用来执行.

当一个对象调用一个虚拟函数时,被选择的函数位于与对象类型匹配最佳的类里,即使该类不是在源代码的最前头.

六、异常的基本语法

1. 抛出和捕获异常

        很简单,抛出异常用throw,捕获用try……catch

        捕获异常时的注意事项:

             1. catch子句中的异常说明符必须是完全类型,不可以为前置声明,因为你的异常处理中常常要访问异常类的成员。例外:只有你的catch子句使用指针或者引用接收参数,并且在catch子句内你不访问异常类的成员,那么你的catch子句的异常说明符才可以是前置声明的类型。

             2. catch的匹配过程是找最先匹配的,不是最佳匹配。

             3. catch的匹配过程中,对类型的要求比较严格允许标准算术转换类类型的转换。(类类型的转化包括种:通过构造函数的隐式类型转化和通过转化操作符的类型转化)。

             4. 和函数参数相同的地方有:
                    如果catch中使用基类对象接收子类对象,那么会造成子类对象分隔slice)为父类子对象(通过调用父类的复制构造函数);
                    如果catch中使用基类对象的引用接受子类对象,那么对虚成员的访问时,会发生动态绑定,即会多态调用
                    如果catch中使用基类对象的指针,那么一定要保证throw语句也要抛出指针类型,并且该指针所指向的对象,在catch语句执行是还存在(通常是动态分配的对象指针)。

             5. 和函数参数不同的地方有:  
                    如果throw中抛出一个对象,那么无论是catch中使用什么接收(基类对象、引用、指针或者子类对象、引用、指针),在传递到catch之前,编译器都会另外构造一个对象的副本。也就是说,如果你以一个throw语句中抛出一个对象类型,在catch处通过也是通过一个对象接收,那么该对象经历了两次复制,即调用了两次复制构造函数。一次是在throw时,将“抛出到对象”复制到一个“临时对象”(这一步是必须的),然后是因为catch处使用对象接收,那么需要再从“临时对象”复制到“catch的形参变量”中; 如果你在catch中使用“引用”来接收参数,那么不需要第二次复制,即形参的引用指向临时变量。
                    ② 该对象的类型与throw语句中体现的静态类型相同。也就是说,如果你在throw语句中抛出一个指向子类对象的父类引用,那么会发生分割现象,即只有子类对象中的父类部分会被抛出,抛出对象的类型也是父类类型。(从实现上讲,是因为复制到“临时对象”的时候,使用的是throw语句中类型的(这里是父类的)复制构造函数)。
                    ③ 不可以进行标准算术转换类的自定义转换:在函数参数匹配的过程中,可以进行很多的类型转换。但是在异常匹配的过程中,转换的规则要严厉。

                    ④ 异常处理机制的匹配过程是寻找最先匹配(first fit),函数调用的过程是寻找最佳匹配(best fit)。

  1. 异常类型

        上面已经提到过,在C++中,你可以抛出任何类型的异常。(哎,竟然可以抛出任何类型,刚看到到这个的时候,我半天没反应过来,因为java中这样是不行的啊)。

         注意:也是上面提到过的,在C++中如果你throw语句中抛出一个对象,那么你抛出的对象必须要是能够复制的。因为要进行复制副本传递,这是语言的要求,不是异常处理的要求。(在上面“和函数参数不同的地方”中也讲到了,因为是要复制先到一个临时变量中)

  1. 栈展开

        栈展开指的是:当异常抛出后,匹配catch的过程。

        抛出异常时,将暂停当前函数的执行,开始查找匹配的catch子句。沿着函数的嵌套调用链向上查找,直到找到一个匹配的catch子句,或者找不到匹配的catch子句。

        注意事项:

               1. 在栈展开期间,会销毁局部对象。

                     ① 如果局部对象是类对象,那么通过调用它的析构函数销毁。

                     ② 但是对于通过动态分配得到的对象,编译器不会自动删除,所以我们必须手动显式删除。(这个问题是如此的常见和重要,以至于会用到一种叫做RAII的方法,详情见下面讲述)

               2. 析构函数应该从不抛出异常。如果析构函数中需要执行可能会抛出异常的代码,那么就应该在析构函数内部将这个异常进行处理,而不是将异常抛出去。

                     原因:在为某个异常进行栈展开时,析构函数如果又抛出自己的未经处理另一个异常,将会导致调用标准库 terminate 函数。而默认的terminate 函数将调用 abort 函数,强制从整个程序非正常退出。

               3. 构造函数中可以抛出异常。但是要注意到:如果构造函数因为异常而退出,那么该类的析构函数就得不到执行。所以要手动销毁在异常抛出前已经构造的部分。

4. 异常重新抛出

        语法:使用一个空的throw语句。即写成: throw;  

        注意问题:

                ① throw;  语句出现的位置,只能是catch子句中或者是catch子句调用的函数中。
                ② 重新抛出的是原来的异常对象,即上面提到的“临时变量”,不是catch形参。
                ③ 如果希望在重新抛出之前修改异常对象,那么应该在catch中使用引用参数。如果使用对象接收的话,那么修改异常对象以后,不能通过“重新抛出”来传播修改的异常对象,因为重新抛出不是catch形参,应该使用的是 throw e;  这里“e”为catch语句中接收的对象参数。

5. 捕获所有异常(匹配任何异常)

        语法:在catch语句中,使用三个点(…)。即写成:catch (…)   这里三个点是“通配符”,类似 可变长形式参数。

        常见用法:与“重新抛出”表达式一起使用,在catch中完成部分工作,然后重新抛出异常。

6. 未捕获的异常

        意思是说,如果程序中有抛出异常的地方,那么就一定要对其进行捕获处理。否则,如果程序执行过程中抛出了一个异常,而又没有找到相应的catch语句,那么会和“栈展开过程中析构函数抛出异常”一样,会 调用terminate 函数,而默认的terminate 函数将调用 abort 函数,强制从整个程序非正常退出。

7. 构造函数的函数测试块

        对于在构造函数的初始化列表中抛出的异常,必须使用函数测试块(function try block)来进行捕捉。语法类型下面的形式:

  1. MyClass::MyClass(int i) 
  2. try :member(i) { 
  3.     //函数体 

  4. } catch(异常参数) { 

  5.     //异常处理代码 

        注意事项:在函数测试块中捕获的异常,在catch语句中可以执行一个内存释放操作,然后异常仍然会再次抛出到用户代码中。

8. 异常抛出列表(异常说明 exception specification)

        就是在函数的形参表之后(如果是const成员函数,那么在const之后),使用关键字throw声明一个带着括号的、可能为空的 异常类型列表。形如:throw ()  或者 throw (runtime_error, bad_alloc)   。

        含义:表示该函数只能抛出 在列表中的异常类型。例如:throw() 表示不抛出任何异常。而throw (runtime_error, bad_alloc)表示只能抛出runtime_error 或bad_alloc两种异常。

        注意事项:(以前学java的尤其要注意,和java中不太一样)

                ① 如果函数没有显式的声明 抛出列表,表示异常可以抛出任意列表。(在java中,如果没有异常抛出列表,那么是不能抛出任何异常的)。

                ② C++的 “throw()”相当于java的不声明抛出列表。都表示不抛出任何异常。

                ③ 在C++中,编译的时候,编译器不会对异常抛出列表进行检查。也就是说,如果你声明了抛出列表,即使你的函数代码中抛出了没有在抛出列表中指定的异常,你的程序依然可以通过编译,到运行时才会出错,对于这样的异常,在C++中称为“意外异常”(unexpeced exception)。(这点和java又不相同,在java中,是要进行严格的检查的)。

        意外异常的处理: 
                如果程序中出现了意外异常,那么程序就会调用函数unexpected()。这个函数的默认实现是调用terminate函数,即默认最终会终止程序。

        虚函数重载方法时异常抛出列表的限制 
                在子类中重载时,函数的异常说明 必须要比父类中要同样严格,或者更严格。换句话说,在子类中相应函数的异常说明不能增加新的异常。或者再换句话说:父类中异常抛出列表是该虚函数的子类重载版本可以抛出异常列表的 超集

        函数指针中异常抛出列表的限制 
                 异常抛出列表是函数类型的一部分,在函数指针中也可以指定异常抛出列表。但是在函数指针初始化或者赋值时,除了要检查返回值形式参数外,还要注意异常抛出列表的限制:源指针的异常说明必须至少和目标指针的一样严格。比较拗口,换句话说,就是声明函数指针时指定的异常抛出列表,一定要实际函数的异常抛出列表的超集。 如果定义函数指针时不提供异常抛出列表,那么可以指向能够抛出任意类型异常的函数。               

        抛出列表是否有用  
                 在《More effective C++》第14条,Scott Meyers指出“要谨慎的使用异常说明”(Use exception specifications judiciously)。“异常说明”,就是我们所有的“异常抛出列表”。之所以要谨慎,根本原因是因为C++编译器不会检查异常抛出列表,这样就可能在函数代码中、或者调用的函数中抛出了没有在抛出列表中指定的异常,从而导致程序调用unexpected函数,造成程序提前终止。同时他给出了三条要考虑的事情:
                         ① 在模板不要使用异常抛出列表。(原因很简单,连用来实例模板的类型都不知道,也就无法确定该函数是否应该抛出异常,抛出什么异常)。 
                         ② 如果A函数内调用了B函数,而B函数没有声明异常抛出列表,那么A函数本身也不应该设定异常抛出列表。(原因是,B函数可能抛出没有在A函数的异常抛出列表中声明的异常,会导致调用unex函数);
                         ③ 通过set_unexpected函数指定一个新的unexpected函数,在该函数中捕获异常,并抛出一个统一类型的异常。

                 另外,在《C++ Primer》4th 中指出,虽然异常说明应用有限,但是如果能够确定该函数不会抛出异常,那么显式声明其不抛出任何异常 有好处。通过语句:"throw ()"。这样的好处是:对于程序员,当调用这样的函数时,不需要担心异常。对于编译器,可以执行被可能抛出异常所抑制的优化。

灵巧指针

第一次用到灵巧指针是在写ADO代码的时候,用到com_ptr_t灵巧指针;但一直印象不是很深;

其实灵巧指针的作用很大,对我们来说垃圾回收,ATL等都会使用到它.

在More effective 的条款后面特意增加这个节点,不仅是想介绍它在异常处理方面的作用,还希望对编写别的类型代码的时候可以有所帮助.

smart pointer(灵巧指针)其实并不是一个指针,其实是某种形式的类.

不过它的特长就是模仿C/C++中的指针,所以就叫pointer 了.

所以希望大家一定要记住两点:smart pointer是一个类而非指针,但特长是模仿指针.

那怎么做到像指针的呢?

C++的模板技术和运算符重载给了很大的发挥空间.

首先smart pointer必须是高度类型化的(strongly typed ),模板给了这个功能.

其次需要模仿指针主要的两个运算符->和*,那就需要进行运算符重载.

详细的实现:

template<CLASS&NBSP; T> class SmartPtr
{
public:
       SmartPtr(T* p = 0);
       SmartPtr(const SmartPtr& p);
       ~SmartPtr();
       SmartPtr& operator =(SmartPtr& p);
       T& operator*() const {return *the_p;}
       T* operator->() const {return the_p;}
private:
       T *the_p;
}

这只是一个大概的印象,很多东西是可以更改的.

比如可以去掉或加上一些const ,这都需要根据具体的应用环境而定.

注意重载运算符*和->,正是它们使smart pointer看起来跟普通的指针很相像.

而由于smart pointer是一个类,在构造函数、析构函数中都可以通过恰当的编程达到一些不错的效果.
举例:

比如C++标准库里的std::auto_ptr 就是应用很广的一个例子.

它的实现在不同版本的STL 中虽有不同,但原理都是一样,大概是下面这个样子:

template<CLASS&NBSP; X> class auto_ptr
{
public:
     typedef X element_type;
     explicit auto_ptr(X* p = 0) throw():the_p(p) {}
     auto_ptr(auto_ptr& a) throw():the_p(a.release()) {}
     auto_ptr& operator =(auto_ptr& rhs) throw()
     {
           reset(rhs.release());
           return *this;
     }
     ~auto_ptr() throw() {delete the_p;}
     X& operator* () const throw() {return *the_p;}
     X* operator-> () const throw() {return the_p;}
     X* get() const throw() {return the_p;}
     X* release() throw()
     {
           X* tmp = the_p;
           the_p = 0;
           return tmp;

     }
     void reset(X* p = 0) throw()
     {
           if(the_p!=p)
           {
                 delete the_p;
                 the_p = p;
           }
     }
private:
     X* the_p;
};

关于auto_ptr 的使用可以找到很多的列子,这里不在举了.

它的主要优点是不用 delete ,可以自动回收已经被分配的空间,由此可以避免资源泄露的问题.

很多Java 的拥护者经常不分黑白的污蔑C++没有垃圾回收机制,其实不过是贻笑大方而已.

抛开在网上许许多多的商业化和非商业化的C++垃圾回收库不提, auto_ptr 就足以有效地解决这一问题.

并且即使在产生异常的情况下, auto_ptr 也能正确地回收资源.

这对于写出异常安全(exception-safe )的代码具有重要的意义.

在使用smart pointer 的过程中,要注意的问题:

针对不同的smart pointer ,有不同的注意事项。比如auto_ptr ,就不能把它用在标准容器里,因为它只在内存中保留一份实例.

把握我前面说的两个原则:smart pointer 是类而不是指针,是模仿指针,那么一切问题都好办.

比如,smart  pointer 作为一个类,那么以下的做法就可能有问题.

SmartPtr p; 
if(p==0) 
if(!p) 
if(p)

很显然, p 不是一个真正的指针,这么做可能出错.

而SmartPtr 的设计也是很重要的因素.

您可以加上一个bool SmartPtr::null() const 来进行判断.

如果坚持非要用上面的形式, 那也是可以的,我们就加上operator void* ()试试:

template<CLASS&NBSP; T> class SmartPtr
{
public: ...
       operator void*() const {return the_p;}
... private:
       T* the_p;
};

这种方法在basic_ios 中就使用过了。这里也可以更灵活地处理,比如类本身需要operator void*()这样地操作,

那么上面这种方法就不灵了。但我们还有重载operator !()等等方法来实现.
总结smart pointer的实质:

smart pointer 的实质就是一个外壳,一层包装。正是多了这层包装,我们可以做出许多普通指针无法完成的事,比如前面资源自动回收,或者自动进行引用记数,比如ATL 中CComPtr 和 CComQIPtr 这两个COM 接口指针类.

然而也会带来一些副作用,正由于多了这些功能,又会使 smart pointer 丧失一些功能.

七、标准库中的异常类

        和java一样,标准库中也提供了很多的异常类,它们是通过类继承组织起来的。标准异常被组织成八个

        异常类继承层级结构图如下: 
图片 1

    每个类所在的头文件在图下方标识出来.

    标准异常类的成员: 
        ① 在上述继承体系中,每个类都有提供了构造函数、复制构造函数、和赋值操作符重载。
        ② logic_error类及其子类、runtime_error类及其子类,它们的构造函数是接受一个string类型的形式参数,用于异常信息的描述;
        ③ 所有的异常类都有一个what()方法,返回const char* 类型(C风格字符串)的值,描述异常信息。

    标准异常类的具体描述: 

异常名称

描述

exception 所有标准异常类的父类
bad_alloc 当operator new and operator new[],请求分配内存失败时
bad_exception 这是个特殊的异常,如果函数的异常抛出列表里声明了bad_exception异常,当函数内部抛出了异常抛出列表中没有的异常,这是调用的unexpected函数中若抛出异常,不论什么类型,都会被替换为bad_exception类型
bad_typeid 使用typeid操作符,操作一个NULL指针,而该指针是带有虚函数的类,这时抛出bad_typeid异常
bad_cast 使用dynamic_cast转换引用失败的时候
ios_base::failure io操作过程出现错误
logic_error 逻辑错误,可以在运行前检测的错误
runtime_error 运行时错误,仅在运行时才可以检测的错误

        logic_error的子类

 

异常名称

描述

length_error 试图生成一个超出该类型最大长度的对象时,例如vector的resize操作
domain_error 参数的值域错误,主要用在数学函数中。例如使用一个负值调用只能操作非负数的函数
out_of_range 超出有效范围
invalid_argument 参数不合适。在标准库中,当利用string对象构造bitset时,而string中的字符不是’0’或’1’的时候,抛出该异常

 

        runtime_error的子类

 

异常名称

描述

range_error 计算结果超出了有意义的值域范围
overflow_error 算术计算上溢
underflow_error 算术计算下溢

 

WIN结构化异常

对使用WIN32平台的人来说,对WIN的结构化异常应该要有所了解的。WINDOWS的结构化异常是操作系统的一部分,而C++异常只是C++的一部分,当我们用C++编写代码的时候,我们选择C++的标准异常(也可以用MS VC的异常),编译器会自动的把我们的C++标准异常转化成SEH异常。

微软的Visual C++也支持C + +的异常处理,并且在内部实现上利用了已经引入到编译程序和Windows操作系统的结构化异常处理的功能。

SEH实际包含两个主要功能:结束处理(termination handling)和异常处理(exceptionhandling).

在MS VC的FAQ中有关于SEH的部分介绍,这里摘超其中的一句:

“在VC5中,增加了新的/EH编译选项用于控制C++异常处理。C++同步异常处理(/EH)使得编译器能生成更少的代码,/EH也是VC的缺省模型。”

一定要记得在背后的事情:在使用SEH的时候,编译程序和操作系统直接参与了程序代码的执行。

八、编写自己的异常类

        1. 为什么要编写自己的异常类? 
                ① 标准库中的异常是有限的;
                ② 在自己的异常类中,可以添加自己的信息。(标准库中的异常类值允许设置一个用来描述异常的字符串)。

        2. 如何编写自己的异常类?
                ① 建议自己的异常类要继承标准异常类。因为C++中可以抛出任何类型的异常,所以我们的异常类可以不继承自标准异常,但是这样可能会导致程序混乱,尤其是当我们多人协同开发时。
                ② 当继承标准异常类时,应该重载父类的what函数虚析构函数
                ③ 因为栈展开的过程中,要复制异常类型,那么要根据你在类中添加的成员考虑是否提供自己的复制构造函数

上一篇:的mem函数和strcopy函数的区别和应用,c语言之字符串处理 下一篇:没有了