Essential C++ Chapter 7学习记录(异常处理)

Chapter 7 异常处理

4.6节实现Triangular_iteratorclass时,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子句加以处理。

例如下例尝试在firstlast所标示的范围内,寻找特定的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()的设计是返回truefalse(如下)(而非上面抛出异常的设计),我们的要求是如果_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会在此处被悄悄调用
}

pml都是局部(local)对象。如果process()执行无误,则相应的destructor便会在函数结束前自动作用于pml身上,资源则被释放;如果执行过程中有任何异常被抛出,C++保证,在异常处理机制终结某个函数前,函数中所有的局部对象的destructor都会被调用。因此,在上面的例子中,无论是否有异常被抛出,pml的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对象。stringclass里的转换函数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;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值