Chapter 7 异常处理
4.6节实现Triangular_iterator
class时,iterator有潜在的错误,可能引发错误状态的产生。
class Triangular
{
friend class Triangular_iterator;
public:
static bool is_elem(int);
static void gen_elements(int length);
static void gen_elems_to_value(int value);
static void display(int length,int beg_pos,ostream &os = cout);
private:
static vector<int> _elems;
static const int _max_elems = 1024;
};
class Triangular_iterator
{
public:
Triangular_iterator(int index) :_index(index-1){}
bool operator==(const Triangular_iterator&) const;
bool operator!=(const Triangular_iterator&) const;
int operator*() const;
Triangular_iterator& operator++();
Triangular_iterator operator++(int);
// friend class Triangular;
private:
void check_integrity() const;
int _index;
};
其数据成员_index
可能被设置为某值,大于static vector的大小。对于该class的使用者来说,这个问题并不容易辨识。
不过作为设计者,我们可将这一问题概括为***iterator不再处于有效状态,再也不能被程序继续使用下去了。***但我们仍不知道这个问题对于整个程序的危害程度,只有iterator的用户才知道问题的重要性。
因此,我们的职责便是通知用户,告诉他发生了什么事。我们以C++的异常处理机制(exception handling facility)来完成通知任务。
7.1 抛出异常
异常处理机制有两个主要成分:异常的鉴定与发出&异常的处理方式
通常,无论是member function或non-member function。都可能产生异常以及处理异常。异常出现后,正常程序的执行便被暂停(suspended)。与此同时,异常处理机制开始搜索程序中有能力处理这一异常的地点。异常被处理完毕后,程序的执行便会继续(resume),从异常处理点接着执行下去。
C++通过throw
表达式产生异常:
inline void Triangular_iterator::check_integrity() const
{
if(_index >= Triangular::_max_elems)
throw iterator_overflow(_index,Triangular::_max_elems);
if(_index >= Triangular::_elems.size() )
Triangular::gen_elements(_index + 1);
}
如果满足第一条if
语句,类型为iterator_throw
的异常对象便会被抛出(throw),于是第二条if
语句便不会执行。
何谓抛出(throw
)一个异常?所谓异常(exception)是某种对象。最简单的异常对象可以设计为整数或字符串:
throw 42;
throw "panic:no buffer!";
大部分时候,被抛出的异常都属于特定的异常类(也许形成一个继承体系)。例如下面的定义:
class iterator_overflow{
public:
iterator_overflow(int index,int max)
:_index(index),_max(max) {}
int index() {return _index;}
int max() {return _max;}
void what_happened(ostream &os = cerr){
os<<"Internal error:current index"<<_index
<<" exceeds maximum bound: "<<_max;
}
private:
int _index;
int _max;
};
因此下面代码中的throw
实际上会调用拥有两个参数的constructor。
if(_index >= Triangular::_max_elems)
throw iterator_overflow(_index,Triangular::_max_elems);
我们也可以换一种方式,明确指出被抛出的对象的名称:
if(_index >= Triangular::_max_elems){
iterator_overflow ex(_index, Triangular::_max_elems);
throw ex;
}
7.2 捕获异常
我们可以使用单条或一连串的catch
子句来捕获(catch)被抛出的异常对象。catch
子句由三部分组成:
关键字catch
、小括号内的一个类型或对象、大括号内的一组语句(用以处理异常)
// extern 关键字表明这是一个声明, 其定义可能在其他文件处, 注意不能对变量进行初始化或者对函数进行定义,
// 否则表明这是一个定义而不是声明.
extern void log_message(const char*);
extern string err_messages[];
extern ostream log_file;
bool some_function()
{
bool status = true;
// ……假设我们到达此处
catch(int errno){
log_message(err_messages[errno]);
status = false;
}
catch(const char* str){
log_message(str);
status = false;
}
catch(iterator_overflow &iof){
iof.what_happened(log_file);
status = false;
}
return status;
}
上述catch
语句分别会处理前一节所抛出的三个异常对象
throw 42;
throw "panic:no buffer!";
throw iterator_overflow(_index,Triangular::_max_elems);
这表示异常对象的类型会被拿来逐一地和每个catch
子句比对。如果类型符合,则该catch
子句的内容便会执行。
有时我们可能无法完成异常的完整处理。在记录信息之外,还需要重新抛出(rethrow)异常,以寻求其他catch
子句的协助,做进一步的处理:
catch(iterator_overflow &iof)
{
log_message(ios.what_happened());
// 重新抛出异常,令另一个catch语句接手处理
throw;
}
重新抛出时,只需写下关键字throw
即可。它只能出现在catch
子句中。它会将捕捉的异常对象再一次抛出,并由另一个类型吻合的catch
子句协助处理
如果我们像捕获任何类型的异常,可以用一网打尽的方式(catch-all)的方式。只需在异常声明部分指定省略号即可(…),如下:
// 捕获任何类型的异常
catch(...)
{
log_message("exception of unknown type");
// 清理(clean up)然后退出
}
7.3 提炼异常
catch
子句应该和try
块组合出现。catch
放在try
块的末尾,这表示如果try
块内有任何异常发生,便由接下来的catch
子句加以处理。
例如下例尝试在first
和last
所标示的范围内,寻找特定的elem
。在此范围内的迭代可能会引发iterator_overflow
异常,因此将这段程序代码放在try
块内,并在接下来的catch
子句中指定要捕获iterator_overflow
异常。
bool has_elem(Triangular_iterator first,
Triangular_iterator last, int elem)
{
bool status = true;
try{
while(first != last)
{
if(*first == elem)
return status;
++first;
}
}
catch(iterator_overflow &iof)
{
log_message(iof.what_happened());
// 上面这行好像有错,what_happend()返回的是void,
// 但log_message要求的是const char*
log_message("check if iterators address same container");
}
status = false;
return status;
}
// *first会调用重载的dereference运算符
inline int Triangular_iterator::
operator*()
{
check_integrity();
return Triangular::_elems[_index];
}
inline void Triangular_iterator::
check_integrity()
{
if(_index >= Triangular::_max_elems)
throw iterator_overflow(_index, Triangular::_max_elems);
// ...
}
假设某刻last
的_index
值大于_max_elems
,于是check_integrity()
获得结果为true
,并抛出异常:
异常处理机制开始查看异常由何处抛出,并判断是否在try
块内,若是,则检验相应的catch
子句,并看它是否具备处理此异常的能力。如果有,这个异常则被处理,而程序也就继续执行下去。
但上面例子的throw
表达式没在try
内,因此异常处理机制不处理它,这导致check_integrity()
的剩余内容不会被执行,因为异常处理机制终止了check_integrity()
的执行权。但异常处理机制会继续在check_integrity()
的调用端搜寻类型吻合的catch
子句。
于是同一个问题再在调用端(重载的dereference运算符)被提出一次:是否check_integrity()
调用操作发生于try
块内?显然没有,于是dereference运算符被中断执行,异常处理机制继续上溯到运算符的调用端(即*first
),发现此时check_integrity()
位于try
块内。于是相应的catch
子句被拿出来查看,类型吻合者会被执行,如此便完成了异常处理。
接下来,正常的程序执行则于“被执行的catch
子句”的下方第一行语句继续,即:
// 发生iterator_overflow异常或未发现elem时,执行以下代码
status = false;
return status;
如果不断回溯,直到main()
函数还没找到合适的catch()
子句,便调用标准库提供的terminate()
(其默认行为是中断整个程序的执行)
从上面的也能看出,在try
块内放置哪些语句,都是设计者的主观意愿。如果某一语句有可能引发一场,而它不在块内,则该异常不会在此函数内被捕获处理,这可能有问题,也可能没有。并非每个函数都必须处理每一个可能发生的异常。
例如上面的dereference运算符并未将check_integrity()
调用放在try
内(即使check_integrity()
可能会引发异常),这是因为dereference运算符并未准备好要处理那些异常,即使异常发生,dereference运算符被中断也是安全的。
那么问题就来了:如何知道某个函数是否能够安全地忽略可能发生的异常呢?再次回到之前dereference运算符的定义:
inline int Triangular_iterator::
operator*()
{
check_integrity();
return Triangular::_elems[_index];
}
为了便于对比说明,假设check_integrity()
的设计是返回true
或false
(如下)(而非上面抛出异常的设计),我们的要求是如果_index
无效,就不能执行return
语句。那么上面代码可重写为:
inline int Triangular_iterator::
operator*()
{
return check_integrity()
? Triangular::_elems[_index]
: 0;
}
那么上述这个dereference运算符便有必要在check_integrity()
返回false
时进行某种防范措施,而dereference运算符的调用者同样需要在dereference运算符返回0时进行相应的防范措施。
但由于第一份check_integrity()
是以“抛出异常”的方式表现错误,因此只有在无任何异常抛出情况下,dereference运算符才会执行return
语句,只要发生任何异常,函数便会在“return
语句被执行前”终端中断。这就回答了上面的问题。
人们常犯的错误是:将C++异常和segmentation fault或是bus error这类硬件异常混淆在一起,面对任何一个被抛出的C++异常,都可以在程序某处找到一个相应的throw
表达式。(有些深藏在标准库中)
7.4 局部资源管理
下面的函数首先要求分配相关资源,然后进行某些处理操作。函数结束前,这些资源会被释放。
extern Mutex m;
void f()
{
// 请求资源
int *p = new int;
m.acquire();
process(p);
// 释放资源
m.release();
delete p;
}
问题出在,如果process()
本身或其调用的函数出现异常,那么之后的两条用以释放资源的语句便不会被执行。
一种可能的解决办法是导入try
块以及相应的catch
子句。捕获所有异常、释放所有资源,再将异常重新抛出:
void f()
{
try{
// ...
}
catch(...)
{
m.release();
delete p;
throw;
}
}
这样虽然可以解决问题,但释放资源的代码得出现两次,而且捕获异常、释放资源、重新抛出异常这些操作会使异常处理程序的搜寻时间进一步延长。我们希望写出更具防护性、更自动化的处理方式。在C++中这通常意味着定义一个专属的class。
所谓资源管理(resource management)的手法,就是在初始化阶段即进行资源请求(resource acquisition is initialization)。对对象而言,初始化操作发生于constructor内,资源请求亦是。而资源的释放则应该在destructor内完成。这样虽然无法将资源管理自动化,但可简化我们的程序:
#include<memory>
void f()
{
auto_ptr<int> p(new int);
MutexLock ml(m);
process(p);
// p和ml的destructor会在此处被悄悄调用
}
p
和ml
都是局部(local)对象。如果process()
执行无误,则相应的destructor便会在函数结束前自动作用于p
和ml
身上,资源则被释放;如果执行过程中有任何异常被抛出,C++保证,在异常处理机制终结某个函数前,函数中所有的局部对象的destructor都会被调用。因此,在上面的例子中,无论是否有异常被抛出,p
和ml
的destructor保证被调用。
下面的例子实现了MutexLock
class
class MutexLock{
public:
MutexLock(Mutex m):_lock(m)
{_lock.acquire();}
~MutexLock(){_lock.release();}
private:
Mutex &_lock;
};
auto_ptr
是标准库提供的class template,它会自动删除通过new
表达式分配的对象,例如先前例子中的p
。使用它之前,它将dereference运算符和arrow运算符予以重载(如4.6节),使我们能像使用指针一样地使用auto_ptr
对象:
// 使用`auto_ptr`之前,必须包含相应的`memory`头文件:
#include <memory>
auto_ptr<string> aps(new string("vermeer"));
string *ps = new string("vermeer");
if((aps->size() == ps->size()) && (*aps == *ps))
// ...
在03版本中标准库第一次引入了智能指针std::auto_ptr
,这也是当时唯一的智能指针类型。但是在11版本中,其被废弃。
7.4 标准异常
如果new
表达式无法从程序的空闲空间分配到足够的内存,他会抛出bac_alloc异常对象,例如:
vector<string>*
init_text_vector(ifstream &infile)
{
vector<string> *ptext = 0;
try{
ptext = new vector<string>;
//打开file和file vector
}
catch(bad_alloc){
cerr<<"ouch.head memory exhaausted!\n";
// 清理并推出
}
return ptext;
}
语句ptext = new vector<string>;
会分配足够的内存,然后将vector<string>
default constructor应用于heap对象之上,然后再将对象地址设置给ptext
若没有足够内存足以表现一个vector<string>
对象,default constructor就不会被调用,ptext
也不会被设置值,因为这时bad_alloc
异常对象会被抛出,程序流程会跳到try
块后的catch
子句,下面这个声明:
catch(bad_alloc) // bad_alloc是个class,不是一个object
并未声明出任何对象,因为我们只对捕获到的类型感兴趣,并未打算在catch
子句中实际操作该对象
当然,若果想要抑制bad_alloc异常被抛出,我们可以这样写:
ptext = new(nothrow) vector<string>;
// 当new操作失败,会返回0.任何人使用ptext前,都应检验它是否为0
标准库定义了一套异常类体系(exception class hierarchy),其根部是名为exception的抽象基类。exception
声明有一个what()
虚函数,会返回一个const char *
,用以表示被抛出异常的文字描述。bad_alloc派生自exception基类,我使用的编译器会返回“bad allocation”这样的信息。
我们也可以将自己编写的iterator_overflow继承于exception基类之下。首先必须包含标准头文件exception
,而且必须提供自己的what()
:
#include<exception>
class iterator_overflow : public exception{
public:
iterator_overflow(int index,int max)
:_index(index),_max(max) {}
int index() {return _index;}
int max() {return _max;}
// overrides exception::what()
const char* what() const;
private:
int _index;
int _max;
}
// iterator_overflow的what()的某种实现方式,
// 其中运用ostringstream对象对输出信息进行了格式化
#include<sstream> // 使用ostringstream class前,须先包含该标准头文件
#include<string>
const char*
iterator_overflow::what() const
{
ostringstream ex_msg;
static string msg;
// 将输出信息写到内存内的ostringstream对象中
// 将整数值转为字符串表示
ex_msg << "Internal error:current index "<< _index
<<" exceeds maximum bound: " << _max;
// 萃取出string对象
msg = ex_msg.str();
// 萃取出const char*表述式
return msg.c_str();
}
代码说明:
ostringstream
class提供“内存的输出操作”,输出到一个string
对象上。当我们需要将多笔不同类型的数据格式化为字符串时,它能自动将_index、_max
这类数值对象转换为相应的字符串,我们不必考虑储存空间、转换算法等问题。ostringstream
提供的member functionstr()
,会将“与ostringstream
对象相呼应”的那个string
对象返回。- 标准库让
what()
返回一个const char *
,而非一个string
对象。string
class里的转换函数c_str()
将string
对象转成C-style字符串,返回我们所要的const char*
- iostream库提供的
istringstream
class能将非字符串数据的字符串表示(eg:整数值、内存地址)转换为其实际类型
将iterator_overflow融入标准的exception类体系的好处是,它可以被任何“打算捕获抽象基类exception”的程序代码所捕获。这意味着不必修改原有的程序代码,就可以让那些程序代码认识这个class。我们也不必再用一网打尽的方式来捕获所有异常。例如下面的catch
子句就能捕捉exception所有派生类,当bad_alloc异常被抛出,就会打印出“bad allocation”信息;当biterator_overflow异常被抛出,就会打印出“Internal error : current index …”
catch(const exception &ex)
{
cerr<<ex.what()<<endl;
}