异常处理

异常通信的好处

使用异常处理,程序中独立开发的各部分能够就程序执行期间出现的问题相互通信,并处理这些问题。

注解:通过异常我们能够将问题的检测和问题的解决分离,这样程序的问题检测部分可以不必了解如何处理问题 C++的异常处理中,需要有问题检测部分抛出一个对象给处理代码,通过这个对象的 类型和内容,两部分能够就出现了什么错误进行通信

示例:

1
2
3
4
5
6
7
8
Sales_item operator +(const Sale_item& lhs, const Sale_item& rhs)
{
if(!lhs.same_isbn(rhs))
throw runtime_error("Data must refer to same isbn");
Sale_item ret(lhs);
ret += rhs;
return ret;
}

程序中将Sale_item对象相加的那些部分可以使用一个try块,以便在异常发生的时候捕获异常

1
2
3
4
5
6
7
8
9
10
11
12
13
Sale_item item1 , item2 , sum;
while(cin>>item1>>item2)
{
try
{
sum = item1 + item2;
}
catch(const runtime_error(&e))
{
cerr<<e.what()<<"Try again.\n"
<<endl;
}
}

抛出类类型的异常

异常是通过抛出对象而引发的。

  • 该对象的类型决定应该激活哪个处理代码,被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那个
  • 异常以类似于将实参传递给函数的方式抛出和捕获, 可能传给引用或非引用,这意味着必须能够复制该类型的对象
  • 不存在数组或函数类型的异常,因为如果抛出这两种类型的异常会被自动转换为指针的
    – 如果抛出一个数组,被抛出的对象转换为指向数组首元素的指针
    – 如果抛出一个函数,函数被转化为指向该函数的指针

  • 一旦执行throw的时候,不会执行跟在throw后面的语句,而是将控制从throw转移到匹配的catch。控制从一个地方转到另一个地方

有另个含义:

  1. 沿着调用链的函数提早退出
  2. 一般而言,在处理异常的时候,抛出异常的块中的局部存储不在了

因为在处理异常的时候会释放局部存储,所以被抛出的对象就不能再局部存储,而是用throw表达式 初始化一个称为异常对象的特色对象。异常对象由编译器管理,而且保证驻留在 可能被激活的任意catch都可以访问的 空间 (这个对象由throw创建,并被初始化被抛出的表达式的副本。异常对象将传给对应的catch,并且在完全处理之后c撤销

异常对象与继承

注解:当抛出一个表达式的时候,被抛出对象的静态编译时类型将决定异常对象的类型,所以当抛出问题的时候我们可以确切的知道异常类型

异常与指针

用抛出表达式抛出静态类型时,比较麻烦的一种情况是:在抛出中对指针解引用。对指针的解引用的结果是一个对象,其类型与指针的类型相匹配。如果指针指向继承层次中的一种类型,指针所指对象的类型就可能与指针的类型不同。

无论对象的实际类型是什么,异常对象的类型都与指针的静态类型相匹配。

无论对象的实际类型是什么,异常对象的类型都与指针的静态类型相匹配。

如果该指针是一个指向派生类对象的基类指针,则那个对象将被分割,只抛出基类部分。

如果抛出的是指针本身,可能会引发比分割对象更严重的问题。 具体而言,抛出指向局部对象的指针总是错误的,其理由与从函数返回局部对象的指针同罪。抛出指针的时候,必须确定进入处理代码时指针所指向的对象存在

如果抛出指向局部对象的指针,而且处理代码在另一个函数中,则执行处理代码时指针所指向的对象将不再存在

及时处理代码在同一函数中,也必须确信指针所指的对象在catch处存在。如果指针指向某个在catch之前退出的块中的对象,那么, 将在catch之前撤销该局部对象

栈展开

简而言之,就是沿着嵌套函数调用链接向上,直至为异常找到一个catch语句,只要找到能够处理异常的catch子句,就进入该catch子句,并在该处理代码中继续执行。当catch结束后,退出整个异常处理语句,继续执行与给try块相关的最后一个catch子句之后的点继续执行

为局部对象调用析构函数

栈展开期间,提早退出包含throw的函数和调用链中可能的其他函数。

一般而言,这些函数已经创建了可以在退出函数时撤销的局部对象。因异常而退出函数时,编译器保证适当地撤销 局部对象。

每个函数退出时,它的局部存储都被释放,在释放内存之前,撤销在异常发生之前创建的所有对象。

如果局部对象是类类型,那么调用它的析构函数,如果是内置类型,不撤销

注意啦: 如果一个块直接分配资源,而且在释放资源之前发生异常,在栈展开期间将不会释放该资源 例如:一个块通过调用new动态分配内存,如果该块因异常而退出,编译器不会删除该指针,已分配的内存不会释放

析构函数应该从不抛出异常

理由:在为某个异常进行栈展开的时候,析构函数如果又抛出自己未处理的另一个异常,将会导致调用 标准库函数 terminate 函数,一般而言, terminate函数将调用abort函数,强制程序退出,所以,这样做,没意义!

异常与构造函数

构造函数可以抛出异常,一旦抛出异常,那么之前构造的成员必须逐个析构

未捕获的异常终止程序

不能不处理异常。如果找不到匹配的catch语句,程序将调用terminate函强制终止

捕获异常

catch语句中的异常说明符看起来像只包含一个形参的形参表。

  • 说明符的类型决定了处理代码能够捕获的异常种类。类型必须是完全类型
  • catch为了处理异常只需要了解异常的类型的时候,异常说明符可以省略形参名;如果处理代码需要已发生异常 的类型之外的信息,则异常说明符就包含形参名,catch使用这个名字访问异常对象

查找匹配的处理代码

注意:在查找匹配的catch期间,找到的catch不必是与异常最匹配的那个catch,相反,将选择第一个找到的可以处理该异常的 catch语句。 因此,最特殊的catch必须最先出现!!!

异常与catch异常说明符匹配的规则

  • 完全匹配
  • 允许从非constconst的转换,也就是说,非const对象的throw可以与指定接收const引用的catch匹配
  • 允许从派生类到基类的转换
  • 允许将数组转换为指向数组类型的指针,将函数转化为指向函数类型的指针
  • ———除上述转换为,其他一概非法——————————————–

异常说明符

进入catch的时候,用异常对象初始化catch形参。像函数形参一样

  • 如果是引用,则像引用形参一样,不存在单独的catch对象,对catch形参所做的改变会作用于异常对象
  • 如果是非引用,那就没什么好说的啦,无论如何改变形参,异常对象不会改变

异常说明符与继承

基类的异常说明符可以用于捕获派生类的异常对象,而且,异常说明符的静态类型决定catch子句可以执行的动作。如果被抛出的异常对象是派生类类型的,但由接受基类类型的 catch 处理,那么,catch不能使用派生类特有的任何成员

注解: 通常,如果catch子句处理因继承而相关的类型的异常,它就应该将自己的形参定义为引用

如果catch形参是引用类型,catch就可以直接访问异常对象,这样catch对象的静态类型可以与其动态类型不同。 如果不是引用,那只能取异常对象的静态类型部分来进行操作

//只有通过引用或指针调用时,才会发生动态绑定,通过对象的调用不进行动态绑定!!

catch子句的次序必须反映类型的层次

因为catch子句按出现的次序匹配,所以使用来自继承层次的异常的程序必须将它们的catch子句进行排序,以便派生类型的处理代码出现在其基类的catch之前

重新抛出

可能单个catch不能完全处理一个异常。在进行了一些校正行动之后,catch可能确定该异常必须由函数调用链中更上层的函数来处理,catch语句就可以重新抛出将异常上传(调用链的上层)表达式为:

throw;

当然重新抛出的还是原来传递过来的异常对象,而不是catch形参。

  • catch形参是基类类型的时候,我们不知道由重新抛出表达式的实际类型,取决于该异常对象的动态类型
  • catch形参是引用的时候,catch语句中所做的 改变会影响到重抛的异常对象
  • ————除此之外,没有其他意外发生—————————–

捕获所有异常的处理代码

1
2
catch(...) //该子句可以捕获所有类型的异常
{}

catch子句经常与重新抛出表达式相结合使用
实例:

1
2
3
4
5
6
7
8
9
10
void main()
{
try
{}
catch(...)
{
//做一些局部工作啦
throw;
}
}

函数测试块与构造函数

为了检测构造函数初始化列表那一部分可能出现的异常,伟大的C++发明者这样获取它的异常
例:

1
2
3
4
5
6
7
template <class T>
Handle<T>::Handle(T *p)
try:ptr(p) , use(new size_t(1))
{
}
catch(const std::bad_alloc &e)
{ handle_out_of_memory(e)}

人家管这种定义try块的方式为函数测试块
当然,构造函数要处理来自构造函数初始化式的异常,唯一的方法是将构造函数编写为函数测试块

异常类的层次

如图:

exception类型所定义的唯一操作是一个名为what的虚成员,该函数返回 const char*对象,它一般用来在抛出位置构造异常对象的信息。

当然,应用程序还经常通过从exception类或者中间基类派生附加类型来扩充exception层次
例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class out_of_stock: public std::runtime_error
{
public:
explicit out_of_stock(const std::string &s):std::runtime_error(s){}
};
class isbn_mismatch : public std::logic_error
{
public:
explicit isbn_mismatch(const std::string &s):logic_error(s){}
isbn_mismatch(const std::string &s , const std::string &lhs, const std::string &rhs):
std::logic_error(s) , left(lhs) , right(rhs){}
const std::string left , right;
virtual ~isbn_mismatch() throw() {} //这个的解释要往后看哦
};

使用自定义的异常类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Sale_item operator+(const Sale_item& lhs , const Sale_item& rhs)
{
if(!lhs.same_isbn(rhs))
throw isbn_mismatch("isbn mismatch" , lhs , rhs);
Sale_item ret(lhs);
ret += rhs;
return ret;
}
Sale_item itm1 , itm2, sum;
while(cin>>itm1>>itm2)
{
try
{
sum = itm1 +itm2;
}
catch(const isbn_mismatch &e)
{
cerr<<e.what()<<": left isbn("<<e.left
<<") right isbn ("<<e.right<<")"
<<endl;
}
}

自动资源释放

考虑下面这个问题:

1
2
3
4
5
6
7
8
9
10
void f()
{
vector<string> v;
string s;
while(cin>>s)
v.push_back(s);
string *p = new string[v.size()];
//do some
delete []p;
}

如果在函数内部发生异常,则将撤销vector对象而不会释放数组。原因嘛,你懂得啦(不就是析构函数不能析构指针么)

用类管理资源分配

RAII :资源分配即初始化

该技术通过定义一个类来封转资源的分配和释放,可以保证正确的资源释放

其原理呢很简单:
定义一个类,这个类呢,构造函数负责分配资源,而析构函数负责释放资源。
想要分配的资源的时候,就定义该类的类对象,如果不发生异常,就在获得资源的对象超出作用域的时候释放资源。更为重要的是:如果创建了对象之后但在它超出作用域之前发生异常,那么编译器保证撤销该对象,作为展开定义对象的作用域的一部分

auto_ptr

1
2
3
4
5
6
7
8
9
10
11
12
#include <memory>
=============================================================================================
auto_ptr<T> ap 创建名为ap的未绑定的auto_ptr对象
auto_ptr<T> ap(p) 创建名为ap的auto_ptr对象,ap拥有指针p指向的对象,该构造函数为explicit
auto_ptr<T> ap(ap2) ap保存原来存储的ap2中的指针。将所有权转给ap,ap2成为未绑定的auto_ptr对象
ap1 = ap2 将所有权从ap2转给ap1,删除ap1所指的对象并且使ap1指向ap2指向的对象,使ap2成为未绑定的
~ap 析构函数,删除ap所指对象
*ap 返回对ap所绑定的对象的引用
ap-> 返回ap保存的指针
ap.reset(p) 如果p与ap的值不同,则删除ap指向的对象并且将ap绑定到p
ap.release() 返回ap所保存的指针并且使ap成为未绑定的
ap.get() 返回ap保存的指针

注意:auto_ptr只能用于管理从new返回的一个对象,它不能管理动态分配的数组。

auto_ptrr被复制或赋值的时候,有不寻常的行为,因此,不能将auto_ptr存储在标准库容器类型中

为异常安全的内存分配使用auto_ptr

那么上一节的问题就有解啦:

1
2
3
4
5
void f()
{
auto_ptr<int> ap(new int 42);
}
//在这个例子中,编译器保证在栈展开的越过f之前运行ap的析构函数

auto_ptr是可以保保存任何类型指针的模板

auto_ptr绑定到指针

常见情况下,将auto_ptr对象初始化为由new表达式返回的对象的地址

1
auto_ptr<string> ap1(new string ("hello"));

因为接受指针的的构造函数为explicit 构造函数,所以必须使用初始化的直接形式来创建auto_ptr对象

1
2
auto_ptr<int> pi = new int(1024); //error
auto_ptr<int> pi(new int (1024));

使用auto_ptr对象

1
2
3
4
5
6
7
8
9
string *ptr = new string ("hello");
if(ptr -> empty())
//do anything
=>
auto_ptr<string> ap1(new string ("hello"));
*ap1 = "Trex"; //直接赋值
string s= *ap1;
if(ap1 -> empty())
//auto_ptr 重载了* 和 -> 操作符,所以可以像上述那样使用

注解:auto_ptr的主要目的是,在保证自动删除auto_ptr对象引用的对象的同时,支持普通指针式的行为

auto_ptr 对象的赋值和复制是破坏性操作

示例:

1
2
auto_ptr<string> ap1(new string("hello"));
auto_ptr<string> ap2(ap1); //……这的动作就不解说了,看前面的那张“表” ,这样做的目的是防止共享指针带来的后果

赋值删除左操作数指向的对象

1
2
auto_ptr<string> ap3(new string("func"));
ap3 = ap2;
  • 删除了ap3指向的对象
  • 将ap3置为指向ap2所指的对象
  • ap2是未绑定的auto_ptr对象

auto_ptr的默认构造函数

1
auto_ptr<int> p_auto;

默认情况下,auto_ptr的内部指针置为0,对未绑定的auto_ptr对象解引用,程序出错

测试auto_ptr对象

废话不多说,直接上例子:

1
2
3
4
5
6
if(p_auto)
*p_auto =1024; //error,can not use an auto_ptr as a condition
=>
if(p_auto.get())
*p_auto = 1024;
//要测试auto_ptr对象,必须使用它的get成员,该成员返回包含在auto_ptr对象中的基础指针

注意:应该只用get询问auto_ptr对象或者使用返回的指针值,不能用get作为创建其他auto_ptr对象的实参

因为这样做违反auto_ptr的设计原则:在任何时刻只有一个auto_ptr对象保存给定的指针,如果两个auto_ptr对象保存相同的指针,该指针会被delete两次

reset 操作

auto_ptr对象与内置指针的另一个区别是:不能直接将一个地址(或者其他指针)赋给auto_ptr对象:

1
p_auto = new int(1024);

相反,必须调用reset函数来改变指针

1
2
3
4
5
if(p_auto.get())
*p_auto = 1024;
else
p_auto.reset(new int (1024));
//当参数是0 , 相当于复位p_auto

注意:调用 auto_ptr 对象的 reset 函数时,在将 auto_ptr 对象绑定到其他对象之前,会删除 auto_ptr 对象所指向的对象(如果存在)。但是,正如自身赋值是没有效果的一样,如果调用该 auto_ptr 对象已经保存的同一指针的reset 函数,也没有效果,不会删除对象。

异常说明

异常说明指定:如果函数抛出异常,被抛出的异常将是包含在该说明中的一种,或者是从列出的异常中派生的类型

警告:auto_ptr的缺陷 :

存在下列限制:

  • 不要使用auto_ptr对象保存指向静态分配的指针,否则,当auto_ptr对象本身被销毁的时候,它将试图删除指向非动态分配对象的指针,导致未定义行为
  • 永远不要使用两个auto_ptr对象指向同一个对象,导致这一错误的明显方式是:
    – 使用同一指针来初始化或者reset两个不同的auto_ptr对象。
    – 使用一个auto_ptr对象的get函数的结果来初始化或者reset另一个auto_ptr对象
  • 不要使用auto_ptr对象保存指向动态分配数组的指针。因为删除对象时只会使用delete ,而不会使用delete[]
  • 不要讲auto_ptr对象存储在容器中,因为auto_ptr 对象的赋值和复制是破坏性操作

定义异常说明

异常说明跟在函数形参之后。一个异常说明在关键字throw之后跟着一个(可能为空的)由圆括号括住的异常类型列表:

1
2
3
4
5
6
void recoup(int) throw(runtime_error);
//这个声明指出,recoup是接受int值的函数,并返回void,如果recoup抛出一个异常,
//该异常将是runtime_error对象,或者是由runtime_error派生的类型的异常
空说明列表指出函数不抛出任何异常
void no_problem() throw();
//异常说明是函数接口的一部分,函数定义以及该函数的任意声明必须具有相同的异常说明

注解:如果一个函数声明没有指定异常说明,则该函数可以抛出任意类型的异常

违反异常说明

如果函数抛出了没有在其异常说明中列出的异常,就调用标准库函数unexpected.默认情况下,unexpected 函数调用terminate函数,然后该函数一般会终止程序

//在编译的时候,编译器不能也不会试图验证异常说明

1
2
3
4
void f() throw()
{ throw exception}
//对此,编译器不会给出任何提示的
//相反,编译器会产生代码以便保证:如果抛出了一个违反异常说明的异常,就调用unexpected函数

确定函数不抛出异常

确定函数将不抛出任何异常,对函数的用户和编译器都有所帮助:知道函数不抛出异常会简化编写调用该函数的异常安全的代码的工作, 而且,如果编译器知道不会抛出异常,它就可以执行 被可能抛出异常的代码所抑制的 优化

异常说明与成员函数

定义方式:(示例说明)

1
2
3
4
5
6
7
8
9
class bad_alloc :public exception
{
public:
bad_alloc() throw();
bad_alloc(const bad_alloc &) throw();
bad_alloc& operator=(const bad_alloc&) throw();
virtual ~bad_alloc() throw() {}
virtual const char* what() const throw(); //如果是const成员函数声明,异常说明跟在const限定符之后
};

异常说明与析构函数

1
2
3
4
5
class isbn_mismatch : public std::logic_error
{
public:
~isbn_mismatch() throw() {}
}

注意:当继承这两个类的一个时 , 我们的析构函数也必须承诺不抛出任何异常

任意其他标准库类析构函数一样 , 不抛出异常。但是,标准库的析构函数没有定义异常说明,在这种情况下, 我们知道,但编译器不知道,,我们必须定义自己的析构函数来恢复析构函数不抛出异常的承诺

异常说明与虚函数

基类中的虚函数的异常说明,可以与派生类中对应虚函数的异常说明不同
但是,派生类虚函数的异常说明必须与对应基类的虚函数的异常说明同样严格,或者比后者更受限
//这个限制保证了当使用指向基类类型的指针调用派生类虚函数的时候,派生类的异常说明不会增加新的
//可抛出的异常

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Base
{
public:
virtual double f1(double) throw();
virtual int f2(int) throw (std::logic_error);
virtual std::string f3() throw(std::logic_error , std::runtime_error);
}
class Derived : public Base
{
public:
double f1(double) throw(std::underflow_error);//error!
int f2(int) throw(std::logic_error);//ok
std::string f3() throw();//ok
};

第一个错误的原因是:派生类中不能再异常说明列表中增加异常,因为继承层次的用户应该能够编写 依赖于该说明列表的代码。如果通过基类指针或引用进行函数调用,那么, 这些类的用户所涉及的应该只是在 基类中指定的异常

注解:基类中的异常列表是虚函数的派生类版本可以抛出的异常列表的超集

1
2
3
4
5
6
7
8
9
10
void compute(Base *pb) throw()
{
try
{
pb->f3();
}
catch(const logic_error &le){}
catch(const runtime_error $re){}
}
//在确定可能需要捕获什么异常的时候,compute函数使用基类中的异常说明

函数指针的异常说明

定义方式:

1
2
void *(pf) (int) throw(runtime_error);
//如果不提供异常说明,该指针就可以指向能够抛出的任意类型异常的具有匹配类型的函数

在用另一个指针初始化带异常说明的函数的指针,或者将后者赋值给函数地址的时候,两个指针的异常说明不必相同, 但是,源指针的异常说明必须至少与目标指针的一样严格
举个例子就明白了:

1
2
3
4
5
void recoup(int) throw(runtime_error);
void (*pf) (int) throw(runtime_error) = recoup; //ok
void (*pf2)(int) throw(runtime_error , logic_error) = recoup;//ok
void (*pf3)(int) throw() = recoup ; //error, recoup is less restrictive than pf3
void (*pf4)(int) = recoup ; //ok , recoup is more restrictive than pf4

解释:
第三个初始化是错误的。指针指示,pf3不抛出任何异常,但是recoup函数可以抛出异常,而源指针recoup函数抛出的 异常超出了目标指针pf3所指定的,所以错误。

热评文章