P38-第15章友元、异常和其他-03异常详细介绍

1. 异常

程序有时会遇到运行阶段错误,导致程序无法正常地运行下去。例如,程序可能试图打开一个不可用的文件,请求过多的内存,或者遭遇不能容忍的值。通常,程序员都会试图预防这种意外情况。
C++异常为处理这种情况提供了一种功能强大而灵活的工具。异常是相对较新的C+功能,有些老式编译器可能没有实现。另外,有些编译器默认关闭这种特性,您可能需要使用编译器选项来启用它。

2. 调用 abort()

对于异常这种问题,处理方式之一是, 调用abort()函数。
abort()函数的原型位于头文件 cstdlib(或 stdlib.h)中,其典型实现是向标准错误流(即cerr使用的错误流)发送消息 abnormal program termination(程序异常终止),然后终止程序。

它还返回一个随实现而异的值,告诉操作系统(如果程序是由另一个程序调用的,则告诉父进程),处理失败。abort()是否刷新文件缓冲区(用于存储读写到文件中的数据的内存区域)取决于实现。如果愿意,也可以使用exit(),该函数刷新文件缓冲区,但不显示消息。

程序清单15.7是一个使用 abort()的小程序。

1. 一个除数不能为 0的例子

程序清单15.7 error1.cpp

//error1.cpp -- using the abort() function
#include <iostream>
#include <cstdlib>
int MyDiv(int a, int b);

int main()
{
    
    
    int x, y, z;

    std::cout << "Enter two numbers: ";
    while (std::cin >> x >> y)
    {
    
    
        z = MyDiv(x,y);
        std::cout << x << " / " << y
            << " is " << z << std::endl;
        std::cout << "Enter next set of numbers <q to quit>: ";
    }
    std::cout << "Bye!\n";
    return 0;
}

int MyDiv(int a, int b)
{
    
    
    if (b == 0)
    {
    
    
        std::cout << "untenable arguments to MyDiv()\n";
        std::abort();
    }
    return (a / b); 
}

程序的运行结果:

book@book-desktop:~/meng-yue/c++/friend_abnormal/03$ ./error1
Enter two numbers: 1 3
1 / 3 is 0
Enter next set of numbers <q to quit>: 4 0
untenable arguments to MyDiv()
Aborted
book@book-desktop:~/meng-yue/c++/friend_abnormal/03$

3. 异常机制

下面介绍如何使用异常机制来处理错误。C++异常是对程序运行过程中发生的异常情况(例如被0除)的一种响应。

异常提供了将控制权从程序的一个部分传递到另一部分的途径。对异常的处理有3个组成部分:

  • 引发异常;
  • 使用处理程序捕获异常;
  • 使用try块。
    程序在出现问题时将引发异常。例如,可以修改程序清单15.7中的 MyDiv(),使之引发异常,而不是调用 abort()函数。

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

程序使用异常处理程序( exception handler)来捕获异常,异常处理程序位于要处理问题的程序中。 catch关键字表示捕获异常。

处理程序以关键字 catch开头,随后是位于括号中的类型声明,它指出了异常处理程序要响应的异常类型;然后是一个用花括号括起的代码块,指出要采取的措施。

catch关键字和异常类型用作标签,指出当异常被引发时,程序应跳到这个位置执行。

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

要了解这3个元素是如何协同工作的,最简单的方法是看一个简短的例子,如程序清单15.9所示。

程序清单15.9 error2.cpp

//error1.cpp -- using the abort() function
#include <iostream>
#include <cstdlib>
int MyDiv(int a, int b);

int main()
{
    
    
    int x, y, z;

    std::cout << "Enter two numbers: ";
    while (std::cin >> x >> y)
    {
    
    
    	try {
    
     //try块的开始
			z = MyDiv(x,y);
		}    // try块的结束
		catch (const char *s) //异常处理程序的开始
		{
    
    
			std::cout << s << std::endl;
			std::cout << "Enter a new pair of numbers: ";
			continue;
		}
        std::cout << x << " / " << y
            << " is " << z << std::endl;
        std::cout << "Enter next set of numbers <q to quit>: ";
    }
    std::cout << "Bye!\n";
    return 0;
}

int MyDiv(int a, int b)
{
    
    
    if (b == 0)
    {
    
    
    	throw "b == 0 is not allowed!";
    }
    return (a / b); 
}

程序运行的结果如下:

book@book-desktop:~/meng-yue/c++/friend_abnormal/03$ ./error2
Enter two numbers: 3 4
3 / 4 is 0
Enter next set of numbers <q to quit>: 4 0
b == 0 is not allowed!
Enter a new pair of numbers: q
Bye!
book@book-desktop:~/meng-yue/c++/friend_abnormal/03$

程序说明
在程序清单15.9中,try块与下面类似:

    	try {
    
     //try块的开始
			z = MyDiv(x,y);
		}    // try块的结束

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

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

    if (b == 0)
    {
    
    
    	throw "b == 0 is not allowed!";
    }

其中被引发的异常是字符串"b == 0 is not allowed!"。异常类型可以是字符串(就像这个例子中那样)或其他C++类型;通常为类类型,本章后面的示例将说明这一点。

执行 throw语句类似于执行返回语句,因为它也将终止函数的执行;但 throw不是将控制权返回给调用程序,而是导致程序沿函数调用序列后退,直到找到包含try块的函数。

在程序清单15.9中,该函数是调用函数。稍后将有一个沿函数调用序列后退多步的例子。另外,在这个例子中, throw将程序控制权返回给main()。程序将在main()中寻找与引发的异常类型匹配的异常处理程序(位于try块的后面)。

处理程序(或 catch块)与下面类似:

		catch (const char *s) //异常处理程序的开始
		{
    
    
			std::cout << s << std::endl;
			std::cout << "Enter a new pair of numbers: ";
			continue;
		}

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

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

因此处理值3和4时,程序清单15.9中程序执行报告结果的输出语句。
接下来看将4和9传递给 MyDiv()函数后发生的情况。if语句导致 MyDiv()引发异常。这将终止MyDiv()的执行。

程序向后搜索时发现, MyDiv()函数是从main()中的try块中调用的,因此程序查找与异常类型匹配的 catch块。

程序中唯一的一个 catch块的参数为char*,因此它与引发异常匹配。程序将字符串 b == 0 is not allowed! 赋给变量s,然后执行处理程序中的代码。处理程序首先打印s——捕获的异常,然后打印要求用户输入新数据的指示,最后执行 continue语句,命令程序跳过 while循环的剩余部分,跳到起始位置。

continue使程序跳到循环的起始处,这表明处理程序语句是循环的一部分,而 catch行是指引程序流程的标签

4. 将对象用作异常类型

通常,引发异常的函数将传递一个对象。这样做的重要优点之一是,可以使用不同的异常类型来区分不同的函数在不同情况下引发的异常。

另外,对象可以携带信息,程序员可以根据这些信息来确定引发异常的原因。同时, catch块可以根据这些信息来决定采取什么样的措施。例如,下面是针对函数 MyDiv()引发的异常而提供的一种设计:

class bad_MyDiv
{
    
    
private:
    double v1;
    double v2;
public:
    bad_MyDiv(double a = 0, double b = 0) : v1(a), v2(b){
    
    }
    void mesg();
};

可以将一个 bad_MyDiV对象初始化为传递给函数 MyDiV()的值,而方法mesg()可用于报告问题(包括传递给函数 MyDiV()的值)。

函数 MyDiV()可以使用下面这样的代码:

if (b == 0)
	throw bad_MyDiv(a, b);

上述代码调用构造函数 bad_MyDiV(),以初始化对象,使其存储参数值。

程序清单15.10和15.11添加了另一个异常类 bad_gmean以及另一个名为 gmean()的函数,该函数引发bad_gmean异常。

函数 gmean()计算两个数的几何平均值,即乘积的平方根。这个函数要求两个参数都不为负,如果参数为负,它将引发异常。

程序清单15.10是一个头文件,其中包含异常类的定义:而程序清单15.11是一个示例程序,它使用了该头文件。注意,try块的后面跟着两个 catch块:

try {
    
     // start of try block

} 	// end of try block
catch (bad_MyDiV & bg) // start of catch block
{
    
    

}
catch (bad_gmean &hg)
{
    
    

)/ end of catch block

如果函数 MyDiV()引发 bad_MyDiV异常,第一个 catch块将捕获该异常;如果 gmean()引发 bad_gmean异常,异常将逃过第一个 catch块,被第二个 catch块捕获。
程序清单15.10 exc_mean.h

// exc_mean.h  -- exception classes for MyDiv(), gmean()
#include <iostream>

class bad_MyDiv
{
    
    
private:
    double v1;
    double v2;
public:
    bad_MyDiv(double a = 0, double b = 0) : v1(a), v2(b){
    
    }
    void mesg();
};

inline void bad_MyDiv::mesg()
{
    
       
    std::cout << "MyDiv(" << v1 << ", " << v2 <<"): "
              << "invalid arguments: b = 0\n";
}

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";
}

程序清单15.11 error4.cpp

//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 MyDiv(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 = MyDiv(x,y);
            cout << x << " / " << y
                << " is " << z << endl;
            cout << "Geometric mean of " << x << " and " << y
                << " is " << gmean(x,y) << endl;
            cout << "Enter next set of numbers <q to quit>: ";
        }// end of try block
        catch (bad_MyDiv & 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 MyDiv(double a, double b)
{
    
    
    if (0 == b)
        throw bad_MyDiv(a,b);
    return (a / b);
}

double gmean(double a, double b)
{
    
    
    if (a < 0 || b < 0)
        throw bad_gmean(a,b);
    return std::sqrt(a * b); 
}

程序的运行结果:

book@book-desktop:~/meng-yue/c++/friend_abnormal/03$ g++ -o error4 error4.cpp
book@book-desktop:~/meng-yue/c++/friend_abnormal/03$ ./error4
Enter two numbers: 4 2
4 / 2 is 2
Geometric mean of 4 and 2 is 2.82843
Enter next set of numbers <q to quit>: 4 0
MyDiv(4, 0): invalid arguments: b = 0
Try again.

4 -4
4 / -4 is -1
gmean() arguments should be >= 0
Values used: 4, -4
Sorry, you don't get to play any more.
Bye!
book@book-desktop:~/meng-yue/c++/friend_abnormal/03$

首先, bad_MyDiv异常处理程序使用了一条 continue语句,而bad_gmean异常处理程序使用了一条 break语句。

因此,如果用户给函数 MyDiv()提供的参数不正确,将导致程序跳过循环中余下的代码,进入下次循环;而用户给函数 gmean()提供的参数不正确时将结朿循环。

这演示了程序如何确定引发的异常(根据异常类型)并据此采取相应的措施。其次,异常类 bad_gmean和 bad_MyDiv使用的技术不同,具体地说, bad_gmean使用的是公有数据和一个公有方法,该方法返回一个C-风格字符串

5. 异常规范和C++11

有时候,一种理念看似有前途,但实际的使用效果并不好。一个这样的例子是异常规范( exception specification),这是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++确实支持一种特殊的异常规范:您可使用新增的关键字 noexcept指出函数不会引发异常:

double marm() noexcept; //marm()doesnt throw an exception

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

通过使用这个关键字,编写函数的程序员相当于做出了承诺。
还有运算符 noexcept(),它判断其操作数是否会引发异常,详情请参阅附录E。

6.栈解退

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

这涉及到栈解退( unwinding the stack),下面进行介绍。

首先来看一看C++通常是如何处理函数调用和返回的。C++通常通过将信息放在栈(参见第9章)中来理函数调用。

具体地说,程序将调用函数的指令的地址(返回地址)放到栈中。当被调用的函数执行完毕后,程序将使用该地址来确定从哪里开始继续执行。

另外,函数调用将函数参数放到栈中。**在栈中,这些函数参数被视为自动变量。如果被调用的函数创建了新的自动变量,则这些变量也将被添加到栈中。**如果被调用的函数调用了另一个函数,则后者的信息将被添加到梭中,依此类推。

当函数结束时,程序流程将跳到该函数被调用时存储的地址处,同时栈顶的元素被释放。因此,函数通常都返回到调用它的函数,依此类推,同时每个函数都在结束时释放其自动变量。如果自动变量是类对象,则类的析构函数(如果有的话)将被调用。

现在假设函数由于出现异常(而不是由于返回)而终止,则程序也将释放栈中的内存,但不会在释放栈的第一个返回地址后停止,而是继续释放栈,直到找到一个位于 try 块(参见图15.3)中的返回地址。
在这里插入图片描述

随后,控制权将转到块尾的异常处理程序,而不是函数调用后面的第一条语句。这个过程被称为栈解退。

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

如果没有栈解退这种特性,则引发异常后,对于中间函数调用放在栈中的自动类对象,其析构函数将不会被调用。

程序清单15.12是一个栈解退的示例。其中,main()调用了 means((),而 means()又调用了 hmean()和 gmean()。

函数 means()计算算术平均数、调和平均数和几何平均数。main()和 means()都创建demo类型的对象(demo是一个喋喋不休的类,指出什么时候构造函数和析构函数被调用),以便您知道发生异常这些对象将被如何处理。

函数main()中的try块能够捕获 bad_hmean和 bad_gmean异常,而函数 means()中的try块只能捕获 bad_hmean异常。 catch块的代码如下:

catch (bad_hmean &bg) //start of catch block
{
    
    
	bg.mesg();
	std::cout << "Caught in means()\n";
	throw; //rethrows the exception
}

上述代码显示消息后,重新引发异常,这将向上把异常发送给main()函数。
一般而言,重新引发的异常将由下一个捕获这种异常的try- catch块组合进行处理,如果没有找到这样的处理程序,默认情况下程序将异常终止。

程序清单15.12 error5.cpp

//error5.cpp -- unwinding the stack
#include <iostream>
#include <cmath> // or math.h, unix users may need -lm flag
#include <string>
#include "exc_mean1.h"

class demo
{
    
    
private:
    std::string word;
public:
    demo (const std::string & str)
    {
    
    
        
        word = str;
        std::cout << "demo " << word << " created\n";
    }
    ~demo()
    {
    
    
        std::cout << "demo " << word << " destroyed\n";
    }
    void show() const
    {
    
    
        std::cout << "demo " << word << " lives!\n";
    }
}; 

// function prototypes
double hmean(double a, double b);
double gmean(double a, double b);
double means(double a, double b);

int main()
{
    
    
    using std::cout;
    using std::cin;
    using std::endl;
    
    double x, y, z;
	{
    
    
        demo d1("found in block in main()");
        cout << "Enter two numbers: ";
        while (cin >> x >> y)
        {
    
    
               try {
    
                      // start of try block
                   z = means(x,y);
                   cout << "The mean mean of " << x << " and " << y
                           << " is " << z << endl;
                   cout << "Enter next pair: ";
               } // end of try block
               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
        }
        d1.show();
    }
    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); 
}

double means(double a, double b)
{
    
    
    double am, hm, gm;
    demo d2("found in means()");
    am = (a + b) / 2.0;    // arithmetic mean
    try 
    {
    
    
        hm = hmean(a,b);
        gm = gmean(a,b);
    }
    catch (bad_hmean & bg) // start of catch block
    {
    
    
        bg.mesg();
        std::cout << "Caught in means()\n";
        throw;             // rethrows the exception 
    }          
    d2.show();
    return (am + hm + gm) / 3.0;
}

exc_mean1.h

// 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";
}

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";
}

程序的运行情况:

book@book-desktop:~/meng-yue/c++/friend_abnormal/03$ ./error5
demo found in block in main() created
Enter two numbers: 6 12
demo found in means() created
demo found in means() lives!
demo found in means() destroyed
The mean mean of 6 and 12 is 8.49509

Enter next pair: 6 -6
demo found in means() created
hmean(6, -6): invalid arguments: a = -b
Caught in means()
demo found in means() destroyed
hmean(6, -6): invalid arguments: a = -b
Try again.

6 -8
demo found in means() created
demo found in means() destroyed
gmean() arguments should be >= 0
Values used: 6, -8
Sorry, you don't get to play any more.
demo found in block in main() lives!
demo found in block in main() destroyed
Bye!
book@book-desktop:~/meng-yue/c++/friend_abnormal/03$

程序说明
来看看该程序的运行过程。
首先,正如demo类的构造函数指出的,在main()函数中创建了一个demo对象。接下来,调用了函数 means(),它创建了另一个demo对象。

函数 means()使用6和2来调用函数 hmean()和 gmean(),它们将结果返回给 means(),后者计算一个结果并将其返回。
返回结果前, means()调用了d2.show();返回结果后,函数 means()执行完毕,因此自动为d2调用析构函数:

demo found in means() lives!
demo found in means() destroyed

接下来的输入循环将值6和-6发送给函数 means(),然后 means(()创建一个新的demo对象,并将值传递给hmean()。函数 hmean()引发 bad_hmean异常,该异常被 means()中的 catch块捕获,下面的输出指出了这一点:

hmean(6, -6): invalid arguments: a = -b
Caught in means()

该 catch块中的 throw语句导致函数 means()终止执行,并将异常传递给main()函数。语句d2.show()没有被执行表明 means()函数被提前终止。但需要指出的是,还是为d2调用了析构函数:

demo found in means() destroyed

这演示了异常极其重要的一点:程序进行栈解退以回到能够捕获异常的地方时,将释放栈中的自动存储型变量。如果变量是类对象,将为该对象调用析构函数。

与此同时,重新引发的异常被传递给main(),在该函数中,合适的 catch块将捕获它并对其进行处理:

hmean(6, -6): invalid arguments: a = -b
Try again.

接下来开始了第三次输入循环:6和-8被发送给函数 means()。同样, means()创建一个新的demo对象,然后将6和-8传递给 hmean(),后者在处理它们时没有出现问题。然而, means()将6和-8传递给
gmean(),后者引发了 bad_gmean异常。由于 means()不能捕获 bad_gmean异常,因此异常被传递给main(),同时不再执行 means()中的其他代码。同样,当程序进行栈解退时,将释放局部的动态变量,因此为d2调用了析构函数:

demo found in means() destroyed

最后,main()中的 bad_gmean异常处理程序捕获了该异常,循环结束:

gmean() arguments should be >= 0
Values used: 6, -8
Sorry, you don't get to play any more.

然后程序正常终止:显示一些消息并自动为d1调用析构函数。如果 catch块使用的是 exit(EXIT_FAILURE)而不是 break,则程序将立刻终止,用户将看不到下述消息:

demo found in main() lives!
Bye

但仍能够看到如下消息:

demo found in main() destroyed

同样,异常机制将负责释放栈中的自动变量。

7. 其他异常特性

虽然 throw- catch机制类似于函数参数和函数返回机制,但还是有些不同之处。
其中之一是函数fun()中的返回语句将控制权返回到调用fun()的函数,但throw语句将控制权向上返回到第一个这样的函数:包含能够捕获相应异常的 try-catch组合。

例如,在程序清单15.12中,当函数 hmeans()引发异常时,控制权将传递给函数 means();然而,当 gmean()引发异常时,控制权将向上传递main()。

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

class problem {
    
    };

void super() throw (problem)
{
    
    
	if (oh_no) {
    
    
		problem oops; // construct object
		throw oops;// throw it
	}
try {
    
    
	super();
}

catch( problem & p)
{
    
    
	//statements
}

p将指向oops的副本而不是oops本身。这是件好事,因为函数 super()执行完毕后,oops将不复存在。

顺便说一句,将引发异常和创建对象组合在一起将更简单:

throw problem(); // construct and throw default problem object

您可能会问,既然 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)
{
    
    
}
catch(bad_2 &be)
{
    
    
}
catch(bad_1 &be)
{
    
    
}

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

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

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

catch(...) {
    
    // statements } // catches any type exception

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

try {
    
    
	duper();
}
catch(bad_3 &be)
{
    
    
}
catch(bad_2 &be)
{
    
    
}
catch(bad_1 &be)
{
    
    
}
catch(...) //catch whatever is left
{
    
    
}

可以创建捕获对象而不是引用的处理程序。在 catch语句中使用基类对象时,将捕获所有的派生类象,但派生特性将被剥去,因此将使用虚方法的基类版本。

8. exception类

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. stdexcept 异常类

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

class logic_error : public exception {
    
    
public:
	explicit logic_error(const string & what_arg);
};

class domain_error : public logic_error {
    
    
public:
	explicit domain_error(const string & what_arg);
}

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

下面是一个例子,证明了上面说的是对的。
excep.cpp

#include <iostream>
#include <string>
#include <exception>
#include <stdexcept>
using std::cout;
using std::cin;
using std::endl;
using std::string;
using std::logic_error;

using namespace std;

//class logic_error : public exception {
    
    
//public:
//	explicit logic_error(const string & what_arg) {}
//};
int main()
{
    
    
	logic_error e("error!");
	
	cout << e.what() << endl; 
	return 0;
}

运行结果:

book@book-desktop:~/meng-yue/c++/friend_abnormal/03$ ./excep
error!

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

  • domain_error
  • invalid_argument
  • length_error
  • out_of_bounds。

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

数学函数有定义域( domain)和值域( range)。定义域由参数的可能取值组成,值域由函数可能的返回值组成。例如,正弦函数的定义域为负无穷大到正无穷大,因为任何实数都有正弦值;但正弦函数的值域为-1到+1,因为它们分别是最大和最小正弦值。另一方面,反正弦函数的定义域为-1到+1,值域为-π到+π。

如果您编写一个函数,该函数将一个参数传递给函数std::sin(),则可以让该函数在参数不在定义域-1到+1之间时引发 domain_error异常。

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

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

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

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

  • range_error;
  • overflow_error;
  • underflow_error;

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

下溢( underflow)错误在浮点数计算中。一般而言,存在浮点类型可以表示的最小非零值,计算结果比这个值还小时将导致下溢错误。

整型和浮点型都可能发生上溢错误,当计算结果超过了某种类型能够表示的最大数量级时,将发生上溢错误。计算结果可能不再函数允许的范围之内,但没有发生上溢或下溢错误,在这种情况下,可以使用 range_error异常。

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

例如,下面的代码首先单独捕获 out_of_bounds异常,然后统一捕获其他 logic_error系列异常,最后统一捕获 exception异常、runtime_error系列异常以及其他从 exception派生而来的异常:

try {
    
    

}
catch(out_of_bonds &oe) //catch out_of_bounds error
{
    
    
}
catch(logic_error &oe) //catch remaining logic_error family
{
    
    
}
catch(exception &oe) //catch runtime_error, exception objects
{
    
    
}

2. bad_alloc异常和new

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

程序清单15.13演示了最新的方法。捕获到异常后,程序将显示继承的 what()方法返回的消息(该消息息随实现而异),然后终止

程序清单15.13 newexcp.cpp

// 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; 
}

程序的运行结果:

book@book-desktop:~/meng-yue/c++/friend_abnormal/03$ g++ -o newexcp newexcp.cpp
book@book-desktop:~/meng-yue/c++/friend_abnormal/03$ ./newexcp
Trying to get a big block of memory:
Caught the exception!
std::bad_alloc
book@book-desktop:~/meng-yue/c++/friend_abnormal/03$

在这里,方法what()返回字符串 std::bad_alloc

如果程序在您的系统上运行时没有出现内存分配问题,可尝试提高请求分配的内存量。

3. 空指针和new

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

int *pi = new(std::nothrow) int;
int *pa = new(std::nowthrow) int [500];

使用这种new,可将程序清单15.13的核心代码改为如下所示:

Big *pb;
pb = new (std::nothrow) Big [10000] //1, 600,000,000 bytes
if (pb == 0)
{
    
    
	cout << "Could not allocate memory. Bye.\n";
	exit(EXIT_FAILURE);
}

9. 异常、类和继承

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

程序清单15.14带领我们开始了上述一些可能性的探索之旅。这个头文件声明了一个 Sales类,它用于存储一个年份以及一个包含12个月的销售数据的数组。 LabeledSales类是从 Sales派生而来的,新增了一个用于存储数据标签的成员。

程序清单15.14 sales.h

// sales.h  -- exceptions and inheritance
#include <stdexcept>
#include <string>

class Sales
{
    
    
public:
    enum {
    
    MONTHS = 12};   // could be a static const
    class bad_index : public std::logic_error
    {
    
    
    private:
        int bi;  // bad_index value
    public:
        explicit bad_index(int ix,
            const std::string & s = "Index error in Sales object\n");
        int bi_val() const {
    
    return bi;}
        virtual ~bad_index() throw() {
    
    }
    };
    explicit Sales(int yy = 0);
    Sales(int yy, const double * gr, int n);
    virtual ~Sales() {
    
     }
    int Year() const {
    
     return year; }
    virtual double operator[](int i) const;
    virtual double & operator[](int i);
private:
    double gross[MONTHS];
    int year;
};

class LabeledSales : public Sales
{
    
    
  public:
    class nbad_index : public Sales::bad_index
    {
    
    
    private:
        std::string lbl;
    public:
        nbad_index(const std::string & lb, int ix,
           const std::string & s = "Index error in LabeledSales object\n");
        const std::string & label_val() const {
    
    return lbl;}
        virtual ~nbad_index() throw() {
    
    }
     };
    explicit LabeledSales(const std::string & lb = "none", int yy = 0);
    LabeledSales(const std::string & lb, int yy, const double * gr, int n);
    virtual ~LabeledSales() {
    
     }
    const std::string & Label() const {
    
    return label;}
    virtual double operator[](int i) const;
    virtual double & operator[](int i);
private:
    std::string label;
};

来看一下程序清单15.14的几个细节。

这里注意到 operator[]() 具有两个声明,是不是不一样的重载,意义是什么?

    double operator[](int i) const;
    double & operator[](int i);

excep1.cpp

#include <iostream>
#include <string>
#include <exception>
#include <stdexcept>
using std::cout;
using std::cin;
using std::endl;
using std::string;
using std::logic_error;

class Learn
{
    
    
private:

public:
	double operator[](int i) const
	{
    
    
		cout << "operator[](int i) const" << endl;
	}
	double & operator[](int i)
	{
    
    
		double *pd = new double;
		cout << "operator[](int i)" << endl;
		return *pd;
	}
};

int main()
{
    
    

	Learn l1;
	const Learn l2 = l1;
	l1[0];
	l2[0];
	return 0;
}

上面的这个程序可以验证const成员函数和普通成员函数,即便拥有相同的特征标,也是能够同时存在的。
而且const 对象最开始一定要初始化,要不然会编译报错
不初始化会报下面的这样的错误。

book@book-desktop:~/meng-yue/c++/friend_abnormal/03$ g++ -o excep1 excep1.cpp
excep1.cpp: In function ‘int main():
excep1.cpp:32: error: uninitialized const ‘l2’
book@book-desktop:~/meng-yue/c++/friend_abnormal/03$

程序运行的结果,表明const成员函数和普通成员函数,即便拥有相同的特征标,也是能够同时存在的。而且const类 会和const 成员函数匹配,而且不提供const成员函数的话,会报错,因为const类不能调用非const成员函数。

book@book-desktop:~/meng-yue/c++/friend_abnormal/03$ ./excep1
operator[](int i)
operator[](int i) const
book@book-desktop:~/meng-yue/c++/friend_abnormal/03$

首先,符号常量 MONTHS位于 Sales类的保护部分,这使得派生类(如 LabeledSales)能够使用这个值。

接下来, bad_index被嵌套在 Sales类的公有部分中,这使得客户类的cath块可以使用这个类作为类型。注意,在外部使用这个类型时,需要使用Sales::bad_index来标识。这个类是从 logic_error类派生而来的,能够存储和报告数组索引的超界值(out-of- bounds value)。

nbad_index类被嵌套到 LabeledSales的公有部分,这使得客户类可以通过 LabeledSales:::nbad_index来使用它。它是从 bad_index类派生而来的,新增了存储和报告 LabeledSales对象的标签的功能。

由于 bad_index是从 logic_error派生而来的,因此 nbad_index归根结底也是从 logic_error派生而来的.

这两个类都有重载的 operator[]()方法,这些方法设计用于访问存储在对象中的数组元素,并在索引超界时引发异常bad_index和 nbad_index类都使用了异常规范 throw(),这是因为它们都归根结底是从基类 exception派生而来的,而 exception的虚构造函数使用了异常规范 throw()。这是C++98的一项功能,在C++1中,exception的构造函数没有使用异常规范。

程序清单15.15是程序清单中没有声明为内联的方法的实现。注意,对于被嵌套类的方法,需要使用多个作用域解析运算符。另外,如果数组索引超界,函数 operator[]()将引发异常

程序清单15.15 sales.cpp

// sales.cpp -- Sales implementation
#include "sales.h"
using std::string;

Sales::bad_index::bad_index(int ix, const string & s )
    : std::logic_error(s), bi(ix)
{
    
    
}

Sales::Sales(int yy)
{
    
    
    year = yy;
    for (int i = 0; i < MONTHS; ++i)
        gross[i] = 0;
}

Sales::Sales(int yy, const double * gr, int n)
{
    
    
    year = yy;
    int lim = (n < MONTHS)? n : MONTHS;
    int i;
    for (i = 0; i < lim; ++i)
        gross[i] = gr[i];
    // for i > n and i < MONTHS
    for ( ; i < MONTHS; ++i)
        gross[i] = 0;
}

double Sales::operator[](int i) const
{
    
    
    if(i < 0 || i >= MONTHS)
        throw bad_index(i);
    return gross[i];
}

double & Sales::operator[](int i)
{
    
    
    if(i < 0 || i >= MONTHS)
        throw bad_index(i);
    return gross[i];
}

LabeledSales::nbad_index::nbad_index(const string & lb, int ix,
           const string & s ) : Sales::bad_index(ix, s)
{
    
    
    lbl = lb;
}

LabeledSales::LabeledSales(const string & lb, int yy)
         : Sales(yy)
{
    
    
    label = lb;
}

LabeledSales::LabeledSales(const string & lb, int yy, const double * gr, int n)
        : Sales(yy, gr, n)
{
    
    
    label = lb;
}

double LabeledSales::operator[](int i) const
{
    
        if(i < 0 || i >= MONTHS)
        throw nbad_index(Label(), i);
    return Sales::operator[](i);
}

double & LabeledSales::operator[](int i)
{
    
    
    if(i < 0 || i >= MONTHS)
        throw nbad_index(Label(), i);
    return Sales::operator[](i);
}

程序清单15.16在一个程序中使用了这些类: 首先试图超越 LabeledSales对象 sales2中数组的末尾, 然后试图超越 Sales对象 sales1中数组的末尾。这些尝试是在两个try块中进行的,让您能够检测每种异常。

程序清单15.16 use_sales.cpp

// use_sales.cpp  -- nested exceptions
#include <iostream>
#include "sales.h"

int main()
{
    
    
    using std::cout;
    using std::cin;
    using std::endl;

    double vals1[12] =
    {
    
    
        1220, 1100, 1122, 2212, 1232, 2334,
        2884, 2393, 3302, 2922, 3002, 3544
    };

    double vals2[12] =
    {
    
    
        12, 11, 22, 21, 32, 34,
        28, 29, 33, 29, 32, 35
    };

    Sales sales1(2011, vals1, 12);
    LabeledSales sales2("Blogstar",2012, vals2, 12 );

    cout << "First try block:\n";
    try
    {
    
    
        int i;
        cout << "Year = " << sales1.Year() << endl;
        for (i = 0; i < 12; ++i)
        {
    
    

            cout << sales1[i] << ' ';
            if (i % 6 == 5)
                cout << endl;
        }
        cout << "Year = " << sales2.Year() << endl;
        cout << "Label = " << sales2.Label() << endl;
        for (i = 0; i <= 12; ++i)
        {
    
    

            cout << sales2[i] << ' ';
            if (i % 6 == 5)
                cout << endl;
        }
        cout << "End of try block 1.\n";
   }
   catch(LabeledSales::nbad_index & bad)
   {
    
    
        cout << bad.what();
        cout << "Company: " << bad.label_val() << endl;
        cout << "bad index: " << bad.bi_val() << endl;
   }
   catch(Sales::bad_index & bad)
   {
    
    
        cout << bad.what();
        cout << "bad index: " << bad.bi_val() << endl;
   }
   cout << "\nNext try block:\n";
   try
    {
    
    
        sales2[2] = 37.5;
        sales1[20] = 23345;
        cout << "End of try block 2.\n";
   }
   catch(LabeledSales::nbad_index & bad)
   {
    
    
        cout << bad.what();
        cout << "Company: " << bad.label_val() << endl;
        cout << "bad index: " << bad.bi_val() << endl;
   }
   catch(Sales::bad_index & bad)
   {
    
    
        cout << bad.what();
        cout << "bad index: " << bad.bi_val() << endl;
   }
   cout << "done\n";
   // std::cin.get();
   return 0;
}

程序的运行结果:

book@book-desktop:~/meng-yue/c++/friend_abnormal/03$ ./use_sales
First try block:
Year = 2011
1220 1100 1122 2212 1232 2334
2884 2393 3302 2922 3002 3544
Year = 2012
Label = Blogstar
12 11 22 21 32 34
28 29 33 29 32 35
Index error in LabeledSales object
Company: Blogstar
bad index: 12

Next try block:
Index error in Sales object
bad index: 20
done
book@book-desktop:~/meng-yue/c++/friend_abnormal/03$

上面的代码最开始是 当i = 12的时候,sales2 数组下标越界了。

for (i = 0; i <= 12; ++i)
{
    
    
    cout << sales2[i] << ' ';
    if (i % 6 == 5)
        cout << endl;
}

sales2[12] 和下面的函数原型匹配。

double & LabeledSales::operator[](int i)
{
    
    
    if(i < 0 || i >= MONTHS)
        throw nbad_index(Label(), i);
    return Sales::operator[](i);
}

当 i >= 12的时候,抛出一个异常,

throw nbad_index(Label(), i);

Label() 返回的是 sales2.label成员,是开始构造类实例的时候初始化的

LabeledSales sales2("Blogstar",2012, vals2, 12 );

与之匹配的是下面的这个构造函数

LabeledSales::LabeledSales(const string & lb, int yy, const double * gr, int n)
        : Sales(yy, gr, n)
{
    
    
    label = lb;
}

构造LabeledSales 类之前,先构造基类 Sales,并且 label 成员被赋值 为字符串 "Blogstar"

所以代码 throw nbad_index(Label(), i); 的what 成员函数 使用的默认参数 "Index error in LabeledSales object\n"

 nbad_index(const std::string & lb, int ix,
    const std::string & s = "Index error in LabeledSales object\n");

LabeledSales::nbad_index::nbad_index(const string & lb, int ix,
           const string & s ) : Sales::bad_index(ix, s)
{
    
    
    lbl = lb;
}

Sales::bad_index::bad_index(int ix, const string & s )
    : std::logic_error(s), bi(ix)
{
    
    
}

抛出来的异常被 下面的代码 catch到

   catch(LabeledSales::nbad_index & bad)
   {
    
    
        cout << bad.what();
        cout << "Company: " << bad.label_val() << endl;
        cout << "bad index: " << bad.bi_val() << endl;
   }

所有屏幕上面有下面的打印

Index error in LabeledSales object
Company: Blogstar
bad index: 12

代码接着往下执行,又遇到了 sales1 数组越界

        sales1[20] = 23345;

也会抛出一个异常

double & Sales::operator[](int i)
{
    
    
    if(i < 0 || i >= MONTHS)
        throw bad_index(i);
    return gross[i];
}

Sales::bad_index::bad_index(int ix, const string & s )
    : std::logic_error(s), bi(ix)
{
    
    
}

所以出现下面的打印

Index error in Sales object
bad index: 20

10. 异常何时会迷失方向

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

首先,如果它是在带异常规范的函数中引发的,则必须与规范列表中的某种异常匹配(在继承层次结构中,类类型与这个类及其派生类的对象匹配),否则称为外异常( unexpected exception)。

在默认情况下,这将导致程序异常终止(虽然C++11摒弃了异常规范, 但仍支持它,且有些现有的代码使用了它)。如果异常不是在函数中引发的(或者函数没有异常规范),则必须捕获它。如果没被捕获(在没有try块或没有匹配的catch块时,将出现这种情况),则异常被称为未获异常( uncaught exception)。

在默认情况下,这将导致程序异常终止。然而,可以修改程序对意外异常和未捕获异常的反应。下面来看如何修改,先从未捕获异常开始。

未捕获异常不会导致程序立刻异常终止。相反,程序将首先调用函数 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。首先,请包含头文件 exception。可以使用 using编译指令、适当的 using声明或std::限定符,使其声明可用。

#include <exception>
using namespace std;

然后,设计一个完成上述两种操作所需的函数,其原型如下

void myQuit ()
{
    
    
	cout << "Terminating due to uncaught exception\n";
	exit(5);
}

最后,在程序的开头,将终止操作指定为调用该函数

set terminate(myQuit);

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

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

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

则可以这样使用该函数

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

}

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

原则上,异常规范应包含函数调用的其他函数引发的异常。例如,如果Argh()调用了Duh()函数,而后者可能引发 retort对象异常,则Argh()和Duh()的异常规范中都应包含 retort。除非自己编写所有的函数,并且特别仔细,否则无法保证上述工作都已正确完成。

例如,可能使用的是老式商业库,而其中的函数没有异常规范。这表明应进一步探讨这样一点,即如果函数引发了其异常规范中没有的异常,情况将如何?

这也表明异常规范机制处理起来比较麻烦,这也是C++11将其摒奔的原因之一。

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

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;

然后,设计一个替代函数,将意外异常转换为 bad_exception异常,该函数的原型如下:

void myUnexpected()
{
    
    
	throw std::bad_exception(); //or just throw;
}

仅使用 throw,而不指定异常将导致重新引发原来的异常。然而,如果异常规范中包含了这种类型,则该异常将被 bad_exception对象所取代。

接下来在程序的开始位置,将意外异常操作指定为调用该函数:

set_unexpected(myUnexpected);

最后,将 bad_exception类型包括在异常规范中,并添加如下 catch块序列:

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

11. 有关异常的注意事项

从前面关于如何使用异常的讨论可知,应在设计程序时就加入异常处理功能,而不是以后再添加。
这样做有些缺点。例如,使用异常会增加程序代码,降低程序的运行速度。异常规范不适用于模板,因为模板函数引发的异常可能随特定的具体化而异。

异常和动态内存分配并非总能协同工作。
下面进一步讨论动态内存分配和异常。首先,请看下面的函数:

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章将讨论的智能指针模板之一

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

猜你喜欢

转载自blog.csdn.net/sgy1993/article/details/114097903
今日推荐