一、简单的例子
首先通过一个简单的例子来熟悉C++ 的 try/catch/throw:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
catch 的数据类型需要与throw出来的数据类型相匹配的。
二、catch(...)的作用
catch(…)能够捕获多种数据类型的异常对象,所以它提供给程序员一种对异常对象更好的控制手段,使开发的软件系统有很好的可靠性。因此一个比较有经验的程序员通常会这样组织编写它的代码模块,如下:
void Func()
{
try
{
}
catch(DataType1& d1)
{
}
catch(DataType2& d2)
{
}
catch(DataType3& d3)
{
}
// 注意上面try block中可能抛出的DataType1、DataType2和DataType3三
// 种类型的异常对象在前面都已经有对应的catch block来处理。但为什么
// 还要在最后再定义一个catch(…) block呢?这就是为了有更好的安全性和
// 可靠性,避免上面的try block抛出了其它未考虑到的异常对象时导致的程
// 序出现意外崩溃的严重后果,而且这在用VC开发的系统上更特别有效,因
// 为catch(…)能捕获系统出现的异常,而系统异常往往令程序员头痛了,现
// 在系统一般都比较复杂,而且由很多人共同开发,一不小心就会导致一个
// 指针变量指向了其它非法区域,结果意外灾难不幸发生了。catch(…)为这种
// 潜在的隐患提供了一种有效的补救措施。
catch(…)
{
}
}
三、异常中采用面向对象的处理
首先看下面的例子:
void OpenFile(string f)
{
try
{
}
catch(FileOpenException& fe)
{
}
}
void ReadFile(File f)
{
try
{
}
catch(FileReadException& fe)
{
}
}
void WriteFile(File f)
{
try
{
}
catch(FileWriteException& fe)
{
}
}
void Func()
{
try
{
}
// 注意:FileException是FileOpenException、FileReadException和FileWriteException
// 的基类,因此这里定义的catch(FileException& fe)能捕获所有与文件操作失败的异
// 常。
catch(FileException& fe)
{
}
}
下面是更多面向对象和异常处理结合的例子:
#include <iostream.h>
class ExceptionClass
{
public:
}
void main()
{
}
这是输出信息:
Construct Test
Construct my throw
Destruct my throw
****************
Destruct my throw
Destruct Test
======================================
不过一般来说我们可能更习惯于把会产生异常的语句和要throw的异常类分成不同的类来写,下面的代码可以是我们更愿意书写的:
class ExceptionClass
{
public:
};
class ArguClass
{
public:
};
_tmain()
{
}
输出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
四、构造和析构中的异常抛出
先看个程序,假如我在构造函数的地方抛出异常,这个类的析构会被调用吗?可如果不调用,那类里的东西岂不是不能被释放了?
#include <iostream.h>
#include <stdlib.h>
class ExceptionClass1
{
public:
};
void main()
{
}
结果为:
ExceptionClass1()
throw a exception
在这两句输出之间,我们已经给S分配了内存,但内存没有被释放(因为它是在析构函数中释放的)。应该说这符合实际现象,因为对象没有完整构造。
为了避免这种情况,我想你也许会说:应避免对象通过本身的构造函数涉及到异常抛出。即:既不在构造函数中出现异常抛出,也不应在构造函数调用的一切东西中出现异常抛出。
但是在C++中可以在构造函数中抛出异常,经典的解决方案是使用STL的标准类auto_ptr。
那么,在析构函数中的情况呢?我们已经知道,异常抛出之后,就要调用本身的析构函数,如果这析构函数中还有异常抛出的话,则已存在的异常尚未被捕获,会导致异常捕捉不到。
五、标准C++异常类
标准异常都派生自一个公共的基类exception。基类包含必要的多态性函数提供异常描述,可以被重载。下面是exception类的原型:
class exception
{
public:
};
C++有很多的标准异常类:
namespace std
{
}
标准库异常类定义在以下四个头文件中
exception
runtime_error
range_error
overflow_error
underflow_error
logic_error
domain_error
invalid_argument
length_error
out_of_range
class BadInitializers
{
public:
};
class OutOfBounds
{
public:
};
class SizeMismatch
{
public:
};
然后要在程序中需要的地方使用throw来抛出异常类,两个抛出异常类的例子如下
template <class T>
Array1D<T>::Array1D(int sz)
{
}
template <class T>
T &Array1D<T>::operator[](int i) const
{
}
然后在主程序中使用try...catch...来捕获异常,并进行相应的处理,如下
try
{
}
六、try finally使用
__try
{
}
__finally
{
}
在上面的代码段中,操作系统和编译程序共同来确保结束处理程序中的__f i n a l l y代码块能够被执行,不管保护体(t r y块)是如何退出的。不论你在保护体中使用r e t u r n,还是g o t o,或者是longjump,结束处理程序(f i n a l l y块)都将被调用。
我们来看一个实列:(返回值:10, 没有Leak,性能消耗:小)
DWORD Func_SEHTerminateHandle()
{
DWORD dwReturnData = 0;
HANDLE hSem = NULL;
const char* lpSemName = "TermSem";
hSem =
__try
{
}
__finally
{
}
dwReturnData += 5;
return dwReturnData;
}
这段代码应该只是做为一个基础函数,我们将在后面修改它,来看看结束处理程序的作用:
====================
在代码加一句:(返回值:5, 没有Leak,性能消耗:中下)
DWORD Func_SEHTerminateHandle()
{
DWORD dwReturnData = 0;
HANDLE hSem = NULL;
const char* lpSemName = "TermSem";
hSem =
__try
{
}
__finally
{
}
dwReturnData += 5;
return dwReturnData;
}
在try块的末尾增加了一个return语句。这个return语句告诉编译程序在这里要退出这个函数并返回dwTemp变量的内容,现在这个变量的值是5。但是,如果这个return语句被执行,该线程将不会释放信标,其他线程也就不能再获得对信标的控制。可以想象,这样的执行次序会产生很大的问题,那些等待信标的线程可能永远不会恢复执行。
通过使用结束处理程序,可以避免return语句的过早执行。当return语句试图退出try块时,编译程序要确保finally块中的代码首先被执行。要保证finally块中的代码在try块中的return语句退出之前执行。在程序中,将ReleaseSemaphore的调用放在结束处理程序块中,保证信标总会被释放。这样就不会造成一个线程一直占有信标,否则将意味着所有其他等待信标的线程永远不会被分配CPU时间。
在finally块中的代码执行之后,函数实际上就返回。任何出现在finally块之下的代码将不再执行,因为函数已在try块中返回。所以这个函数的返回值是5,而不是10。
读者可能要问编译程序是如何保证在try块可以退出之前执行finally块的。当编译程序检查源代码时,它看到在try块中有return语句。这样,编译程序就生成代码将返回值(本例中是5)保存在一个编译程序建立的临时变量中。编译程序然后再生成代码来执行f i n a l l y块中包含的指令,这称为局部展开。更特殊的情况是,由于try块中存在过早退出的代码,从而产生局部展开,导致系统执行finally块中的内容。在finally块中的指令执行之后,编译程序临时变量的值被取出并从函数中返回。
可以看到,要完成这些事情,编译程序必须生成附加的代码,系统要执行额外的工作。
finally块的总结性说明
我们已经明确区分了强制执行finally块的两种情况:
• 从try块进入finally块的正常控制流。
• 局部展开:从try块的过早退出(goto、long jump、continue、break、return等)强制控制转移到finally块。
第三种情况,全局展开( global unwind),这个以后再看。
七、C++异常参数传递
从语法上看,在函数里声明参数与在catch子句中声明参数是一样的,catch里的参数可以是值类型,引用类型,指针类型。例如:
try
{
}
catch(A a)
{
}
catch(B& b)
{
}
catch(C* c)
{
}
class A;
void func_throw()
{
}
try
{
}
catch(A a)
{
}
当我们抛出一个异常对象时,抛出的是这个异常对象的拷贝。当异常对象被拷贝时,拷贝操作是由对象的拷贝构造函数完成的。该拷贝构造函数是对象的静态类型(static type)所对应类的拷贝构造函数,而不是对象的动态类型(dynamic type)对应类的拷贝构造函数。此时对象会丢失RTTI信息。
异常是其它对象的拷贝,这个事实影响到你如何在catch块中再抛出一个异常。比如下面这两个catch块,乍一看好像一样:
catch (A& w) // 捕获异常
{
// 处理异常
throw; // 重新抛出异常,让它继续传递
}
catch (A& w) // 捕获Widget异常
{
// 处理异常
throw w; // 传递被捕获异常的拷贝
}
看看以下这三种声明:
catch (A w) ... // 通过传值
catch (A& w) ... // 通过传递引用,一个被异常抛出的对象(总是一个临时对象)可以通过普通的引用捕获
catch (const A& w) ... //const引用
catch (A w) ... // 通过传值捕获
会建立两个被抛出对象的拷贝,一个是所有异常都必须建立的临时对象,第二个是把临时对象拷贝进w中。实际上,编译器会优化掉一个拷贝。同样,当我们通过引用捕获异常时,
catch (A& w) ... // 通过引用捕获
catch (const A& w) ... //const引用捕获
这仍旧会建立一个被抛出对象的拷贝:拷贝是一个临时对象。相反当我们通过引用传递函数参数时,没有进行对象拷贝。话虽如此,但是不是所有编译器都如此。
另外,通过指针抛出异常与通过指针传递参数是相同的。不论哪种方法都是一个指针的拷贝被传递。你不能认为抛出的指针是一个指向局部对象的指针,因为当异常离开局部变量的生存空间时,该局部变量已经被释放。Catch子句将获得一个指向已经不存在的对象的指针。这种行为在设计时应该予以避免。
另外一个重要的差异是在函数调用者或抛出异常者与被调用者或异常捕获者之间的类型匹配的过程不同。在函数传递参数时,如果参数不匹配,那么编译器会尝试一个类型转换,如果存在的话。而对于异常处理的话,则完全不是这样。见一下的例子:
void func_throw()
{
}
try
{
func_throw();
}
catch(const char* s)
{
}
抛出的是CString,如果用const char*来捕获的话,是捕获不到这个异常的。
尽管如此,在catch子句中进行异常匹配时可以进行两种类型转换。第一种是基类与派生类的转换,一个用来捕获基类的catch子句也可以处理派生类类型的异常。反过来,用来捕获派生类的无法捕获基类的异常。
第二种是允许从一个类型化指针(typed pointer)转变成无类型指针(untyped pointer),所以带有const void* 指针的catch子句能捕获任何类型的指针类型异常:
catch (const void*) ... //可以捕获所有指针异常
另外,你还可以用catch(...)来捕获所有异常,注意是三个点。
传递参数和传递异常间最后一点差别是catch子句匹配顺序总是取决于它们在程序中出现的顺序。因此一个派生类异常可能被处
理其基类异常的catch子句捕获,这叫异常截获,一般的编译器会有警告。
class A {
public:
A()
{
cout << "class A creates" << endl;
}
void print()
{
cout << "A" << endl;
}
~A()
{
cout << "class A destruct" << endl;
}
};
class B: public A
{
public:
B()
{
cout << "class B create" << endl;
}
void print()
{
cout << "B" << endl;
}
~B()
{
cout << "class B destruct" << endl;
}
};
void func()
{
}
try
{
}
catch( B& b) //必须将B放前面,如果把A放前面,B放后面,那么B类型的异常会先被截获。
{
}
catch (A& a)
{
}