【C++】一文读懂C++中的异常处理机制

C++ 中的异常处理机制

1.1 什么是异常?

程序有时会遇到运行阶段错误,导致程序无法正常地运行下去。例如,程序可能试图打开一个不可用的文件,请求过多的内存,或者遭遇不能容忍的值。通常,程序员都会试图预防这种意外情况。C++异常为处理这种情况提供了一种功能强大而灵活的工具。

2.0 * x * y / (x + y);

对于上述表达式,如果y是x的负值,则上述公式将导致被零除——一种不允许的运算。对于被零除的情况,很多新式编译器通过生成一个表示无穷大的特殊浮点值来处理,cout将这种值显示为Inf、inf、INF或类似的东西;而其他的编译器可能生成在发生被零除时崩溃的程序。最好编写在所有系统上都以相同的受控方式运行的代码。

1.2 调用abort()

对于这种问题,处理方式之一是,如果其中一个参数是另一个参数的负值,则调用abort()函数。abort()函数的原型位于头文件cstdlib(或stdlib.h)中,其典型实现是向标准错误流(即cerr使用的错误流)发送消息abnormal program termination(程序异常终止),然后终止程序。它还返回一个随实现而异的值,告诉操作系统(如果程序是由另一个程序调用的,则告诉父进程),处理失败。abort()是否刷新文件缓冲区(用于存储读写到文件中的数据的内存区域)取决于实现。如果愿意,也可以使用exit(),该函数刷新文件缓冲区,但不显示消息。

//error1.cpp -- using the abort() function
#include <iostream>
#include <cstdlib>
double hmean(double a, double b);
 
int main() {
    double x, y, z;
    std::cout << "Enter two numbers: ";
    while (std::cin >> x >> y) {
        z = hmean(x,y);
        std::cout << "Harmonic mean of " << x << " and " << y
            << " is " << z << std::endl;
        std::cout << "Enter next set of numbers <q to quit>: ";
    }
    std::cout << "Bye!\n";
    return 0;
}
 
double hmean(double a, double b) {
    if (a == -b) {
        std::cout << "untenable arguments to hmean()\n";
        std::abort();
    }
    return 2.0 * a * b / (a + b); 
}

注意,在hmean( )中调用abort( )函数将直接终止程序,而不是先返回到main( )。一般而言,显示的程序异常中断消息随编译器而异。

1.3 返回错误码

一种比异常终止更灵活的方法是,使用函数的返回值来指出问题。

例如,ostream类的get(void)成员通常返回下一个输入字符的ASCII码,但到达文件尾时,将返回特殊值EOF。

对hmean()来说,这种方法不管用。任何数值都是有效的返回值,因此不存在可用于指出问题的特殊值。在这种情况下,可使用指针参数或引用参数来将值返回给调用程序,并使用函数的返回值来指出成功还是失败。istream族重载>>运算符使用了这种技术的变体。通过告知调用程序是成功了还是失败了,使得程序可以采取除异常终止程序之外的其他措施。下面程序清单是一个采用这种方式的示例,它将hmean()的返回值重新定义为bool,让返回值指出成功了还是失败了,另外还给该函数增加了第三个参数,用于提供答案。

//error2.cpp -- returning an error code
#include <iostream>
#include <cfloat>  // (or float.h) for DBL_MAX
bool hmean(double a, double b, double * ans);
 
int main() {
    double x, y, z;
    std::cout << "Enter two numbers: ";
    while (std::cin >> x >> y) {
        if (hmean(x,y,&z))
            std::cout << "Harmonic mean of " << x << " and " << y
                << " is " << z << std::endl;
        else
            std::cout << "One value should not be the negative "
                << "of the other - try again.\n";
        std::cout << "Enter next set of numbers <q to quit>: ";
    }
    std::cout << "Bye!\n";
    return 0;
}
 
bool hmean(double a, double b, double * ans) {
    if (a == -b) {
        *ans = DBL_MAX;
        return false;
    } else {
        *ans = 2.0 * a * b / (a + b);
        return true;
    }
}

1.4 异常机制

下面介绍如何使用异常机制来处理错误:

C++异常是对程序运行过程中发生的异常情况(例如被0除)的一种响应。异常提供了将控制权从程序的一个部分传递到另一部分的途径。对异常的处理有3个组成部分:

  • 引发异常
  • 使用处理程序捕获异常
  • 使用try块

throw语句实际上是跳转,即命令程序跳到另一条语句。throw关键字表示引发异常,紧随其后的值(例如字符串或对象)指出了异常的特征。

程序使用异常处理程序来捕获异常,异常处理程序位于要处理问题的程序中。catch关键字表示捕获异常。处理程序以关键字catch开头,随后是位于括号中的类型声明,它指出了异常处理程序要响应的异常类型;然后是一个用花括号括起的代码块,指出要采取的措施。catch关键字和异常类型用作标签,指出当异常被引发时,程序应跳到这个位置执行。异常处理程序也被称为try catch块。

try块标识其中特定的异常可能被激活的代码块,它后面跟一个或多个catch块。try块是由关键字try指示的,关键字try的后面是一个由花括号括起的代码块,表明需要注意这些代码引发的异常。

// error3.cpp -- using an exception
#include <iostream>
double hmean(double a, double b);
int main() {
    double x, y, z;
    std::cout << "Enter two numbers: ";
    while (std::cin >> x >> y) {
        try {
            z = hmean(x,y);
        } catch (const char * s) {// start of exception handler
            std::cout << s << std::endl;
            std::cout << "Enter a new pair of numbers: ";
            continue;
        }                       // end of handler
        std::cout << "Harmonic mean of " << x << " and " << y
            << " is " << z << std::endl;
        std::cout << "Enter next set of numbers <q to quit>: ";
    }
    std::cout << "Bye!\n";
    return 0;
}
 
double hmean(double a, double b) {
    if (a == -b)
        throw "bad hmean() arguments: a = -b not allowed";
    return 2.0 * a * b / (a + b); 
}

try块与下面类似:

try{
z = hmean(x,y);
}

如果其中的某条语句导致异常被引发,则后面的catch块将对异常进行处理。如果程序在try块的外面调用hmean( ),将无法处理异常。

引发异常的代码与下面类似:

if (a == -b)
        throw "bad hmean() arguments: a = -b not allowed";

其中被引发的异常是字符串“bad hmean( )arguments: a = -b not allowed”。异常类型可以是字符串(就像这个例子中那样)或其他C++类型;通常为类类型。

执行throw语句类似于执行返回语句,因为它也将终止函数的执行;但throw不是将控制权返回给调用程序,而是导致程序沿函数调用序列后退,直到找到包含try块的函数。在这个例子中,throw将程序控制权返回给main( )。程序将在main( )中寻找与引发的异常类型匹配的异常处理程序(位于try块的后面)。程序中唯一的一个catch块的参数为char*,因此它与引发异常匹配。

catch块点类似于函数定义,但并不是函数定义。关键字catch表明这是一个处理程序,而char*s则表明该处理程序与字符串异常匹配。s与函数参数定义极其类似,因为匹配的引发将被赋给s。另外,当异常与该处理程序匹配时,程序将执行括号中的代码。

执行完try块中的语句后,如果没有引发任何异常,则程序跳过try块后面的catch块,直接执行处理程序后面的第一条语句。

接下来看将10和−10传递给hmean( )函数后发生的情况。If语句导致hmean( )引发异常。这将终止hmean( )的执行。程序向后搜索时发现,hmean( )函数是从main( )中的try块中调用的,因此程序查找与异常类型匹配的catch块。程序中唯一的一个catch块的参数为char*,因此它与引发异常匹配。程序将字符串“bad hmean( )arguments: a = -b not allowed”赋给变量s,然后执行处理程序中的代码。处理程序首先打印s——捕获的异常,然后打印要求用户输入新数据的指示,最后执行continue语句,命令程序跳过while循环的剩余部分,跳到起始位置。continue使程序跳到循环的起始处,这表明处理程序语句是循环的一部分,而catch行是指引程序流程的标签.

在这里插入图片描述

如果函数引发了异常,而没有try块或没有匹配的处理程序时,将会发生什么情况。在默认情况下下,程序最终将调用abort()函数,但可以修改这种行为。稍后将讨论这个问题。

1.5 将对象用作异常类型

通常,引发异常的函数将传递一个对象。这样做的重要优点之一是,可以使用不同的异常类型来区分不同的函数在不同情况下引发的异常。另外,对象可以携带信息,程序员可以根据这些信息来确定引发异常的原因。同时catch块可以根据这些信息来决定采取什么样的措施。例如,下面是针对函数hmean( )引发的异常而提供的一种设计:

// exc_mean.h  -- exception classes for hmean(), gmean()
#include <iostream>
 
class bad_hmean {
private:
    double v1;
    double v2;
public:
    bad_hmean(double a = 0, double b = 0) : v1(a), v2(b){}
    void mesg();
};
 
inline void bad_hmean::mesg() {   
    std::cout << "hmean(" << v1 << ", " << v2 <<"): "
              << "invalid arguments: a = -b\n";
}

//用法:
if(a == -b)
{
    throw bad_hmean(a,b);
}

//函数gmean( )计算两个数的几何平均值,即乘积的平方根。这个函数要求两个参数都不为负,如果参数为负,它将引发异常
class bad_gmean {
public:
    double v1;
    double v2;
    bad_gmean(double a = 0, double b = 0) : v1(a), v2(b){}
    const char * mesg();
};
 
inline const char * bad_gmean::mesg() {  
    return "gmean() arguments should be >= 0\n";
}

try{
    ...
}
catch(bad_hmean & bg)
{
    ...
}
catch(bad_gmean & hg)
{
    ...
}

如果函数hmean( )引发bad_hmean异常,第一个catch块将捕获该异常;如果gmean( )引发bad_gmean异常,异常将逃过第一个catch块,被第二个catch块捕获。

//error4.cpp – using exception classes
#include <iostream>
#include <cmath> // or math.h, unix users may need -lm flag
#include "exc_mean.h"
// function prototypes
double hmean(double a, double b);
double gmean(double a, double b);
int main() {
    using std::cout;
    using std::cin;
    using std::endl;
    double x, y, z;
    cout << "Enter two numbers: ";
    while (cin >> x >> y) {
        try {                  // start of try block
            z = hmean(x,y);
            cout << "Harmonic mean of " << x << " and " << y
                << " is " << z << endl;
            z = gmean(x,y);
            cout << "Geometric mean of " << x << " and " << y
                << " is " << z << endl;
            cout << "Enter next set of numbers <q to quit>: ";
        } catch (bad_hmean & bg) { // start of catch block
            bg.mesg();
            cout << "Try again.\n";
            continue;
        } catch (bad_gmean & hg) {
            cout << hg.mesg();
            cout << "Values used: " << hg.v1 << ", " 
                 << hg.v2 << endl;
            cout << "Sorry, you don't get to play any more.\n";
            break;
        } // end of catch block
    }
    cout << "Bye!\n";
    // cin.get();
    // cin.get();
    return 0;
}
 
double hmean(double a, double b) {
    if (a == -b)
        throw bad_hmean(a,b);
    return 2.0 * a * b / (a + b);
}
 
double gmean(double a, double b) {
    if (a < 0 || b < 0)
        throw bad_gmean(a,b);
    return std::sqrt(a * b); 
}

首先,bad_hmean异常处理程序使用了一条continue语句,而bad_gmean异常处理程序使用了一条break语句。因此,如果用户给函数hmean( )提供的参数不正确,将导致程序跳过循环中余下的代码,进入下一次循环;而用户给函数gmean( )提供的参数不正确时将结束循环。这演示了程序如何确定引发的异常(根据异常类型)并据此采取相应的措施。

其次,异常类bad_gmean和bad_hmean使用的技术不同,具体地说,bad_gmean使用的是公有数据和一个公有方法,该方法返回一个C-风格字符串。

1.6 异常规范和C++11

异常规范,这是C++98新增的一项功能,但C++11却将其摒弃了。这意味着C++11仍然处于标准之中,但以后可能会从标准中剔除,因此不建议您使用它。然而,忽视异常规范前,你至少应该知道它是什么样的,如下所示:

double harm(double a) throw(bad_thing);// may throw bad_thing exception
double marm(double) throw();// doesn't throw an exception

其中的throw()部分就是异常规范,它可能出现在函数原型和函数定义中,可包含类型列表,也可不包含。

异常规范的作用之一是,告诉用户可能需要使用try块。然而,这项工作也可使用注释轻松地完成。

异常规范的另一个作用是,让编译器添加执行运行阶段检查的代码,检查是否违反了异常规范。这很难检查。例如,marm()可能不会引发异常,但它可能调用一个函数,而这个函数调用的另一个函数引发了异常。另外,你给函数编写代码时它不会引发异常,但库更新后它却会引发异常。总之,编程社区(尤其是尽力编写安全代码的开发人员)达成的一致意见是,最好不要使用这项功能。而C++11也建议您忽略异常规范。

然而,C++11确实支持一种特殊的异常规范:您可使用新增的关键字noexcept指出函数不会引发异常:

double marm() noexcept;// marm() doesn't throw an exception

有关这种异常规范是否必要和有用存在一些争议,有些人认为最好不要使用它(至少在大多数情况下如此);而有些人认为引入这个新关键字很有必要,理由是知道函数不会引发异常有助于编译器优化代码。通过使用这个关键字,编写函数的程序员相当于做出了承诺。

还有运算符noexcept(),它判断其操作数是否会引发异常。

1.7 栈解退

1.7.1 return和throw的区别

return 为返回,每个函数调用另一个函数,都会将被调函数的指令地址存于主调函数的栈中,函数层层调用,栈也层层叠加,在return时,当前函数返回给上级函数,直至上级函数return才返回给上上级函数,同时return的函数栈释放,以此类推。

throw 为抛出异常,其目标不再是上层函数,而是try块,try块在哪,它在哪里停止,在层层调用函数下,底层函数throw,会按顺序释放栈,直至遇到try块。

简单一句话:调用return的函数返回上一级,直至在上一级遇到return才会返回上上级,而上一级return之前的语句照常执行。调用throw,自下往上以此释放栈,直到在某一级函数遇见try块,中间级函数的其他代码部分无法继续执行。

1.7.2 什么是栈解退

假设try块没有直接调用引发异常的函数,而是调用了对引发异常的函数进行调用的函数,则程序流程将从引发异常的函数跳到包含try块和处理程序的函数。这涉及到栈解退。

首先来看一看C++通常是如何处理函数调用和返回的。

C++通常通过将信息放在栈(参见第9章)中来处理函数调用。具体地说,程序将调用函数的指令的地址(返回地址)放到栈中。当被调用的函数执行完毕后,程序将使用该地址来确定从哪里开始继续执行。另外,函数调用将函数参数放到栈中。在栈中,这些函数参数被视为自动变量。如果被调用的函数创建了新的自动变量,则这些变量也将被添加到栈中。如果被调用的函数调用了另一个函数,则后者的信息将被添加到栈中,依此类推。当函数结束时,程序流程将跳到该函数被调用时存储的地址处,同时栈顶的元素被释放。因此,函数通常都返回到调用它的函数,依此类推,同时每个函数都在结束时释放其自动变量。如果自动变量是类对象,则类的析构函数(如果有的话)将被调用。

现在假设函数由于出现异常(而不是由于返回)而终止,则程序也将释放栈中的内存,但不会在释放栈的第一个返回地址后停止,而是继续释放栈,直到找到一个位于try块(参见图15.3)中的返回地址。随后,控制权将转到块尾的异常处理程序,而不是函数调用后面的第一条语句。这个过程被称为栈解退。

引发机制的一个非常重要的特性是,和函数返回一样,对于栈中的自动类对象,类的析构函数将被调用。然而,函数返回仅仅处理该函数放在栈中的对象,而throw语句则处理try块和throw之间整个函数调用序列放在栈中的对象。如果没有栈解退这种特性,则引发异常后,对于中间函数调用放在栈中的自动类对象,其析构函数将不会被调用。

在这里插入图片描述

1.8 其他异常特性

虽然throw-catch机制类似于函数参数和函数返回机制,但还是有些不同之处。

其中之一是函数fun( )中的返回语句将控制权返回到调用fun()的函数,但throw语句将控制权向上返回到第一个这样的函数:包含能够捕获相应异常的try-catch组合。

另一个不同之处是,引发异常时编译器总是创建一个临时拷贝,即使异常规范和catch块中指定的是引用。

class problem{};

void super()throw(problem)
{
    if(oh_no){
        problem oops;
        throw oops;
    }
}

try{
    super();
}
catch(problem &p)
{
    
}

p将指向oops的副本而不是oops本身。这是件好事,因为函数super()执行完毕后,oops将不复存在。顺便说一句,将引发异常和创建对象组合在一起将更简单:

throw problem();

您可能会问,既然throw语句将生成副本,为何代码中使用引用呢?毕竟,将引用作为返回值的通常原因是避免创建副本以提高效率。

答案是,引用还有另一个重要特征:基类引用可以执行派生类对象。假设有一组通过继承关联起来的异常类型,则在异常规范中只需列出一个基类引用,它将与任何派生类对象匹配。

假设有一个异常类层次结构,并要分别处理不同的异常类型,则使用基类引用将能够捕获任何异常对象;而使用派生类对象只能捕获它所属类及从这个类派生而来的类的对象。

引发的异常对象将被第一个与之匹配的catch块捕获。这意味着catch块的排列顺序应该与派生顺序相反:

class bad_1{}
class bad_2 : public bad_1{...}
class bad_3 : public bad_2{...}

void duper()
{
    if(oh_no){
        throw bad_1();
    }
    if(rats){
        throw bad_2();
    }
    if(drat){
        throw bad_3();
    }
}

try{
    duper();
}
catch(bad_3 &be)
{//statements
}
catch(bad_2 &be)
{//statements
}
catch(bad_1 &be)
{//statements
}

如果将bad_1 &处理程序放在最前面,它将捕获异常bad_1、bad_2和bad_3;通过按相反的顺序排列,bad_3异常将被bad_3 &处理程序所捕获。

如果有一个异常类继承层次结构,应这样排列catch块:将捕获位于层次结构最下面的异常类
的catch语句放在最前面,将捕获基类异常的catch语句放在最后面。

通过正确地排列catch块的顺序,让您能够在如何处理异常方面有选择的余地。然而,有时候可能不知道会发生哪些异常。例如,假设您编写了一个调用另一个函数的函数,而您并不知道被调用的函数可能引发哪些异常。在这种情况下,仍能够捕获异常,即使不知道异常的类型。方法是使用省略号来表示异常类型,从而捕获任何异常:

catch(...){}

如果知道一些可能会引发的异常,可以将上述捕获所有异常的catch块放在最后面,这有点类似于switch语句中的default:

try {
    duper();
} catch (bad_3 &be) {
    // statements
} catch (bad_2 &be) {
    // statements
} catch (bad_1 &be) {
    // statements
} catch (...) {
    // statements
}

1.9 excepyion类

C++异常的主要目的是为设计容错程序提供语言级支持,即异常使得在程序设计中包含错误处理功能更容易,以免事后采取一些严格的错误处理方式。异常的灵活性和相对方便性激励着程序员在条件允许的情况下在程序设计中加入错误处理功能。

较新的C++编译器将异常合并到语言中。为支持该语言,exception头文件(以前为exception.h或except.h)定义了exception类,C++可以把它用作其他异常类的基类。代码可以引发exception异常,也可以将exception类用作基类。有一个名为what( )的虚拟成员函数,它返回一个字符串,该字符串的特征随实现而异。然而,由于这是一个虚方法,因此可以在从exception派生而来的类中重新定义它:

#include <exception>
class bad_hmean : public std::exception {
public:
    const char* what() { return "bad arguments to hmean()"; }
...
};
 
class bad_gmean : public std::exception {
public:
    const char* what() { return "bad arguments to gmean()"; }
...
};

如果不想以不同的方式处理这些派生而来的异常,可以在同一个基类处理程序中捕获它们:

try {
    ...
} catch (std::exception& e {
    cout << e.what() << endl;
    ...
}

否则,可以分别捕获它们。C++库定义了很多基于exception的异常类型。

1.9.1 stdexcept异常类

头文件<stdexcept>定义了其他几个异常类。首先,该文件定义了logic_error和runtime_error类,它们都是以公有方式从exception派生而来的:

class logic_error : public exception {
public:
    explicit logic_error(const string& what_arg);
    explicit logic_error(const char* what_arg);
...
};
 
class domain_error : public logic_error {
public:
    explicit domain_error(const string& what_arg);
    explicit domain_error(const char* what_arg);
...
};

注意,这些类的构造函数接受一个string对象作为参数,该参数提供了方法what( )以C-风格字符串方式返回的字符数据。

这两个新类被用作两个派生类系列的基类。异常类系列logic_error描述了典型的逻辑错误。总体而言,通过合理的编程可以避免这种错误,但实际上这些错误还是可能发生的。每个类的名称指出了它用于报告的错误类型:

  • domain_error;

数学函数有定义域(domain)和值域(range)。定义域由参数的可能取值组成,值域由函数可能的返回值组成。例如,正弦函数的定义域为负无穷大到正无穷大,因为任何实数都有正弦值;但正弦函数的值域为−1到+1,因为它们分别是最大和最小正弦值。另一方面,反正弦函数的定义域为−1到+1,值域为−π到+π。如果你编写一个函数,该函数将一个参数传递给函数std::asin(),则可以让该函数在参数不在定义域−1到+1之间时引发domain_error异常。

  • invalid_argument;

异常invalid_argument指出给函数传递了一个意料外的值。例如,如果函数希望接受一个这样的字符串:其中每个字符要么是‘0’要么是‘1’,则当传递的字符串中包含其他字符时,该函数将引发invalid_argument异常。

  • length_error;

异常length_error用于指出没有足够的空间来执行所需的操作。例如,string类的append()方法在合并得到的字符串长度超过最大允许长度时,将引发length_error异常。

  • out_of_bounds。

异常out_of_bounds通常用于指示索引错误。例如,您可以定义一个类似于数组的类,其operator()[]在使用的索引无效时引发out_of_bounds异常。

每个类独有一个类似于logic_error的构造函数,让您能够提供一个供方法what( )返回的字符串。

接下来,runtime_error异常系列描述了可能在运行期间发生但难以预计和防范的错误。每个类的名称指出了它用于报告的错误类型:

  • range_error;
  • overflow_error;
  • underflow_error。

下溢(underflow)错误在浮点数计算中。一般而言,存在浮点类型可以表示的最小非零值,计算结果比这个值还小时将导致下溢错误。整型和浮点型都可能发生上溢错误,当计算结果超过了某种类型能够表示的最大数量级时,将发生上溢错误。计算结果可能不再函数允许的范围之内,但没有发生上溢或下溢错误,在这种情况下,可以使用range_error异常。

每个类独有一个类似于runtime_error的构造函数,让您能够提供一个供方法what( )返回的字符串。

一般而言,logic_error系列异常表明存在可以通过编程修复的问题,而runtime_error系列异常表明存在无法避免的问题。所有这些错误类有相同的常规特征,它们之间的主要区别在于:不同的类名让你能够分别处理每种异常。另一方面,继承关系让你能够一起处理它们(如果你愿意的话)。

如果上述库类不能满足你的需求,应该从logic_error或runtime_error派生一个异常类,以确保你异常类可归入同一个继承层次结构中。

1.9.2 bad_alloc异常和new

对于使用new导致的内存分配问题,C++的最新处理方式是让new引发bad_alloc异常。头文件包含bad_alloc类的声明,它是从exception类公有派生而来的。但在以前,当无法分配请求的内存量时,new返回一个空指针。

// newexcp.cpp -- the bad_alloc exception
#include <iostream>
#include <new>
#include <cstdlib>  // for exit(), EXIT_FAILURE
using namespace std;
 
struct Big {
    double stuff[20000];
};
 
int main() {
    Big * pb;
    try {
        cout << "Trying to get a big block of memory:\n";
        pb = new Big[10000]; // 1,600,000,000 bytes
        cout << "Got past the new request:\n";
    } catch (bad_alloc & ba) {
        cout << "Caught the exception!\n";
        cout << ba.what() << endl;
        exit(EXIT_FAILURE);
    }
    cout << "Memory successfully allocated\n";
    pb[0].stuff[0] = 4;
    cout << pb[0].stuff[0] << endl;
    delete [] pb;
    // cin.get();
    return 0; 
}

img

1.9.3 空指针和new

很多代码都是在new在失败时返回空指针时编写的。为处理new的变化,有些编译器提供了一个标记(开关),让用户选择所需的行为。当前,C++标准提供了一种在失败时返回空指针的new,其用法如下:

int* pi = new (std::nothrow) int;
int* pa = new (std::nowthrow) int[500];
Big* pb = new (std::nothrow) Big[10000];
if (pb == 0) {
    cout << "Could not allocate memory. Bye.\n";
    exit(EXIT_FAILURE);
}

1.10 异常,类和继承

异常、类和继承以三种方式相互关联。首先,可以像标准C++库所做的那样,从一个异常类派生出另一个;其次,可以在类定义中嵌套异常类声明来组合异常;第三,这种嵌套声明本身可被继承,还可用作基类。

1.11 异常何时会迷失方向

异常被引发后,在两种情况下,会导致问题。

  • 首先,如果它是在带异常规范的函数中引发的,则必须与规范列表中的某种异常匹配(在继承层次结构中,类类型与这个类及其派生类的对象匹配),否则称为意外异常(unexpected exception)。在默认情况下,这将导致程序异常终止(虽然C++11摒弃了异常规范,但仍支持它,且有些现有的代码使用了它)。

  • 如果异常不是在函数中引发的(或者函数没有异常规范),则必须捕获它。如果没被捕获(在没有try块或没有匹配的catch块时,将出现这种情况),则异常被称为未捕获异常(uncaught exception)。在默认情况下,这将导致程序异常终止。

    然而,可以修改程序对意外异常和未捕获异常的反应。下面来看如何修改,先从未捕获异常开始。

1.11.1 未捕获异常

未捕获异常不会导致程序立刻异常终止。相反,程序将首先调用函数terminate( )。在默认情况下,terminate( )调用abort( )函数。可以指定terminate( )应调用的函数(而不是abort( ))来修改terminate( )的这种行为。为此,可调用set_terminate( )函数。set_terminate( )和terminate( )都是在头文件exception中声明的:

typedef void (*terminate_handler)();
terminate_handler set_terminate(terminate_handler f) throw();// C++98
terminate_handler set_terminate(terminate_handler f) noexcept;// C++11
void terminate();// C++98
void terminate() noexcept;// C++11

其中的typedef使terminate_handler成为这样一种类型的名称:指向没有参数和返回值的函数的指针。set_terminate()函数将不带任何参数且返回类型为void的函数的名称(地址)作为参数,并返回该函数的地址。如果调用了set_terminate()函数多次,则terminate()将调用最后一次set_terminate()调用设置的函数。

来看一个例子。假设希望未捕获的异常导致程序打印一条消息,然后调用exit()函数,将退出状态值设置为5。

#include <exception>
using namespace std;
...
void myQuit() {
    cout << "Terminating due to uncaught exception\n";
    exit(5);
}
...
int main() {
    set_terminate(myQuit);
...
};

现在,如果引发了一个异常且没有被捕获,程序将调用terminate(),而后者将调用MyQuit()。

1.11.2 意外异常

接下来看意外异常。通过给函数指定[异常规范],可以让函数的用户知道要捕获哪些异常。假设函数的原型如下:

double Argh(double, double) throw(out_of_bounds);

则可以这样使用该函数:

try {
    x = Argh(a, b);
} catch (out_of_bounds& ex) {
    ...
}

知道应捕获哪些异常很有帮助,因为默认情况下,未捕获的异常将导致程序异常终止。

意外异常行为与未捕获的异常极其类似。如果发生意外异常,程序将调用unexpected()函数。这个函数将调用terminate(),它在默认情况下将调用abort()。正如有一个可用于修改terminate()的行为的set_terminate()函数一样,也有一个可用于修改unexpected()的行为的set_unexpected()函数。这些新函数也是在头文件中声明的:

typedef void (*unexpected_handler)();
unexpected_handler set_unexpected(unexpected_handler f) throw();// C++98
unexpected_handler set_unexpected(unexpected_handler f) noexcept;// C++11
void unexpected();// C++98
void unexpected() noexcept;// C++11

然而,与提供给set_terminate()的函数的行为相比,提供给set_unexpected()的函数的行为受到更严格的限制。具体地说,unexpected_handler函数可以:

  • 通过调用terminate()(默认行为)、abort()或exit()来终止程序;
  • 引发异常。

引发异常(第二种选择)的结果取决于unexpected_handler函数所引发的异常以及引发意外异常的函数的[异常规范]:
如果新引发的异常与原来的[异常规范]匹配,则程序将从那里开始进行正常处理,即寻找与新引发的异常匹配的catch块。基本上,这种方法将用预期的异常取代意外异常;

如果新引发的异常与原来的[异常规范]不匹配,且[异常规范]中没有包括std::bad_exception类型,则程序将调用terminate()。bad_exception是从exception派生而来的,其声明位于头文件<exception>中;

如果新引发的异常与原来的[异常规范]不匹配,且原来的[异常规范]中包含了std::bad_exception类型,则不匹配的异常将被std::bad_exception异常所取代。

总之,如果要捕获所有的异常(不管是预期的异常还是意外异常),则可以这样做:

#include <exception>//首先确保异常头文件的声明可用:
using namespace std;
...
void myUnexcepted() {//然后,设计一个替代函数,将意外异常转换为bad_exception异常,该函数的原型如下
    throw std::bad_exception();// or just throw
}//仅使用throw,而不指定异常将导致重新引发原来的异常。然而,如果异常规范中包含了这种类型,则该异常将被bad_exception对象所取代。

 
double Argh(double, double) throw(out_of_bounds, bad_exception);
 
int main() {
    set_unexpected(myUnexcepted);//在程序的开始位置,将意外异常操作指定为调用该函数:
    try {
        x = Argh(a, b);//最后,将bad_exception类型包括在异常规范中,并添加如下catch块序列:
    } catch (out_of_bounds& ex) {
        ...
    } catch (bad_exception& ex) {
        ...
    }
}

1.12 有关异常的注意事项

  • 异常规范不适用于模板,因为模板函数引发的异常可能随特定的具体化而异。
  • 异常和动态内存分配并非总能协同工作。

下面进一步讨论动态内存分配和异常。首先,请看下面的函数:

void test1(int n) {
    string mesg("I'm trapped in an endless loop");
    ...
    if (oh_no)
        throw exception();
    ...
    return;
}

string类采用动态内存分配。通常,当函数结束时,将为mesg调用string的析构函数。虽然throw语句过早地终止了函数,但它仍然使得析构函数被调用,这要归功于栈解退。因此在这里,内存被正确地管理。

接下来看下面这个函数:

void test2(int n) {
    double* ar = new double[n];
    ...
    if (oh_no)
        throw exception();
    ...
    delete[] ar;
    return;
}

这里有个问题。解退栈时,将删除栈中的变量ar。但函数过早的终止意味着函数末尾的delete[]语句被忽略。指针消失了,但它指向的内存块未被释放,并且不可访问。总之,这些内存被泄漏了。

这种泄漏是可以避免的。例如,可以在引发异常的函数中捕获该异常,在catch块中包含一些清理代码,然后重新引发异常:

void test3(int n) {
    double* ar = new double[n];
    ...
    try {
        if (oh_no)
            throw exception();
    } catch (exception& ex) {
        delete[] ar;
        throw;
    }
    ...
    delete[] ar;
    return;
}

然而,这将增加疏忽和产生其他错误的机会。另一种解决方法是使用第16章将讨论的智能指针模板之一。

总之,虽然异常处理对于某些项目极为重要,但它也会增加编程的工作量、增大程序、降低程序的速度。另一方面,不进行错误检查的代价可能非常高。

异常处理

在现代库中,异常处理的复杂程度可能再创新高——主要原因在于文档没有对异常处理例程进行解释或解释得很蹩脚。任何熟练使用现代操作系统的人都遇到过未处理的异常导致的错误和问题。这些错误背后的程序员通常面临一场艰难的战役,需要不断了解库的复杂性:什么异常将被引发,它们发生的原因和时间,如何处理它们,等等。

程序员新手很快将发现,理解库中异常处理像学习语言本身一样困难,现代库中包含的例程和模式可能像C++语法细节一样陌生而困难。要开发出优秀的软件,必须花时间了解库和类中的复杂内容,就像必须花时间学习C++本身一样。通过库文档和源代码了解到的异常和错误处理细节将使程序员和他的软件受益。

猜你喜欢

转载自blog.csdn.net/weixin_43717839/article/details/131362718