c++学习笔记——友元、异常和其他

友元

在前面的学习中将友元函数用于类的扩展接口中,但是类并非只能拥有友元,也可以将类作为友元。友元类的作用:友元类的所有方法都可以访问原始类的私有成员和保护成员,当然,也可以做更严格的限制,只将特定的成员函数指定为另一个类的友元。

注意:友元可以被授予从外部访问类的私有部分的权限,但是它们并不与面向对象的编程思想相悖;相反,它们提高了公有接口的灵活性。

友元类

下面将使用一个例子来介绍友元类的编写:TV(电视机类)和Remote(遥控器类),这两个类不是is-a关系(遥控器不是电视),所以不能用公有继承;这两个类不是has-a关系(遥控器并不是电视机的一部分),所以不能使用私有继承和保护继承。但是遥控器可以改变电视机的状态,这就表明应将Remote类作为TV的一个友元。

首先头文件:

// tv.h -- Tv and Remote classes
#ifndef TV_H_
#define TV_H_

class Tv
{
public:
    friend class Remote;   // Remote can access Tv private parts
    enum {Off, On};
    enum {MinVal,MaxVal = 20};
    enum {Antenna, Cable};
    enum {TV, DVD};

    Tv(int s = Off, int mc = 125) : state(s), volume(5),
        maxchannel(mc), channel(2), mode(Cable), input(TV) {}
    void onoff() {state = (state == On)? Off : On;}
    bool ison() const {return state == On;}
    bool volup();
    bool voldown();
    void chanup();
    void chandown();
    void set_mode() {mode = (mode == Antenna)? Cable : Antenna;}
    void set_input() {input = (input == TV)? DVD : TV;}
    void settings() const; // display all settings
private:
    int state;             // on or off
    int volume;            // assumed to be digitized
    int maxchannel;        // maximum number of channels
    int channel;           // current channel setting
    int mode;              // broadcast or cable
    int input;             // TV or DVD
};

class Remote
{
private:
    int mode;              // controls TV or DVD
public:
    Remote(int m = Tv::TV) : mode(m) {}
    bool volup(Tv & t) { return t.volup();}
    bool voldown(Tv & t) { return t.voldown();}
    void onoff(Tv & t) { t.onoff(); }
    void chanup(Tv & t) {t.chanup();}
    void chandown(Tv & t) {t.chandown();}
    void set_chan(Tv & t, int c) {t.channel = c;}
    void set_mode(Tv & t) {t.set_mode();}
    void set_input(Tv & t) {t.set_input();}
};
#endif

程序编写依据:遥控器的控制能力应该与电视机内置的控制面板功能一致,很多方法都可以通过使用TV方法来实现,另外,要遥控器还有随意选择频道的功能,不用逐次选择频道,另外,有两个选择调节声音的按钮等。 

程序解读:

(1)其中语句friend class Remote;//使得Remote成为友元类,友元类的声明位置可以位于私有部分、公有部分或者是保护部分,其位置无关紧要。另外需要注意的是这里的Remote提到了TV类,所以最简单的办法就是将Remote声明在TV类之后,当然也可以使用前向声明(稍后介绍)。

(2)大多数方法都是被定义成内联的(函数定义位于类声明中的函数都自动为内联函数。如果想让在类声明之外定义其成员函数为内联函数,只需要在类实现部分定义函数时使用inline限定符即可。)。除了构造函数之外,所有的Remote都将定义一个TV对象引用作为其参数,这表明遥控器只针对特定的电视机。

其次,具体的实现文件:

// tv.cpp -- methods for the Tv class (Remote methods are inline)
#include <iostream>
#include "tv.h"

bool Tv::volup()
{
    if (volume < MaxVal)
    {
        volume++;
        return true;
    }
    else
        return false;
}
bool Tv::voldown()
{
    if (volume > MinVal)
    {
        volume--;
        return true;
    }
    else
        return false;
}

void Tv::chanup()
{
    if (channel < maxchannel)
        channel++;
    else
        channel = 1;
}

void Tv::chandown()
{
    if (channel > 1)
        channel--;
    else
        channel = maxchannel;
}

void Tv::settings() const
{
    using std::cout;
    using std::endl;
    cout << "TV is " << (state == Off? "Off" : "On") << endl;
    if (state == On)
    {
        cout << "Volume setting = " << volume << endl;
        cout << "Channel setting = " << channel << endl;
        cout << "Mode = "
            << (mode == Antenna? "antenna" : "cable") << endl;
        cout << "Input = "
            << (input == TV? "TV" : "DVD") << endl;
    }
}

最后测试文件是:

//use_tv.cpp -- using the Tv and Remote classes
#include <iostream>
#include "tv.h"

int main()
{
    using std::cout;
    Tv s42;
    cout << "Initial settings for 42\" TV:\n";
    s42.settings();
    s42.onoff();
    s42.chanup();
    cout << "\nAdjusted settings for 42\" TV:\n";
    s42.settings();

    Remote grey;

    grey.set_chan(s42, 10);
    grey.volup(s42);
    grey.volup(s42);
    cout << "\n42\" settings after using remote:\n";
    s42.settings();

    Tv s58(Tv::On);
    s58.set_mode();
    grey.set_chan(s58, 28);
    cout << "\n58\" settings:\n";
    s58.settings();
    // std::cin.get();
    return 0;
}

输出为:

Initial settings for 42" TV:
TV is Off

Adjusted settings for 42" TV:
TV is On
Volume setting = 5
Channel setting = 3
Mode = cable
Input = TV

42" settings after using remote:
TV is On
Volume setting = 7
Channel setting = 10
Mode = cable
Input = TV

58" settings:
TV is On
Volume setting = 5
Channel setting = 28
Mode = antenna
Input = TV

友元成员函数

上面的程序中大部分Remote方法都是用TV类的公有接口实现的,这就意味着这些方法不是真的需要作为友元。其中唯一直接访问TV成员的Remote方法是Remote::set_chan(),set_chan()的方法定义是 void set_chan(Tv & t, int c) {t.channel = c;},所以它是唯一一个需要作为友元的方法。

的确可以选择让特定的成员成为另一个类的友元而不必让整个类成为友元,如下:

class TV
{
    friend void Remote :: set_chan(TV & t, int c)
    ……
};

使用这个方法会有一些问题:

(1)第一个问题:必须小心各种声明和定义的顺序

因为要想编译器能够处理这条语句,就必须将Remote类的定义放在TV的定义之前。但是Remote的方法提到了TV对象,就意味着TV定义必须放在Remote之前,所以其实这两种需求是矛盾的,要想避免这种循环依赖,可以使用前向声明(forward declaration)。为此,需要在Remote定义前面插入下语句class TV;

最终结果就如下:

class TV;
class Remote {……};
class TV {……};

注意不能是:

class Remote;
class TV {……};
class Remote {……};

原因是,在编译器看到TV类中声明了一个Remote类方法为友元函数之前就应该先看到Remote类的声明和set-chen方法的声明。 

第二个问题:Remote声明包含的内联函数,这些内联函数的函数体中会调用TV类的方法,所以TV类必须位于Remote之前,但是,正如所见,TV类的声明在Remote类之后,解决办法是使Remote声明中只包含方法声明,并将实际的定义放在TV类之后(这样的话Remote类中方法就不是内联函数,要想其任然是内联函数,可以在其原型前加inline)。

所以,第二次修改后的头文件如下:

class TV;                //前向声明
class Remote {……};        //只包含方法声明,不含具体定义
class TV {……};            

前向声明最终结果是编译器遇到Remote类中的方法原型(这些方法原型参数列表中有TV类的引用)可以知道TV是一个类。而当编译器读取Remote类方法的具体定义时,它已经读取到TV类的信息了。

改进后的头文件

// tvfm.h -- Tv and Remote classes using a friend member
#ifndef TVFM_H_
#define TVFM_H_

class Tv;                       // forward declaration

class Remote
{
public:
    enum State{Off, On};
    enum {MinVal,MaxVal = 20};
    enum {Antenna, Cable};
    enum {TV, DVD};
private:
    int mode;
public:
    Remote(int m = TV) : mode(m) {}
    bool volup(Tv & t);         // prototype only
    bool voldown(Tv & t);
    void onoff(Tv & t);
    void chanup(Tv & t);
    void chandown(Tv & t);
    void set_mode(Tv & t);
    void set_input(Tv & t);
    void set_chan(Tv & t, int c);
};

class Tv
{
public:
    friend void Remote::set_chan(Tv & t, int c);
    enum State{Off, On};
    enum {MinVal,MaxVal = 20};
    enum {Antenna, Cable};
    enum {TV, DVD};

    Tv(int s = Off, int mc = 125) : state(s), volume(5),
        maxchannel(mc), channel(2), mode(Cable), input(TV) {}
    void onoff() {state = (state == On)? Off : On;}
    bool ison() const {return state == On;}
    bool volup();
    bool voldown();
    void chanup();
    void chandown();
    void set_mode() {mode = (mode == Antenna)? Cable : Antenna;}
    void set_input() {input = (input == TV)? DVD : TV;}
    void settings() const;
private:
    int state;
    int volume;
    int maxchannel;
    int channel;
    int mode;
    int input;
};

// Remote methods as inline functions
inline bool Remote::volup(Tv & t) { return t.volup();}
inline bool Remote::voldown(Tv & t) { return t.voldown();}
inline void Remote::onoff(Tv & t) { t.onoff(); }
inline void Remote::chanup(Tv & t) {t.chanup();}
inline void Remote::chandown(Tv & t) {t.chandown();}
inline void Remote::set_mode(Tv & t) {t.set_mode();}
inline void Remote::set_input(Tv & t) {t.set_input();}
inline void Remote::set_chan(Tv & t, int c) {t.channel = c;} 
#endif

实现文件和主函数文件与上个实例一样,区别就在于这个只有一个Remote方法是TV类的友元。而上个版本中所有的Remote方法都是TV类的友元。图解如下:

   这里回顾下内联函数,内联函数的链接性是内部的,这就意味着函数定义必须在函数的文件中,本例子内联定义放在头文件中,所以使用函数的文件中包含头文件可确保将定义放在正确的地方。也可以将定义放在实现文件中,但是必须删除关键字inline,这样函数的链接性是外部的。

其他的友元关系

(互为友元)

还有其他友元和类的组合形式,比如说TV类和Remote类互为友元,也就是说一些Remote方法能够影响TV对象,一些TV方法也能影响Remote对象。这也可以通过让类成为对方的友元类实现。但是必须要注意的一点是:对于使用Remote对象的TV类方法,其原型可以在Remote类声明之前声明,但是必须在Remote类声明之后定义它。这样编译器就有足够的信息来编译它,方案如下:

 

共同的友元

需要使用另一种友元的情况是函数需要访问两个类的私有数据。它可以被设置成是一个类的成员函数,而是另一个类的友元。但是将函数作为两个类的友元更加合理。下面使用一个例子说明情况:假设有一个probe类和analyzer类,前者表明某种可编程的测量设备,后者表示某种可编程的分析设备。这两个类中都有内部时钟,且希望它们能够同步,则应该包含下述代码:

嵌套类

在c++中,可以将类声明放在另一个类中,在另一个类中声明的类被称为嵌套类,它通过提供新的类型作用域来避免名称混乱。包括类的成员函数可以创建和使用被嵌套类的对象;而仅当声明位于公有部分,才能在包含类的外面使用嵌套类,而且必须使用作用域解析运算符。

对类进行嵌套和包含并不同,包含意味着将类对象作为另一个类的成员,而对类进行嵌套不创建类成员,而是定义一种类型,该类型仅当在包含嵌套类声明的类中有效。

在类进行嵌套通常是为了帮助实现另一个类,在第十二章中对Queue类示例中就在类中嵌套了结构定义:

class Queue
{
private:
    struct Node {Item item; struct Node * next;};
    ……
}

结构是一种其成员在默认情况下为公有的类,所以Node实际上是一个嵌套类,但该定义它没有显式地构造函数,下面将对其进行改进。改进后就是提供一个适当的构造函数定义:

class Queue
{
    ……
    class Node
    {
    public:
        Item item;
        Node * next;
        Node(const Item & i): item(i), next(0) {}
    };
    ……
};

 该构造函数将节点item成员初始化为i,并将next指针设置成0,这是使用c++编写空值指针的方法之一(使用NULL时,必须包含定义一个NULL的头文件,在c++11中派,也可以使用nullptr)。由于使用Queue类创建的所有节点的next的初始值都被设置成空指针,所以这个类只需要这个构造函数。

这里的例子中是在类声明中定义了构造函数,假设想在方法文件中定义构造函数,将定义必须指出Node类是在Queue类中定义的。这是需要使用两次作用域解析运算符来完成:

Queue::Node::Node(const Item & i) : item(i), next(0) { }

嵌套类和访问权限

嵌套类要注意两种访问权限:第一:嵌套类的声明位置决定了嵌套类的作用域,它决定程序在那些位置可以创建这种类的对象。第二:和别的类一样,嵌套类的公有部分、保护部分和私有部分对类成员的访问控制

(1)作用域

如果一个类是在另一个类的私有部分声明的,那么只有后者知道它。上面例子中的被嵌套在Queue中的Node类就是这样的(虽然看起来Node是在私有之前定义的,但是别忘记类的默认访问权限是私有的),所以对于从Queue派生而来的类,Node也是不可见的,因为派生类不能直接访问基类的私有部分。

当嵌套类位于另一个类的保护部分,则对与后者是可见的,对于外部世界任然则是不可见的,但是派生类将知道嵌套类,并可以直接创建这种类型的对象。

当嵌套类位于另一个类公有部分,则允许后者、后者的派生类以及外部世界使用它,因为它是公有的。然而需要注意的是由于嵌套类的作用域为包含他的类,因此在外部世界使用它时,必须使用类限定符。

在类中是用枚举和嵌套结构的作用域相同,很多程序员都使用公有枚举来提供可供客户使用的类常数。下表总结了嵌套类、枚举和结构的作用域特征:

访问控制

对嵌套类的访问权的控制与常规类相同。在Queue类声明中声明Node类并不会赋予Queue类对Node类的访问权限,也没有赋予Node类任何对Queue类的访问特权。

Queue类对象只能显式地访问Node对象的公有部分,不能访问其私有部分,出于这个原因,在Node类的所有成员被声明为公开的,这样有悖于将数据成员声明为私有的这一惯例,但Node类是Queue类内部实现的一项特性,对于外部世界是不可见的,这是因为Node类在Queue类的私有部分声明的。所以Queue的方法可直接访问Node的成员,但是Queue类的客户不能直接这样做。

 异常

程序有时可能会遭遇运行阶段性错误,导致程序无法正常运行下去。程序可能会试图打开一个不可用的文件,请求过多的内存,或者遭遇不能容忍的值。这时可以使用c++异常来处理,在讨论异常之前,先来了解下一些其他可以用于处理该种情况的基本操作:

调用abort()

程序15.7

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

需要注意的点是:

(1)函数abort()位于cstdlib.h(或者是stdlib.h)中,其典型实现是向标准错误流发送消息abnormal program termination (程序异常终止),然后终止程序。它还可以返回一个随实现而异的值,告诉操作系统处理失败。abort()是否刷新文件缓冲区取决于实现(文件缓冲区用于存储读写到文件中的数据的内存区域)。也可以使用exit(),该函数刷新文件缓冲区,但是不显示消息。

输出为:

Enter two numbers: 6 3
Harmonic mean of 6 and 3 is 4
Enter next set of numbers <q to quit>: 10 -10
untenable arguments to hmean()

从输出来看,在human()中调用abort()函数将直接终止程序,而不是返回到主函数中(因为它并没有输出Bye!)。

返回错误码

除了使用abort()函数外,另一种更灵活的方法就是,使用函数的返回值来指出问题。实例如下:

//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;        //    DBL_MAX是double型的最大值
        return false;
    }
    else
    {
        *ans = 2.0 * a * b / (a + b);
        return true;
    }
}

跟上个程序相比,这个程序避免了错误输入导致的恶果,可以让用户继续输入。该程序使用指针参数或者引用参数来将值返回给调用程序,并使用函数的返回值来指出成功还是失败。其中函数hmean()函数的第三个参数可以使用指针或者是引用,这里使用的是指针(一般程序员都偏向使用指针,这样可以看出那个参数用于提供答案)。

运行结果如下:

Enter two numbers: 6 3
Harmonic mean of 6 and 3 is 4
Enter next set of numbers <q to quit>: 10 -10
One value should not be the negative of the other - try again.
Enter next set of numbers <q to quit>: 5 4
Harmonic mean of 5 and 4 is 4.44444
Enter next set of numbers <q to quit>: q
Bye!

异常机制

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

(1)引发异常:程序出现问题时就会引发异常,修改程序15.7中的hmean(),使之引发异常,而而不是调用abort()函数,就像下面这样:

double hmean(double a, double b)
{
    if (a == -b)
        throw "bad hmean() arguments: a = -b not allowed";
    return 2.0 * a * b / (a + b); 
}

其中关键字throw语句实际上是跳转,也就是命令程序跳到另一条语句,throw关键字表示引发异常,紧随其后的字符串指出了异常特征(这里是"bad hmean() arguments: a = -b not allowed"字符串类型) 。执行throw语句类似于执行返回语句,因为它也终止函数的执行,但是throw不是将控制权返回给调用程序,而是将程序沿函数调用序列后退,直到找到包含try块的函数。在该程序中是将程序控制权返回给main(),之后将在main()中寻找与引发的异常类型(这里是char *,与throw关键字后面的"bad hmean() arguments: a = -b not allowed"字符串匹配)匹配的异常处理程序或者catch块(位于try块的后面)。

(2)使用处理程序捕获异常;程序使用异常处理程序来捕获异常,异常处理程序处于要处理问题的程序中。catch关键字表示捕获异常。处理程序以关键字catch开头,随后是位于括号中的类型声明,它指出了异常处理程序要响应的异常类型;然后用一个花括号括起的代码块,指出要采取的措施,就像下面这样:

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

catch关键字和括号中的异常类型用作标签,指出当异常被引发时,程序应该跳到这个位置执行。异常处理程序也被称为catch块。 catch块有点类似于函数定义,但并不是函数定义。关键字catch表明这是一个处理程序,char *s表明该处理程序与字符串异常匹配。s与函数参数定义类似,因为匹配的引发被赋给s(这里是"bad hmean() arguments: a = -b not allowed"付给s)。当异常与该处理程序匹配时,程序将执行括号中的代码。

(3)使用try。try块标识可能会引发异常的代码块,它后面跟一个或者多个catch块,try块是由关键字try指示的,关键字try的后面是一个由花括号括起的代码块,表明需要注意这些代码引发的异常。执行完try块中的语句后,如果没有引发任何异常,就直接跳过catch块,直接执行处理程序后面的第一条语句。

为了了解异常机制,下面使用一个实例来进行介绍:

// 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 {                   // start of try block
            z = hmean(x, y);
        }                       // end of try block
        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);
}

输出为:

Enter two numbers: 3 6
Harmonic mean of 3 and 6 is 4
Enter next set of numbers <q to quit>: 10 -10
bad hmean() arguments: a = -b not allowed
Enter a new pair of numbers: 1 19
Harmonic mean of 1 and 19 is 1.9
Enter next set of numbers <q to quit>: q
Bye!

从输出来看,在human()中调用abort()函数将直接终止程序,而不是返回到主函数中(因为它并没有输出Bye!)。

总结:

使用一个引发异常的例子来介绍具体的过程:当输入10和-10传递给hmean()后,if语句导致hmean()引发异常,程序向后搜索时发现,hmean()函数是从main()中的try块中调用的,因此程序查找与异常类型匹配的catch块。程序中唯一的一个catch块的参数为char*,因此它与引发异常匹配。程序将字符串"bad hmean() arguments: a = -b not allowed"赋给变量s,然后执行处理程序中的代码。处理程序首次打印s也就是捕获的异常,然后打印要求用户输入新数据的提示,然后执行那个continue语句,命令程序跳过while循环的剩余部分,跳到起始位置。continue是程序跳到循环的起始处,这表明处理程序语句是循环的一部分,而catch行是指引程序流程的标签。

当程序没有try块或者没有匹配的处理程序时,在默认情况下程序将调用abort()函数,但可以修改这种行为。

将对象作为异常类型

一般来说,引发异常的函数将传递一个对象,这样做的重要优点之一是使用不同的异常类型来区分不同的函数在不同情况下引发的异常。除此之外,对象还可以携带信息,使得程序员可以根据这些信息来确定引发异常的原因。同时catch块可以根据这些信息来决定采取什么样的措施。

下面针对函数hmaen()引发的异常而提供的一种设计:

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

inline void bad_bmean::mesg()
{
    std::cout << "hmean(" << v1 << ", " << v2 << "): "
                <<"invalid agruments : a = -b\n";
}

可以将一个bad_hmean对象初始化为传递给函数hmean()的值,而方法mesg()可用于报告问题(包含传递给函数hmean()的值)。函数hmean()可以使用下面这样的代码:

if(a == -b)
    throw bad_hmean(a, b);

上面的第二行代码使用bad_hmean类构造函数初始化对象,使其存储参数值。

下面使用一个例子来介绍该种思路,计算两个数的几何平均值(也就是乘积的平方根)这两个参数都不能为负值,它将引发异常。

首先头文件:程序15.10

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

主函数文件:

//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;
            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_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异常处理程序使用了一条conntinue语句,而bad_hmean异常处理程序使用了一条break语句。所以造成的后果就是用户给函数hmean()提供的参数不正确,将导致程序跳过循环中余下的代码进入下一次循环;而当函数gmean()提供的参数不正确将结束循环

输出为:

Enter two numbers: 4 12
Harmonic mean of 4 and 12 is 6
Geometric mean of 4 and 12 is 6.9282
Enter next set of numbers <q to quit>: 5 -5
hmean(5, -5): invalid arguments: a = -b
Try again.
5 -2
Harmonic mean of 5 and -2 is -6.66667
gmean() arguments should be >= 0
Values used: 5, -2
Sorry, you don't get to play any more.
Bye!

栈退解

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

上面是程序正常调用和正常结束时栈的情况,下面假设函数由于出现异常而终止,程序也将释放栈中的内存,但是不会释放栈中第一个返回地址后停止,而是继续释放栈,直到遇到一个位于try块中的返回地址。随后,控制权将转到块尾的异常处理程序,而不是函数调用后面的第一条语句。这个过程被称为栈退解。引发机制的一个非常重要的特性是,和函数返回一样,对于栈中的自动类对象,类的析构函数将被调用。对比可知:函数返回仅仅处理该函数放在栈中的对象,但是throw语句则处理try块和throw之间整个函数调用序列放在栈中的对象。如果没有栈退解这种机制的话,引发异常后,对于中间函数调用放在栈中的自动类对象,析构函数则不会调用。

图解:

下面使用一个实例来介绍两种情况:

程序15.12

//error5.cpp -- unwinding the stack
#include <iostream>
#include <cmath> // or math.h, unix users may need -lm flag
#include <string>
#include "exc_mean.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;
}

上面的程序在main()调用了means(),而means()又调用了hmean()和gmean()。函数means()计算负责计算算术平均数、调和平均数和几何平均数。main()和means()都创建了demo类型的对象。函数main()中的try块能够捕获bad_main和badgmean异常,函数means()中的try块只能捕获bad_hmean异常。

输出为:

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!

输入6和12时,在main()函数中创建一个demo对象。接下来,调用了函数means(),它又创建了另一个demo对象。函数means使用6和12来调用函数hmean()和gheam(),他们将结果返回到means(),means()再计算一个结果并将其返回。means()调用了d2.show();返回结果后,函数means()就执行完毕了,所以自动为d2调用析构函数。

输入6和-6时:将值传递给函数means(),然后means()创建一个demo对象,并将值传递给hmean()。函数hmean()引发bad_mean()异常,该异常被means()中的catch块捕获,结果中输出的hmean(6, -6): invalid arguments: a = -b;Caught in means()可以表明引发的就是means()中的catch块捕获。

之后,在means()中的catch语句导致函数means()终止执行,并将异常传递给main()函数。这里输出就显示出没有执行d2.show()语句(也就是没有输出demo found in means() lives!)means()函数被提前终止。但是还是为d2调用了析构函数(也就输出了demo found in means() destroyed)。

这就显式了异常很重要的一点就是:程序栈解推以回到能够捕获异常的地方时,将释放栈中的自动存储变量。如果变量是类对象,将为该对象调用析构函数。

之后,重新引发的异常被传递给main(),在主函数中,又可以合适的catch可以捕获并处理它,也就是

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

当输入6和-8时:means()创建一个新的demo对象,将6和-8传递给hmean(),hmean()函数在处理它们是并无异常。但是means()将6和-8传递给gmean(),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,则程序将立刻终止,也就是不会执行语句d1.show();不会看到以下输出:

demo found in block in main() lives!
Bye!

但是还是会看到以下消息的,也就是释放类变量,同样的,异常机制将负责释放栈中的自动变量:

demo found in block in main() destroyed

其他异常特性

第一,虽然throw—catch机制有点类似于函数参数和函数返回机制,但是函数中的返回语句将控制权返回到调用函数的函数。

但是throw语句将控制权向上返回到第一个这样的函数:包含能够捕获相应异常的try—catch组合。例如,当函数hmeans()引发异常时,控制权将传递给函数means();然而,当gmean()引发异常时,控制权将向上传递给main()。

第二,引发异常时编译器总是创建一个临时拷贝,即便是异常规范和catch块中指定的是引用。例如下面的代码:

p指向的是oops的副本(throw抛出的oops的副本,不是oops对象本身),这是合理的,因为函数super()执行完毕后,oops将不复存在。虽然throw抛出的是对象的副本,但是这里还是要使用引用,原因是引用的一种重要特性,基类引用可以执行派生类对象,当有一组通过继承关系关联起来的异常类型,则在异常规范中只需列出一个基类引用,它将可以与任何派生类对象匹配。

基类引用可以指向派生类对象,虽然可以使用基类对象来捕获基类和派生类对象,但是想要派生类对象只能捕获它所属类及从它派生出的类,那么catch类的排列顺序应该与派生顺序相反,因为引发异常对象将被第一个与之匹配的catch捕获。例子如下:

向上面的这个程序,如果将bad_1 &放在最前面,那么它将捕获bad_1、bad_2和bad_3;相反,如果将bad_3放在最前面,异常将被bad_3处理程序所捕获。

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

第三,有时并不能确切知道异常发生在那一块,例如,当在一个函数中调用了另一个函数,但是并不知道可能会引发那些异常。这种情况下,想要其依然能够捕获所有的异常,方法如下:

catch (…) {//statement}

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

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

exception类

在较新的c++编译器将异常合并到语言中,为了支持该语言,exception头文件定义了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 argument to gmean()";}
……
};

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

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

否则可以分别捕获它们。

c++中定义了很多基于exception的异常类型。下面介绍几种异常类型

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 domian_error(const string& what_arg);
……
};

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

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

(1)domain_error;

(2)invalid_argument;

(3)length_error;

(4)out_of_bounds。

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

例如反正弦函数的定义域为-1到+1,如果编写一个函数,将一个参数传递给函数std::sin(),则可以让函数在参数不在定义域内是引发domain_error异常。

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

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

异常out_of_bounds通常用于指示索引错误。

runtime_error异常系列描述了可能在运行期间发生但难以预计和防范的错误,下面介绍1几种:

(1)range_error;

(2)overflow_error;

(3)underflow_error.

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

综上,logic_error系列异常表明存在可以通过程序修复的问题,但是runtime_error系列异常表明存在无法避免的问题。它们之间的不同之处在于:不同的类名可以分别处理每种异常。另一方面,继承关系可以让程序员一起处理它们。例如:

try{
 ……
};
catch(out_of_bounds & oe)
{……}
catch(logic_error & oe)
{……}
catch(exception & oe)
{……}

下面的代码首先单独捕获out_of_bounds异常,然后统一捕获其他logic_error系列异常,最后捕获exception异常、runtime_erro系列异常以及从exception派生而来的异常。如果上面的库类不能满足需求,应该从logic_error或者runtime_error中派生出一个异常类,确保异常类可以归入同一个继承层次结构上。

bad_alloc异常和new

对于使用new导致的内存分配问题,可以让new引发bad_alloc异常。头文件new包含bad_alloc类的声明,它是从exception类公有派生而来的。下面将使一个程序来说明下:

// newexcp.cpp -- the bad_alloc exception
#include <iostream>
#include <new>
#include <cstdlib>  // for exit(), EXIT_FAILURE
using namespace std;

struct Big
{
    double stuff[400000];
};

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

 输出为

Trying to get a big block of memory:
Caught the exception!
bad allocation

当试图使用new申请一个过大的内存时,程序就会捕捉到异常,程序将显示继承的what()方法返回的消息,然后终止。

空指针和new

很多程序都是在new失败时返回空指针时编写的(在以前,当无法分配请求的内存量时,new就会返回一个空指针),为处理new的变化,有的编译器提供一个开关,让用户选择所需的行为。当前,c++标准提供了一种在失败时返回空指针的new,用法如下:

int * pi = new (std::nothrow) int;//错误变成空指针
int * pa = new (std::nothrow) int[500];//不会返回空指针,要用bad_alloc

异常、类和继承

异常、类和继承三种方式相互关联:可以从一个异常类派生出另一个,可以在类定义中嵌套异常类声明来组合异常,最后,这种嵌套声明本身可被继承,还可以用作基类。用下面的例子来举证上面:

首先头文件:

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

首先LabelSales类是从Sales派生而来的,新增了一个用于存储数据标签的成员。符号常量MONTHS位于sales的公有部分,这就使得它的派生类可以使用这个值。

其中bad_sales被嵌套在Sales类的公有部分,这就使得客户类的catch块可以使用这个类作为类型。在外部使用这个类型时,需要使用Sales::bad_index来标识。这个类就是从logic_error类派生而来的,能够存储和报告数组索引的超界值。

nbad_index类被嵌套在LabledSales的公有部分,客户类可以使用LableSales::nbad_index来使用它。它从bad_index类派生而来的,新增了存储和报告LabeledSales对象的标注功能。由于bad_index是从logic_error派生而来的,所以nbad_index归根究底也是从logic_error派生而来的。

其次,Sales和LabeledSales类都有重载operator[ ]()方法,这些方法设计用于访问存储在对象中的数字元素,并在索引超界时引发异常。

bad_index和nbad_index类都使用了异常规范throw(),因为它们都是从基类exception派生而来的,而exception的虚折构函数使用了异常规范throw()。

实现文件

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

上面得实现文件中包含了没有实现为内联的方法的实现。对于被嵌套类的方法,需要使用多个作用域解析运算符。另外如果数组索引超界,函数operator[ ]()将引发异常。

测试程序

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

在测试文件中尝试试图超越LbaledSales对象sales2中数组的末尾,然后试图超越Sales对象sales1中数组的末尾。这些尝试是两个try块中进行的,这样可以检测到各种异常。 

输出为:

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

异常何时会迷失方向

异常被引发时,在两种情况下会被导致问题。(1)第一种情况是,如果它是在带有异常规范的函数中引发的,则必须与规范列表中的某种异常匹配(在继承层次结构中,类类型将与这个类及其派生类的对象匹配),否则就称为意外异常。在默认情况下,这将导致程序会异常终止。

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

现在可以修改程序对意外异常和未捕获异常的反应。具体操作如下:

对于未捕获异常:未捕获异常不会都导致程序立即终止。相反,程序首先调用terminate()。默认情况下termiante()调用abort()函数。可以指定termiante()应调用的函数((也就是不调用abort())修改termiante()的这种行为。为了达到这个目的,可以调用set_termiante()函数。set_terminate()和termiante()都是在头文件exception中声明的。

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

来看一个例子,假设希望未捕获的异常导致程序打印一条消息,然后调用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)
{
    ……
}

有关异常的注意事项

从之前的讨论中,应该在设计程序时就加入异常处理程序,而不是以后再添加。增加异常处理机制会增加程序代码,降低程序的运行速度。异常规范不使用与模板,因为模板函数引发的异常可能岁特定的具体化而异。异常和动态内存分配并非总能协同工作。

下面将进一步讨论动态内存分配和异常。

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

string类采用动态内存分配。通常,当函数结束时,将为mesg调用string的析构函数(这里是自动调用的,请对比下面的程序中使用new创建double指针)。虽然throw语句过早地终止了函数,但它仍然使得析构函数被调用,这都是栈解退的作用。所以内存可以被正确地管理。

接下来看下面的这个函数:

void test()
{
    double * ar = new doublep[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;
}

RTTI

RTTI是运行阶段类型识别,它旨在为程序在运行阶段确定对象的类型提供一个标准方式。

用途:假设一个类层次结构,其中的类都是从一个基类派生而来的,则可以让基类指针指向其中任何一个类的对象。这时选择其中一个类并创建这种类型的对象,然后返回它的地址,而该地址可以被赋给基类指针,这时想要知道它指向的是那个类的对象(知道它是那个类的对象的用处主要有1.可调用类方法的正确版本,例如派生对象可能包含不是继承而来的方法,这种情况下,只有那种类型的对象可是使用该方法。3.可能处于调试的目的,想跟踪生成的对象的类型。)可以使用RTTI提供解决方法。

RTTI的工作原理

c++中有3个支持RTTI的元素。

(1)dynamic_cast运算符将使用一个指向基类的指针来生成一个指向派生类的指针;否则,该运算符返回0——空指针。

(2)typeid运算符返回一个指向对象的类型的值。

(3)type_info结构存储了有关特定类型的信息。

注意:RTTI只能用于包含虚函数的类层次结构,原因是只有对于这种类层次结构,才能将派生类对象的地址赋给基类指针。

下面将详细介绍RTTI的这3种元素

(1)dynamic_cast运算符

dynamin_cast是最常见的RTTI组件,它可以指出“是否可以安全地将对象的地址赋给特定类型的指针”,也就是下面这样的继承结构:

class Grand{};
class Super : public Grand{}
class Magnificent:public Superb {};

 假设有下面这样的指针:

Grand * pg = new Grand;
Grand * ps = new Superb;
Grand * pm = new Magnifient;

有下面的这样的类型转换

Magnificent * p1 = (Magnificent *) pm;    //#1
Magnificent * p2 = (Magnificent *) pg;    //#2
Superb *p3 = (Magnificent *) pm;           //#3

 对于#1来说,这种转换是安全的,因为它将Magidicent对象的指针指向类型为Magificent的对象。

对于#2来说,这种转换就是不安全的,因为它将基类(Grand)的地址赋给派生类(Magificent)指针。想要期望基类对象拥有派生类对象的特征是不可能的,因为派生类对象可能包含一些Grand对象没有的数据成员。

对于#3来说,这种转换是安全的,因为它将派生类对象的地址赋给基类指针,公有派生确保Magificent对象同时也使一个Superb对象(直接基类)和一个Grand对象(间接基类)。所以将它的地址赋给这3种类型都是安全的。

虚函数确保了将这3种指针的任何一种指向Magnificent对象时,都将调用Magnificent方法。

首先看一下dynamic_cast的语法,用法如下,其中pg指向一个对象:

Superb * pm = dynamic_cast<super *>(pg);

 上面的语句可以回答指针pg是否可以被安全地转化成Super *,如果可以,运算符将返回对象的地址,否则就返回一个空指针。

下面使用用一个实例来说明这种情况:

首先在程序中定了3个类,名称是Grand、Superb和Magnificent。Grand定义了一个虚函数Speak(),其他类都定义了该虚函数。Superb类定义了一个虚函数Say(),而在Magificent中又重新定义了它。

在程序中定义了GetOne()函数,该函数随机创建了这3种类中某种类的对象,并对其初始化,然后将地址作为Grand*指针返回。

程序15.17

// rtti1.cpp -- using the RTTI dynamic_cast operator
#include <iostream>
#include <cstdlib>
#include <ctime>

using std::cout;

class Grand
{
private:
    int hold;
public:
    Grand(int h = 0) : hold(h) {}
    virtual void Speak() const { cout << "I am a grand class!\n";}
    virtual int Value() const { return hold; }
};

class Superb : public Grand
{
public:
    Superb(int h = 0) : Grand(h) {}
    void Speak() const {cout << "I am a superb class!!\n"; }
    virtual void Say() const
        { cout << "I hold the superb value of " << Value() << "!\n";}
};

class Magnificent : public Superb
{
private:
    char ch;
public:
    Magnificent(int h = 0, char c = 'A') : Superb(h), ch(c) {}
    void Speak() const {cout << "I am a magnificent class!!!\n";}
    void Say() const {cout << "I hold the character " << ch <<
               " and the integer "  << Value() << "!\n"; }
};

Grand * GetOne();

int main()
{
    std::srand(std::time(0));
    Grand * pg;
    Superb * ps;
    for (int i = 0; i < 5; i++)
    {
        pg = GetOne();
        pg->Speak();
        if( ps = dynamic_cast<Superb *>(pg))
            ps->Say();
    }
    // std::cin.get();
    return 0;
}

Grand * GetOne()    // generate one of three kinds of objects randomly
{
    Grand * p;
    switch( std::rand() % 3)
    {
        case 0: p = new Grand(std::rand() % 100);
                    break;
        case 1: p = new Superb(std::rand() % 100);
                    break;
        case 2: p = new Magnificent(std::rand() % 100, 
                              'A' + std::rand() % 26);
                    break;
    }
    return p; 
}

其中如下循环:

for(int i = 0; i < 5; i++)
{
    pg = GetOne();
    pg->Speak();
    ……
}

使用循环将该指针赋给Grand*变量pg,然后使用pg调用Speak()函数,因为这个函数虚拟的,所以可以代码能够正确调用指向的对象的Speak()版本。

但是,不能使用相同的方式(也就是使用指向Grand的指针)来调用Say()函数,因为Grand类没有定义它。但是,可以使用dynamic_cast运算符来检查是否可以将pg的类型安全地转化成Superb指针。如果对象的类型是Superb或者Magnificent,则可以安全转换,因为这两种情况下,都可以安全地调用Say()函数。

也就是函数中的语句中的

if (ps = dynamic_cast<Superb *> (pg))
    ps -> Say();

其中if条件中是一个赋值语句,赋值表达式的值是他左边的值,因此if条件的值是ps,如类型转换成功,则ps的值为非零;如果类型转换失败,即pg指向的是Grand对象,ps的值就是0(false)。

注意:(1)对于有的编译器,会对无目的的赋值提出警告,比如说在if条件语句中,我们一般使用==运算符。(2)即使编译器支持RTTI,但在默认情况下,它可能会关闭该特性,如果该特性被关闭,程序可以仍然通过编译,但是会出现运行阶段错误。这种情况下,可以查看文档或者菜单。

dynamic_cast也可以用于引用,只是用法稍微有所不同,也就是没有与空指针对应的引用值,因此将无法使用特殊的引用值来提示失败。当请求不正确时,dynamic_cast将引发类型为bad_cast的异常,这种异常是从exception类派生而来,它在头文件typeinfo中定义的,所以可以像下面那样使用该运算符,其中rg是对Grand对象的引用。

#include <typeinfo>
……
try{
    Superb & rs = dynamic_cast<Superb &>(rg)
    ……
}
catch(bad_cast &){
……
};

typeid运算符和type_info类

typeid运算符可以确定两个对象是否是同种类型,它与sizeof有些相似,可以接受两种参数:

(1)类名

(2)结果是对象的表达式

typeid运算符可以返回一个对type_info对象的引用。其中,type_info是在头文件typeinfo(以前是typeinfo.h)中定义的一个类。type_info类中重载了==和!=运算符,以便于使用这些运算符来对类型进行比较。

例如语句:

typeid(Magnificent) == typeid(*pg)

上述语句如果pg指向的是一个Magnificent对象,则下述表达式的结果是bool值true,否则为false。如果pg是一个空指针,程序将引发bad_typeid异常,该异常类型是从exception类派生而来的,是在头文件typeinfo中声明的。

type_info类地实现随厂商而异,但都包含一个name成员,该函数返回一个随实现而异的字符串:通常是类的名称。

例如语句:

cout << "Now processing type " << typeid(*pg).name() << ".\n";

其中typeid(*pg)返回一个type_info对象的引用,所以typeid(*pg)可以调用type_info的类方法name(),这里它可以显示指针pg指向的对象所属的类定义的字符串。

在下面的程序中改进上面的程序,使用了typeid运算符和name()成员函数,该程序包含了头文件typeinfo。

程勋15.18

// rtti2.cpp  -- using dynamic_cast, typeid, and type_info
#include <iostream>
#include <cstdlib>
#include <ctime>
#include <typeinfo>
using namespace std;

class Grand
{
private:
    int hold;
public:
    Grand(int h = 0) : hold(h) {}
    virtual void Speak() const { cout << "I am a grand class!\n"; }
    virtual int Value() const { return hold; }
};

class Superb : public Grand
{
public:
    Superb(int h = 0) : Grand(h) {}
    void Speak() const { cout << "I am a superb class!!\n"; }
    virtual void Say() const
    {
        cout << "I hold the superb value of " << Value() << "!\n";
    }
};

class Magnificent : public Superb
{
private:
    char ch;
public:
    Magnificent(int h = 0, char cv = 'A') : Superb(h), ch(cv) {}
    void Speak() const { cout << "I am a magnificent class!!!\n"; }
    void Say() const {
        cout << "I hold the character " << ch <<
            " and the integer " << Value() << "!\n";
    }
};

Grand* GetOne();
int main()
{
    srand(time(0));
    Grand* pg;
    Superb* ps;
    for (int i = 0; i < 5; i++)
    {
        pg = GetOne();
        cout << "Now processing type " << typeid(*pg).name() << ".\n";
        pg->Speak();
        if (ps = dynamic_cast<Superb*>(pg))
            ps->Say();
        if (typeid(Magnificent) == typeid(*pg))
            cout << "Yes, you're really magnificent.\n";
    }
    // std::cin.get();
    return 0;
}

Grand* GetOne()
{
    Grand* p;
    p = NULL;

    switch (rand() % 3)
    {
    case 0: p = new Grand(rand() % 100);
        break;
    case 1: p = new Superb(rand() % 100);
        break;
    case 2: p = new Magnificent(rand() % 100, 'A' + rand() % 26);
        break;
    }
    return p;
}

在上述程序中使用了typeid运算符和name()成员函数。注意它们适用于dynamic_cast和vitual函数不能处理的情况。typeid测试用来选择一种操作,因为这个操作不是类方法,所以不能通过类指针来调用它。name()方法语句演示如何将方法用于调试。

这里使用了rand()来选择类型,它是随机的,所以每次输出的结果可能不同。输出如下所示:

Now processing type class Grand.
I am a grand class!
Now processing type class Grand.
I am a grand class!
Now processing type class Magnificent.
I am a magnificent class!!!
I hold the character M and the integer 52!
Yes, you're really magnificent.
Now processing type class Superb.
I am a superb class!!
I hold the superb value of 81!
Now processing type class Grand.
I am a grand class!

误用RTTI的例子

很多人定义RTTI的口碑不好,认为其是多余的,是效率低下的。下面介绍应该避免地编程方式。

首先,看下上述程序15.17中的核心代码:

Grand * pg;
Superb * ps;
for(int i = 0; i<5; i++)
{
    pg = GetOne();
    pg->Speak();
    if(ps = dynamic_cast<Super *>(pg))
        ps->Say();
}

在程序15.18中将其改为

Grand * pg;
Grand * ps;
for(int i = 0;i<5; i++)
{
    pm = (Magnificent *)pg;
    pm->Speak();
    pm = Say(); 
}
else if(typeid(Superb) == typeid(*pg))
{
    ps = (Superb *) pg;
    ps->Speak();
    ps->Say();
}
else
    pg->Speak();
}

上述代码比15.17更长,并且显式地指定了各个类,存在缺陷。例如说如果想要从Magnifiicent类中派生出一个Insufferable类,而后者需要重新定义Speak()和Say()。使用typeid来显示地测试每个类型时,必须修改for循环的代码,添加一个else if,但无需修改原来的版本。语句pg->Speak()适用于所有从Grand派生而来的类。

下面语句适用于所有从Superb派生而来的类:

if(ps = dynamic_cast<Superb *>(pg))
    ps->Say();

注意:如果发现在扩展的if else语句系列中使用了typeid,则应该考虑是否使用虚函数和dynamic_cast。

类型转换运算符

为了规范松散的类型转换,现在使用更加严格的类型转换,并添加4个类型转换运算符,使得转换过程更加规范。如下:

(1)dynamic_cast;

(2)const_cast;

(3)static_cast;

(4)reinterpret_cast;

可以根根据目的选择一个合适的运算符,而不是使用通用的类型转换,这样即指出类型转换的原因,还可以让编译器能够检查程序的行为是否与设计者想法吻合。

(1)dynamic_cast再回顾一下,High和Low是两个类,而ph和pl都得类型分别是High*和Low*,则仅当Low是High的可访问基类(直接或者间接)时,语句pl = dynamic_cast<Low *> ph;才能将一个Low*赋给pl,否则该语句将空指针赋给pl。可总结出该运算符的语法如下:

dynamic_cast <type_name> (expression)

该运算符的用途是能够在结构层次上进行向上转换(由于是is-a关系,这样的类型转换是安全的),而不允许其他转换。

(2)const_cast运算符用于执行只有一种用途的类型转化,语法结构与dynamic_cast运算符相同,如下:

const_cast <type_name> (expression)

 除了const或者volatile特征可以不同外,type_name和expression的类型必须相同。假设High和Low两个类:

High bar;
const High * pbar = &bar;
……
High * pd = const_cast<High *>(pbar);    //合法
const Low * pl = const_cast<const Low *> (pbar);   //不合法

第一个转换可以使得*pb成为一个可修改bar对象值的指针,它删除const标签。第二个类型转换是非法的,因为它同时尝试将类型从const High * 改为const Low。

该运算符的运用场所是:有时可能需要这样一个值,它在大部分时候是常量,而有时可以修改的。在这种情况下,可以将这个值声明为const,并在需要修改它时,使用const_cast。这也可以通过通用类型转换来实现,但通用转换也可能同时改变类型。例如下面这样:

High bar;
const High * pbar = &bar;
……
High * pb = (High * )(pbar);    //合法
Low * pl = (Low *)(pbar);        //依然合法

上述情况下,由于编程可能会无意间同时改变类型和常量特征,所以使用const_cast更加安全。但是请注意const_cast不是万能的,它可以修改指向一个值的指针,但是修改const的值是不确定的,如下:

// constcast.cpp -- using const_cast<>
#include <iostream>
using std::cout;
using std::endl;

void change(const int * pt, int n);

int main()
{
    int pop1 = 38383;
    const int pop2 = 2000;

    cout << "pop1, pop2: " << pop1 << ", " << pop2 << endl;
    change(&pop1, -103);
    change(&pop2, -103);
    cout << "pop1, pop2: " << pop1 << ", " << pop2 << endl;
    // std::cin.get();
    return 0;
}

void change(const int * pt, int n)
{
    int * pc;
  
    pc = const_cast<int *>(pt);
    *pc += n;
 
}

在上述程序中,const_cast运算符可以删除const int*pt中的const,使得编译器能够接受change()中的语句*pc += n;(不会报错),但是它不能修改pop2的值。原因如下:在change()函数中,指针pt被声明为const int*,所以本身不能用来修改指向的int,之后指针pc删除const特征,所以它是可以用来修改指向的值,当且仅当指向的值不是const时才可以。所以pc可用于修改pop1,但是不能用来修改pop2。

(3)static_cast:该运算符的语法与其他语法类型转换运算符相同,如下:

static_cast <type_name>(expression)

只有当type_name可被隐形转换为expression所属的类型或者expression可被隐式转换成type_name所属的类型时,上述的转换才合法,否则就会出错。例如,High类是Low的基类,而Pond类是一个无关的类,则从High到Low的转换、从Low到High的转换都是合法的,而从Low到Pond的转换是不允许的,如下程序:

High bar;
Low blow;
……
High * pb = static_cast<High *>(&blow);    //合法,向上转换
Low * pl = static_cast<Low *>(&bar);        //合法,向下转换
Pond * pmer = static_cast<Pond *>(&blow);    //不合法

第一种转换是合法的,因为向上的转换是可以显式的进行。

第二种转换是从基类指针到派生类指针,在不进行显式类型转换的情况下,将无法进行。但是由于无需类型转换,使用static_cast来进行这种向下的转换是被允许的。

同理,由于无需进行类型转换,枚举值可以被转换成整型,所以可以用static_cast将整型转换成枚举值。同样可以使用static_cast将double值转换成int、将float转换成long以及其他各种数值转换。

(4)reinterpret_cast运算符适用于危险的类型转换,它的语法结构和其他一样,如下:

reinterpret_cast <type_name>(expression)

 下面的一个示例:

struct dat{short a; short b;};
long value = 0xA224B118;
dat * pd = reinterpret_cast<dat *>(&value);
cout << hex << pd->a;

这样的转换适用于依赖于实现的底层编程技术,是不可移植的,例如不同系统存在诸多字节整型时,可能一不同顺序存储其中的字节。

但是,reinterpret_cast运算符并不支持所有的类型转换,例如,可以将指针类型转换为足以存储指针表示的整型(十六进制数字很大)时,但不能将指针转换成更小的整型或者浮点型。另一个限制是,不能将函数指针转换成数据指针,反之亦然。

最后,在c++中普通的类型转换也可能会受到限制。基本上,可以执行其他类型转换可执行的操作,加上一些组合,如static_cast或者reinterpret_cast后跟const_cast,但不能执行其他转换。

下面这样的类型转换在c中是被允许的,但是在c++中不允许,因为对于大部分c++实现来说,char类型都太小了,不能存储指针。

char ch = char (&d);

本章总结

友元使得能够为类开发更灵活的接口,类可以将其他函数、其他类和其他类的成员函数作为友元。在某种情况下可能需要前向声明,需要注意类和方法声明的顺序,以便正确地组合友元。

嵌套类是在类声明中声明的类,它有助于设计这样的助手类,即实现其他类,但不必是公有接口的组成部分。

c++异常机制为处理不好的编程事件,比如说不适当的值,I/O失败等,提供了一个灵活的方式。引发异常将终止当前的执行的函数,将控制权交给匹配的catch块。catch块紧跟在try块的后面,为了捕获异常,直接或者间接导致异常的函数调用必须位于try块中。这样的程序将执行catch块中的代码。这样试图解决问题或者终止程序。类可以包含嵌套的异常类,嵌套异常类在相应的问题被发现是被引发。

函数可以包含异常规范,指出该函数中可能引发的异常;但是c++11摒弃了这项功能。

未被捕获的异常(没有匹配的catch块的异常)在默认情况下将终止程序,意外异常(不与任何异常规范匹配的异常)也是如此。

RTTI特性让程序能够检测对象的类型。dynamic_cast运算符用于将派生类指针转换成基类指针,其主要用途是确保可以安全地调用虚函数。Typeid运算符返回一个type_info对象。可对两个typeid()的返回值进行比较,以确保对象是否是特定的类型,而返回的type_info对象可用于捕获关于对象的信息。

与通用转换机制相比,dynamic_cast、static_cast、const_cast和reinterpret_cast提供了更安全、更明确的类转换。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值