异常处理

在C++中,定义了标准异常类,我们可以使用它们来进行异常处理。它们的关系如下:基类是exception,他有4个派生类:bad_alloc,bad_cast,runtime_error,logic_error;其中runtime_error有3个派生类:overflow_error、underflow_error、range_error;logic_error有个派生类:domain_error、invalid_argument、length_error以及out_of_range。


异常处理的基本流程是:由待检测的部分抛出一个对象,此后,程序不会执行throw后面的语句,(如果你在throw前面new了一个指针,而在throw后才delete,那么就悲剧了!这个问题我们稍后再讨论)而是转到匹配的catch语句处理。catch处理完以后,执行与try相关的最后一个catch后面的语句。

先看一个简单的例子:

Sales_item operator+(const Sales_item& lhs,const Sales_item& rhs)
{
	
	if(!lhs.same_isbn(rhs))
		throw runtime_error("data must refer to same ISBN ");
	
	Sales_item ret(lhs);
	ret += rhs;
	return ret;
}

int main()
{	
	Sales_item trans1("aaa",10,200);
	Sales_item trans2("bbb",5,100);
	Sales_item trans3;
	try{
	trans3 = trans1 + trans2;
	}catch(const runtime_error &e)
	{
		cerr<<e.what()<<" try agian! "<<endl;
	}
	cout<<trans3;	
	return 0;
}


异常处理有3个关键点:抛出异常时会发生什么,捕获异常时会发生什么,以及用来传递错误对象的含义。在这里,抛出的是一个runtime_error类的异常,并用string对象来初始化这个异常。然后程序将跳转到对应的catch语句处(后面再讨论如何跳转),然后在多条catch语句中寻找第一条匹配的,然后通过传递给catch语句的实参,进行异常处理。这里的处理很简单,打印runtime_error对象的what函数的返回值,这个返回值是初始化runtime_error的C风格的字符串。

注意:在处理异常时,抛出异常的块中的局部存储不存在了。而被抛出的对象不是局部储存的,而是用throw表达式初始化一个异常对象,这个对象由编译器管理,驻留在可能被激活的任意catch都能访问的空间。由于异常抛出时都进行了一次副本拷贝,因此异常对象必须是可以复制的。

通常抛出的类型是任意的,只要我们在catch捕获相同的类型,你完全可以写成:

throw string("data must refer to same ISBN ");

 


而在catch中:

 

catch(const string &e)
	{
		cerr<<e<<" try agian! "<<endl;
	}


但有一点要注意:抛出类型的静态类型决定了异常对象的类型。通常这并没有任何问题,但是当你抛出的是对指针的解引时,问题就来了。根据动态绑定,假设你指向的是一个派生类成员,但是指针的类型却是基类,那么就只能抛出基类部分。如果你抛出的不是指针的解引,而是指针本身,问题就更大了:因为抛出指向局部对象的指针时后,并不能保证这个对象还存在。

 

那么到底是如何查找到catch的呢?是通过一种成为栈展开的方式:先检查throw是不是在try的内部,如果是,检查与该try相关的catch句子,看有没有匹配的,如果有,处理该异常。举一个例子:

	try{
	if(!lhs.same_isbn(rhs))
		throw string("data must refer to same ISBN ");
	}
	catch(const string &e)
	{
		cerr<<e<<" try agian! "<<endl;
	}


但这种情况其实比较少见,因为我们定义异常处理的本意,就是将异常的检测和处理过程分离,要不然的话,我们可以直接:

	if(!lhs.same_isbn(rhs))
	{
		cerr<<"data must refer to same ISBN"<<endl;
		exit(0);

	}


如果找不到,就退出当前函数(释放资源、撤销局部对象),在调用函数中查找。这就是我们例子中的方式。

 

在栈展开的过程,会自动撤销资源。但是如果一个块直接分配资源,并在释放之前异常,那么栈栈展开期间都不会释放资源。如果释放的类类型,那么会调用该类的析构函数,如果析构函数也发生异常,那么会怎么样呢?这会导致调用标准库的terminate函数,而这个函数将调用abort函数,导致整个程序非正常退出。

 

具体是哪个catch呢?这取决于异常说明符的类型与throw类型的匹配。如果有多个符合条件的匹配,则选择第一个catch(而不是最匹配的catch)。其实这种匹配是非常严格的,不能存在隐式类型转化,只存在const与非const之间的转化,数组与指针的转换,以及派生类到基类的转化。正因为如此,当catch的异常说明符是派生类的引用时,也可以捕获基类的异常对象,从而发生匹配。所以,当我们写catch时,一定要把派生类异常说明符的catch写在前面,而把基类的写在后面,否则每次catch到异常,都会交由异常说明符为基类类型引用的catch来处理,举一个例子,对于原始的例子,发出的是throw runtime_error("data must refer to same ISBN ");如果异常处理程序这么写:

	try { 
		trans3 = trans1 + trans2;
	} 
	catch(exception e) 
	{ 
         cerr<<"exception "<<endl;    
	} 
	catch(const runtime_error &re)
	{ 
          cerr<<" runtime_error "<<endl;  
	}
	catch(overflow_error eobj)
	{
		cerr<<" overflow_error "<<endl;
	}


 

那么打印的结果是第一条语句,虽然第二条更加匹配,但是编译器总是选择匹配的第一条语句。在在这种情况下,我们通常是把派生类类型的异常说明符写在前面,而把基类的写在后面。
如果有的异常在这个层次中不能处理,那么我们可以通过throw;语句重新抛出该异常。注意:异常的类型是原来异常的对象,不是catch的形参:

	try{

		try { 
			trans3 = trans1 + trans2;
			} 
		catch(exception e) 
		{ 
        		cerr<<"exception "<<endl; 
			throw;
		
		} 
	}
	catch(const runtime_error &re)
	{ 
          cerr<<" runtime_error "<<endl;  
	}


通常由于我们不知道到底会抛出什么异常,可以通过catch(...)来捕获所有异常:

	try{
		try{

			try { 
				trans3 = trans1 + trans2;
				} 
			catch(exception e) 
			{ 
				 cerr<<"exception "<<endl; 
				throw;
		
			} 
			}
		catch(const overflow_error eobj)
		{ 
          cerr<<" overflow_error"<<endl;  
		}

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


 

程序有一点需要注意:程序触发的异常是runtime_error,catch(exception e) 会捕获它并重新发出该异常,这个异常catch(const overflow_error eobj)处理不了,而是通过catch(...)来处理的。显而易见的,当使用多个catch时,catch(...)必须放在最后一个。


通常,类的构造函数容易发生异常。对于构造函数,很可能发生异常,而对于构造函数列表中初始化的变量,检测异常需要一些特殊的格式,举一个例子:

template <class T> class Foo
{
public:
	Foo(const T& v)
		//构造函数初始化列表
		try:val(v),ptr(new T(v))
	{
		//构造函数体
		cout<<"构造函数"<<endl;
	}catch(const std::bad_alloc &e){cout<<"初始化失败"<<endl;}

	~Foo(){delete ptr;}

	void get(){cout<<val<<"\t"<<*ptr<<endl;}
private:
	T val;
	T *ptr;
};


因为构造函数列表是执行在构造函数体之前的,所以要把try放在列表之前,这样的话就可以从整个构造函数中抛出异常了。
通常我们可以定义自己异常类,也可以从标准异常类中继承下来:

class isbn_mismatch: public std::logic_error
{
public:
	string left,right;
	explicit isbn_mismatch(const string &s):logic_error(s){}	
	isbn_mismatch(const string &s,const string &lhs,const string &rhs):
	logic_error(s),left(lhs),right(rhs){}

	virtual ~isbn_mismatch() throw(){}
};

那么重载+操作符需要修改为:

	if(!lhs.same_isbn(rhs))
		throw isbn_mismatch("ISBN mismatch ",lhs.book(),rhs.book());

在main函数中修改为:

	try{
	trans3 = trans1 + trans2;
	}catch(const isbn_mismatch &e)
	{
		cerr<<e.what()<<": left ISBN: "<<e.left<<" right ISBN: "<<e.right <<endl;
	}


 

我们前面提到过,如果我们在析构函数中释放了资源,那么即使出现异常,在撤销类的时候会调用析构函数而释放资源。如果new了一个指针,而在delete之前发生了异常,那么这个指针将不会被撤销。这促使我们使用更安全的编程策略:异常安全的编程技术—即使异常发生,程序也能正确操作。我们通过一个类来封装资源的分配和释放,而不是传统的把数据直接暴漏出来。这样即使出现异常,类的析构函数也能释放资源。
在C++标准库中,定义auto_ptr类,这个类就完成了上述工作,它在memory头文件中定义先看一个简单的例子:


 

void f1()
{
	int *ptr = new int(1024);
	throw std::runtime_error (" 故意引起的错误");
	delete ptr;
}

void f()
{
	auto_ptr<int> pi(new int(1024));
	throw std::runtime_error (" 故意引起的错误");
}

通过对比它的使用就一目了然了,但是我们需要注意:
1.auto_ptr只能管理new 的指针,不能管理动态分配的数组。更不能用它保存指向静态对象的指针。因为当auto_ptr被撤销时,会试图删除这个指针,从而发生错误。
2.必须使用初始化的方法将auto_ptr对象与指针绑定auto_ptr<int> pi(new int(1024));而不能使用赋值操作符:auto_ptr<int> pi = new int(1024)。
3.这个类重载了解引和箭头操作符,很方便我们的使用:

	auto_ptr<Sales_item> pi(new Sales_item("aaa",10,20));
	pi->book();




 

4.有一点需要特别注意:auto_ptr对象的复制和赋值是破坏性操作:这与内置的指针有很大的区别。当复制auto_ptr对象或者用它给别的auto_ptr对象赋值时,基础对象的所有权从原来的对象转化成了新的对象,原来的对象处于未绑定状态。对于赋值操作,不仅将基础对象的所有权从右操作数传给了左操作数,还删除了左操作的基础对象。因此,我们不能将它储存在容器中,容器要求存储的类型在复制(或者赋值)后,两个对象的指针要相等,而auto_ptr会删除其中的一个对象。
5.必须使用get函数来测试auto_ptr是否绑定,不能直接if(p)来判断指针是否有效,;必须使用reset给对象重新赋值不能直接 p = new int (1024);将地址赋给auto_ptr对象,需要使用reset函数:

int main()
{
	auto_ptr<int> p (new int (1024));
	auto_ptr<int> p1 = p;
	//if(p)
	if(p.get()== 0)
		// p = new int (1024)
		p.reset( new int( 1000));
	cout<<*p1<<endl;
	cout<<*p<<endl;

	return 0;

}

异常说明:如果函数抛出异常,被抛出的异常将是包含在该说明中的一种。我们可以在形参列表之后加上throw,再跟上(可能为空的)异常类型列表:

 

void divide(int dividend = 0,int divisor = 0) throw(std::runtime_error)
{
	if(! divisor)
		throw runtime_error("divisor must not equal to 0! ");
	cout<<dividend/divisor<<endl;

}

这表明该函数如果抛出一个异常,那么这个异常将是runtime_error或者是它的派生类。如果没有异常说明,则表示他可以抛出任何类型的异常;如果列表为空,则不抛出任何异常。

如果程序抛出了没有在异常列表中列出的异常,那么就会调用标准库中的unexpected函数,这个函数调用terminate函数终止程序。编译器不会检查异常说明与抛出的异常是否匹配。这样看来异常说明有点鸡肋,但是有一种情况却可以使用它:如果函数可以确保不抛出任何异常时,我们将throw后面的异常类型为设为空。此时,这个函数就可以执行被可能抛出异常代码所抑制的优化。

注意,当一个函数提供了异常说明后,异常说明也是这个函数类型的一部分,在指向函数的指针中会用到。此时,除了形参,返回值之外,指针抛出的类型也是衡量匹配的标准之一,我们要确保:指针抛出的类型必须更加宽松:指针声明为可以抛出任何异常,函数声明为抛出runtime_error,这样是可以的。如果指针声明不抛出异常,那么函数就不能抛出异常了,否则无法用这个函数给指向函数的指针赋值。同样的情况出现在指针的赋值中:源指针的异常说明必须至少与目标指针一样严格。


在处理继承关系时,我们要确保,派生类的异常说明类型不能比基类“宽松”:基类函数抛出的异常是派生类的超集。举个例子:

class Base
{
public:
	virtual double f1(double) throw();
	virtual int f2(int) throw(std::logic_error);
	virtual string f3()throw (std::logic_error,std::runtime_error);

};

class Derived:public Base
{
public:
	//错误,基类不抛出,而派生类抛出
	double f1(double) throw(std::underflow_error);
	//正确:二者级别一样
	int f2(int) throw(std::logic_error);
	//正确:派生类的是基类的特例
	string f3()throw (std::logic_error);
};


关于异常的问题就是这么多了。

 

你可能感兴趣的:(编程,exception,String,delete,Class,编译器)