C++异常处理:try,catch,throw,finally的⽤法
写在前⾯
所谓异常处理,即让⼀个程序运⾏时遇到⾃⼰⽆法处理的错误时抛出⼀个异常,希望调⽤者可以发现处理问题.
异常处理的基本思想是简化程序的错误代码,为程序键壮性提供⼀个标准检测机制.
也许我们已经使⽤过异常,但是你习惯使⽤异常了吗?
现在很多软件都是n36524⼩时运⾏,软件的健壮性⾄关重要.
内容导读
本⽂包括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
{
}
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
使⽤异常规格编程
如果我们调⽤别⼈的函数,⾥⾯有异常抛出,⽤去查看它的源代码去看看都有什么异常抛出吗?这样就会很烦琐.
⽐较好的解决办法,是编写带有异常抛出的函数时,采⽤异常规格说明,使我们看到函数声明就知道有哪些异常出现。
异常规格说明⼤体上为以下格式:
void ExceptionFunction(argument…)
throw(ExceptionClass1, ExceptionClass2, ….)
所有异常类都在函数末尾的throw()的括号中得以说明了,这样,对于函数调⽤者来说,是⼀清⼆楚的。
注意下⾯⼀种形式:
void ExceptionFunction(argument…) throw()
表明没有任何异常抛出.
⽽正常的void ExceptionFunction(argument…)则表⽰:可能抛出任何⼀种异常,当然,也可能没有异常,意义是最⼴泛的.
异常捕获之后,可以再次抛出,就⽤⼀个不带任何参数的throw语句就可以了.
构造和析构中的异常抛出
这是异常处理中最要注意的地⽅了
先看个程序,假如我在构造函数的地⽅抛出异常,这个类的析构会被调⽤吗?可如果不调⽤,那类⾥的东西岂不是不能被释放了?
#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(...)
{}
}
try catch的使用方法结果为:
ExceptionClass1()
throw a exception
在这两句输出之间,我们已经给S分配了内存,但内存没有被释放(因为它是在析构函数中释放的).
应该说这符合实际现象,因为对象没有完整构造.
为了避免这种情况,我想你也许会说:应避免对象通过本⾝的构造函数涉及到异常抛出.
即:既不在构造函数中出现异常抛出,也不应在构造函数调⽤的⼀切东西中出现异常抛出.
但是在C++中可以在构造函数中抛出异常,经典的解决⽅案是使⽤STL的标准类auto_ptr.
其实我们也可以这样做来实现:
在类中增加⼀个 Init()以及 UnInit();成员函数⽤于进⾏容易产⽣错误的资源分配⼯作,⽽真正的构造函数中先将所有成员置为NULL,然后调⽤ Init();并判断其返回值/或者捕捉 Init()抛出的异常,如果Init();失败了,则在构造函数中调⽤ UnInit(); 并设置⼀个标志位表明构造失败.
UnInit()中按照成员是否为NULL进⾏资源的释放⼯作.
那么,在析构函数中的情况呢?
我们已经知道,异常抛出之后,就要调⽤本⾝的析构函数,如果这析构函数中还有异常抛出的话,则已存在的异常尚未被捕获,会导致异常捕捉不到.
标准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对象,⽽不是⼀个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中获取有关的知识.
抛出⼀个异常的⾏为
个⼈认为接下来的这部分其实说的很经典,对我们理解异常⾏为/异常拷贝是很有帮助的.
条款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 被悄悄地转变为doubl
e类型,并且其返回值也是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⼦句捕获
}
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论