9.使用异常处理,我们能将问题的检测和问题的解决分离。程序的一部分检测到的问题可以简单的将其抛出(throw),检测部分可以不必了解问题如何处理;程序的另一部分(指调用可能抛出异常的模块实现具体功能的部分)则可以通过try-catch捕获异常,然后处理,这样把异常的处理留给具体功能实现时不失为一个最佳的选择。
10.异常处理中,检测部分抛出对象的类型决定了哪个处理部分的代码被激活,被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那个。
11.异常对象与catch进行匹配的规则很严格,一般除了以下几种情况外,异常类型必须与catch的说明类型完全匹配:允许非const到const的转换,允许派生类到基类的转换,将数组和函数类型转换为对应的指针。
12.异常类型匹配将选择第一个找到的可以处理该异常的catch,因此catch子句列表中,最特殊的catch必须最先出现。而带有因继承而相关的类型的多个catch子句,必须将派生类的处理代码放在基类类型处理代码之前。
13.throw表达式抛出的异常对象不同于一般的局部对象,局部对象会在局部模块退出时撤销,而异常对象由编译器管理,而且保证驻留在可能被激活的任意catch都可以访问到的空间中。这个由编译器管理的异常对象由throw表达式创建,并被初始化为被抛出表达式的副本,异常对象将传给对应的catch并在完全处理后才撤销。
14.由于异常抛出时都进行了一次副本拷贝,因此异常对象必须是可以复制的。
15.抛出一个表达式时,被抛出对象的静态编译时类型将决定异常对象的类型。
16.抛出指针通常是一个坏主意,因为抛出指针要求在对应处理代码存在的任意地方都存在指针所指向的对象(注意此时throw抛出时复制的是指针本身,不会去复制指针指向的内容);而且如果该指针是指向派生类对象的基类指针,则那个对象将被分割只抛出基类部分(第15条中的静态类型规则)。
17.基类异常对象可以用于捕获派生类的异常对象,因此如果catch子句处理因继承而相关的类型,它就应该将自己的形参定义为引用来激活运行时调用的多态性。
18.catch可以继续将捕获到的异常抛出,它使用不带表达式的throw语句重新将异常抛出,如:throw;。被重新抛出的异常对象是原来的异常对象,与catch的形参无关(如原来抛出的是派生类Deriver,catch形参是基类Base,则重新抛出后的异常类型是Deriver),当然如果catch形参是引用的话,原来的异常对象可能已被catch修改了。
19.可以用catch(...){}来捕获所有的异常,catch(...){}经常与重新抛出表达式结合使用,catch(...)完成可做的所有局部工作,然后重新抛出异常。
20.构造函数包括初始化列表的异常处理:
view plaincopy to clipboardprint?
Foo::Foo(int n)
try:size(n), array(new int[n])
{
//...
}
catch(const bad_alloc& e)
{
//...
}
view plaincopy to clipboardprint?
Foo::Foo(int n)
try:size(n), array(new int[n])
{
//...
}
catch(const bad_alloc& e)
{
//...
}
Foo::Foo(int n)
try:size(n), array(new int[n])
{
//...
}
catch(const bad_alloc& e)
{
//...
}
这里的函数测试块将初始化列表和函数体中的代码都纳入try块中。
--------------------------------------------------------------------------------
第二部分:
1.标准异常类定义在四个头文件中:exception,new,type_info,stdexcept。
2.exception中定义了exception类,new中定义了bad_alloc类,type_info中定义了bad_cast类,stdexcept中定义了runtime_error、logic_error类。
3.runtime_error类(表示运行时才能检测到的异常)包含了overflow_error、underflow_error、range_error几个子类;logic_error类(一般的逻辑异常)包含了domain_error、invalid_argument、out_of_range、length_error几个子类;而所有的这些类都是exception类的子类。
4.exception、bad_alloc、bad_cast类只定义了默认构造函数,无法在创建这些异常的时候提供附加信息。其它异常类则只定义了一个接受字符串的构造函数,字符串初始化式用于为所发生的异常提供更多的信息。
5.所有异常类都有一个what虚函数,它返回一个指向C风格字符串的指针。
6.应用程序可以从exception或者中间基类派生自已的异常类来扩充exception类层次。
7.异常说明跟在函数形参表之后,一个异常说明在关键字throw之后跟着一个由圆括号括住的异常类型表,如:void foo(int) throw(bad_alloc, invalid_argument);。异常列表还可以为空:void foo(int) throw();,表示该函数不抛出任何异常。
8.异常说明有用的一种重要情况是,如果函数可以保证不会抛出异常。确定函数将不抛出任何异常,对函数的使用者和对编译器都是非常有用的。知道函数不抛出异常会简化编写该函数异常安全的代码工作,而编译器则可以执行被抛出异常抑制的代码优化。
9.标准异常类中的析构函数和what虚函数都承诺不抛出异常,如what的完整声明为:virtual const char* what() const throw();。
10.派生类中的虚函数不能抛出基类虚函数中没有声明的新异常,这样在编写代码时才有一个可依赖的事实:基类中的异常列表是虚函数的派生类版本可以抛出的异常列表的超集。
11.上半部分说过,在异常抛出栈展开的时候,编译器会适当撤销函数退出前分配的局部空间,如果局部对象是类类型,则自动调用它的析构函数。但如果在函数内单独地使用new动态的分配了内存,而且在释放资源之前发生了异常,那么栈展开时这个动态空间将不会被释放。而由类类型对象分配的资源不管是静态的还是动态的一般都会适当的被释放,因为栈展开时保证调用它们的析构函数。因此,在可能存在异常的程序以及分配资源的程序最好使用类来管理那些资源,看一个例子:
view plaincopy to clipboardprint?
void f()
{
const int N=10;
int* p=new int[N];
if(...)
{
throw exception;
}
delete[] p;
}
view plaincopy to clipboardprint?
void f()
{
const int N=10;
int* p=new int[N];
if(...)
{
throw exception;
}
delete[] p;
}
void f()
{
const int N=10;
int* p=new int[N];
if(...)
{
throw exception;
}
delete[] p;
}
当这个异常发生时,p指向的动态空间将不会被正常撤销。现在我们用类来管理这个资源:
view plaincopy to clipboardprint?
template
class Resource
{
private:
unsigned int size;
T* data;
public:
Resource(unsigned int _size=0):size(_size),data(new T[_size])
{
for(unsigned int i=0; i }
Resource(const Resource& r):size(r.size),data(new T[r.size])
{
for(unsigned int i=0; i }
Resource& operator=(const Resource& r)
{
if(&r!=this)
{
size=r.size;
delete[] data;
data=new T[size];
for(int i=0; i }
return *this;
}
~Resource() { delete[] data; }
T operator[](unsigned int index);
const T operator[](unsigned int index) const;
};
void f()
{
const int N=10;
Resource p(N);
if(...)
{
throw exception;
}
}
view plaincopy to clipboardprint?
template
class Resource
{
private:
unsigned int size;
T* data;
public:
Resource(unsigned int _size=0):size(_size),data(new T[_size])
{
for(unsigned int i=0; i }
Resource(const Resource& r):size(r.size),data(new T[r.size])
{
for(unsigned int i=0; i }
Resource& operator=(const Resource& r)
{
if(&r!=this)
{
size=r.size;
delete[] data;
data=new T[size];
for(int i=0; i }
return *this;
}
~Resource() { delete[] data; }
T operator[](unsigned int index);
const T operator[](unsigned int index) const;
};
void f()
{
const int N=10;
Resource p(N);
if(...)
{
throw exception;
}
}
template
class Resource
{
private:
unsigned int size;
T* data;
public:
Resource(unsigned int _size=0):size(_size),data(new T[_size])
{
for(unsigned int i=0; i }
Resource(const Resource& r):size(r.size),data(new T[r.size])
{
for(unsigned int i=0; i }
Resource& operator=(const Resource& r)
{
if(&r!=this)
{
size=r.size;
delete[] data;
data=new T[size];
for(int i=0; i }
return *this;
}
~Resource() { delete[] data; }
T operator[](unsigned int index);
const T operator[](unsigned int index) const;
};
void f()
{
const int N=10;
Resource p(N);
if(...)
{
throw exception;
}
}
这里即使抛出了异常,也会自动调用对象p的析构函数。
12.异常抛出栈展开的时候,编译器对局部类对象析构函数的自动运行导致了一个重要的编程技巧的出现,它使程序更为异常安全的。通过定义一个类来封装资源的分配和释放,可以保证正确的释放资源。这一技术常称为“资源分配即初始化”,简称为RAII。第11条已给出了它的用法。