第十五章 友元、异常和其他

第 15 章 友元、异常和其它

15.1. 友元

友元函数用于类的扩展接口 中,类中不仅可以使用友元函数,也可以将类作为友元。

友元类的所有方法都可以访问原始类的私有成员保护成员

在一个类中将另一个类声明为友元。

15.1.1 友元类

什么时候希望一个类成为另一个类的友元呢?

举个例子:模拟电视机和遥控器的简单程序。要知道遥控器并非电视机,故公有继承is-a关系并不适用。遥控器也非电视机的一部分,因此包含或者私有继承和保护继承的has-a关系也不适用。事实上,遥控器可以改变电视机的状态,这就表明遥控器类应当作为电视类的一个友元。

按照如上描述,决定编写一个模拟电视机和遥控器的简单程序。

首先定义Tv类,可以用一组状态成员(描述电视各个方面的变量)来表示电视机。下面是一些可能的状态:

  • 开/关;
  • 频道设置;
  • 音量设置;
  • 有线电视或天线调节模式
  • TV调谐或A/V输入

ps:当前的电视机都将控件藏在面板之后(换台只能逐频道换),遥控器的控制能力必须得与电视机内置的控制功能相同,它的大部分方法是根据Tv内置的方法来进行改造的,就比如刚刚所说的利用电视机控制面板切台只能逐个频道切换,遥控器在此基础上进行改造,使其能够按个人意愿进行切台。

为什么说遥控器大部分方法是根据Tv方法进行改造的呢?因为遥控器本身还有其他一些自己单独的功能,就好比如切换工作模式,一种拿来控制电视机、一种拿来控制DVD。

下面语句使得Remote成为友元类

friend class Remote;

友元声明可以位于公有、私有或者保护部分,其所在的位置无关紧要。

遥控器是后来者,所以编译器必须了解电视(Tv)类后,才能处理遥控器(Remote)类。

下面开始模拟遥控器操控电视机,或者电视机自己操控自己:

1、头文件

//
// Created by e on 2022/10/18.
//

#ifndef TEST_TV_H
#define TEST_TV_H

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

    Tv(int s = Off, int mc = 125) : state(s), volume(5),
                                    maxChannel(mc), curChannel(2), mode(Cable), input(TV) { }

    void onOff() {
        state = (state == On) ? Off : On;
    }

    bool isOn() const {
        return state == On;
    }

    bool volUp();   //声音提高
    bool volDown(); //声音降低
    void channelUp();  //频道往后调
    void channelDown();   //频道往前调
    void setMode() {
        mode = (mode == Antenna) ? Cable : Antenna;
    }

    void setInput() {
        input = (input == TV) ? DVD : TV;
    }

    void showSettings() const;   //display all TV settings.

private:
    int state;      //on or off
    int volume;     //assumed to be digitized-->假设是用数字大小代表音量大小
    int maxChannel; //maximum number of channels
    int curChannel; //current channel setting
    int input;      //TV or DVD
    int mode;       //broadcast or cable
};

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 channelUp(Tv &t) { t.channelUp(); }

    void channelDown(Tv &t) { t.channelDown(); }

    void setChannel(Tv &t, int c) { t.curChannel = c; }

    void setMode(Tv &t) { t.setMode(); }

    void setInput(Tv &t) { t.setInput(); }
};

#endif //TEST_TV_H

2、源文件

//
// Created by e on 2022/10/18.
//

#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::channelUp() {
    if (curChannel < maxChannel){
        curChannel++;
    }else{
        curChannel = 1;
    }
}

void Tv::channelDown() {
    if (curChannel > 1){
        curChannel--;
    }else{
        curChannel = maxChannel;
    }
}

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

3、执行文件

#include <iostream>
#include "tv.h"

int main(){
    using std::cout;
    Tv s42;
    cout << "Initial settings for 42\" TV:\n";
    s42.showSettings();
    s42.onOff();
    s42.channelUp();
    cout << "\n Adjusted settings for 42\" TV:\n";
    s42.showSettings();

    Remote grey;
    grey.setChannel(s42,10);
    grey.volUp(s42);
    grey.volUp(s42);
    cout << "\n42\" settings after using remote:\n";
    s42.showSettings();

    Tv s58(Tv::On);
    s58.setMode();
    grey.setChannel(s58,28);
    cout << "\n58\" settings:\n";
    s58.showSettings();

    return 0;
}

输出:
Initial settings for 42" TV:
TV is Off

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

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

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

15.1.2 友元成员函数

上个例子,Remote中大多数方法都是用Tv类的公有接口实现的。这说明这些方法实际上不太需要作为友元,可以直接地说多此一举了。在Remote方法中,setChannel()是直接访问了Tv的私有成员,所以它被设置为友元是很应该的。实际上,可以让一部分的类成员成为另一个类的友元。

⚠️:如果按照上面那样做,必须小心排列各种声明和定义的顺序

如果要将友元类中的特定的类成员解析作为原始类友元,无需将整个类作为友元。

class Tv
{
    friend void Remote::setChannel(tv & t, int c);
    ...
};

执行以上语句,需要编译器提前知道Remote的定义,所以将类声明为友元时,需要注意❗️:将类(TV)声明放在包含友元类(Remote)的前面。 这种方法称为 前向声明(forward declaration)

class Tv;  // 前向声明
class Remote {...}
class Tv {...};

思考🤔:能否像以下这样排列声明与定义呢?

class Remote;  // 前向声明
class Tv {...};
class Remote {...}

答案是:❌。解释✅:编译器在Tv类的声明中看到Remote的一个方法被声明为Tv类的友元之前,应该先看到Remote类的声明和setChannel()的方法声明才行。

我们还需要注意Remote声明包含了内联代码,例如

void onOff(Tv & t){t.onOff();}

由上述的语句可以看出,Remote的成员函数调用Tv类的一个方法,所以,Remote得知道有Tv这个类,并且Tv类有这个方法才行。假如按照第一种排序的话,解决办法✅:让Remote声明仅仅包含方法声明即可,将实际的定义放在Tv类之后即可。

下面是修改后的头文件:

//
// Created by e on 2022/10/19.
//

#ifndef TEST_TVFM_H
#define TEST_TVFM_H

class Tv;


class Remote {
public:
    enum {
        Off, On
    };
    enum {
        MinVal, MaxVal = 20
    };
    enum {
        Antenna, Cable
    }; // Antenna->天线 Cable->有线
    enum {
        TV, DVD
    };
private:
    int mode;       //controls TV or DVD
public:
    Remote(int m = Tv::TV) : mode(m) { }

    bool volUp(Tv &t);

    bool volDown(Tv &t);

    void onOff(Tv &t);

    void channelUp(Tv &t);

    void channelDown(Tv &t);

    void setChannel(Tv &t, int c);

    void setMode(Tv &t);

    void setInput(Tv &t);
};



class  Tv{
public:
    friend void Remote::setChannel(Tv &t, int c);
    enum {
        Off, On
    };
    enum {
        MinVal, MaxVal = 20
    };
    enum {
        Antenna, Cable
    }; // Antenna->天线 Cable->有线
    enum {
        TV, DVD
    };

    Tv(int s = Off, int mc = 125) : state(s), volume(5),
                                    maxChannel(mc), curChannel(2), mode(Cable), input(TV) { }

    void onOff() {
        state = (state == On) ? Off : On;
    }

    bool isOn() const {
        return state == On;
    }

    bool volUp();   //声音提高
    bool volDown(); //声音降低
    void channelUp();  //频道往后调
    void channelDown();   //频道往前调
    void setMode() {
        mode = (mode == Antenna) ? Cable : Antenna;
    }

    void setInput() {
        input = (input == TV) ? DVD : TV;
    }

    void showSettings() const;   //display all TV settings.

private:
    int state;      //on or off
    int volume;     //assumed to be digitized-->假设是用数字大小代表音量大小
    int maxChannel; //maximum number of channels
    int curChannel; //current channel setting
    int input;      //TV or DVD
    int mode;       //broadcast or cable
};


//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::channelUp(Tv &t) {t.channelUp(); }
inline void Remote::channelDown(Tv &t) {t.channelDown(); }
inline void Remote::setMode(Tv &t) {t.setInput(); }
inline void Remote::setChannel(Tv &t, int c) { t.curChannel = c; }


#endif //TEST_TVFM_H

下图说明修改前与修改后头文件的区别:

请添加图片描述

请添加图片描述

内联函数的链接性是内部的,意味着函数定义必须在使用函数的文件中。也可将定义放在实现文件中,但必须删除 关键字inline,此时的链接性是外部的。

15.1.3 其它友元关系

随着科技的发达,出现了交互式的遥控器,这启发我们可以让两个类成为彼此的友元。

注意⚠️:对于使用Remote对象的Tv方法,其原型可在Remote类声明之前声明,但必须在Remote类声明之后定义(原因:Tv类在Remote类声明前就声明了,此时Tv类通过friend关键字知道Remote是一个类,但是不知道Remote中有啥成员函数或者方法,所以对于使用Remote对象的Tv方法来说,只能等到Remote类声明完成之后再进行定义,这样可以保证编译器有足够的信息进行该方法的编译

class Tv
{
    friend class Remote;
    public:
        void buzz(Remote & r);
        ...
};

class Remote
{
    friend class Tv;
    public:
        void Bool volup(Tv & t) {t.volup();}
};

inline void Tv::buzz(Remote & r)
{
    ...
};

15.1.4 共同的友元

还有一种需要使用友元的情况:**函数需要访问两个类的私有数据。**首先想到的是,把这个函数设定为这两个类的成员函数,但这是不切实际的。我们可以将这个函数设置为这两个类的友元。

假定有一个Probe类和一个Analyzer类,前者表示某种可编程的测量设备,后者表示某种可编程的分析设备。两者均有内部时钟,且希望它们能够同步。

请添加图片描述

15.2 嵌套类

在C++中,可以将类声明放在另一个类中。在另一个类中声明的类被称为 嵌套类(nested class)

包含类(包含其他类的类)的成员函数可以创建和使用被嵌套类的对象;当类声明位于公有部分时,才能在包含类的外面使用嵌套类,而且必须使用作用域解析运算符。

✅理解:

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

对类进行嵌套和组合不同。

  • 组合将类对象作为另一个类的成员
  • 对类进行嵌套不创建类成员,而是定义一种类型(类型仅包含嵌套类声明的类中有效)。

之前十二章的Queue类示例,嵌套了结构定义,从而实现了一种变相的嵌套类:

class Queue{
private:
//class scope definitions
		//Node is a nested structure definition local to this class
		struct Node {Item item;struct Node *next;};
}

我们知道结构是一种其成员在默认情况下为公有的类,所以换句话说Node在这里就是一个嵌套类,但是该定义并没有彰显它作为一个类所具有的功能(例如:没有显式构造函数),我们接下来根据唯一创建了Node对象的enqueue()方法进行改造一下:

bool Queue::enqueue(const Item &item){
		if(isfull()){
				return false;
		}
		Node *add = new Node; //create node
		add->item = item;
		add->next = NULL;
}

我们按照上述的赋值来显式构造一个函数

class Queue{
// class scope definitions
		//Node is a nested class definition local to this class
		class Node{
				public:
						Item item;
						Node *next;
						Node(const Item &i):item(i),next(0){}
		};
};

由上述声明,我们可以稍微把enqueue()方法修改一下:

bool Queue::enqueue(const Item &item){
		if(isfull()){
				return false;
		}
		Node *add = new Node(item); //create node
}

15.2.1 嵌套类和访问权限

对类进行嵌套是为了实现另一个类,并避免名称冲突。

嵌套类的访问权限控制

  • 嵌套类的声明位置决定了嵌套类的作用域,决定了程序的哪些部分可以创建其类的对象
  • 和其它类一样,嵌套类的公有部分、私有部分和保护部分控制了对类成员的访问。

嵌套类访问的两种方式:

  • 作用域

    • 如果嵌套类是在另一个类的私有部分声明的,则只有后者知道它。对于从包含类派生出来的类,嵌套类也是不可见的,因为派生类不能直接访问基类的私有部分。

    • 如果嵌套类是在另一个类的保护部分声明的,则后者是可以看到它的。对于从包含类派生出来的类也是知道嵌套类的存在,并且可以直接创建这种类型的对象。

    • 嵌套类的作用域为包含它的类,在类外部使用,则需要使用类限定符

    • class Team{
      public:
      	 class Coach{...};
      	 ...
      }
      

      举个例子,假如有一个失业的教练,他不属于任何球队。要在Team类的外面创建Coach对象,可以这样做:

      Team::Coach forhire;	//create a Coach object outside the Team class
      

请添加图片描述

  • 访问控制

    • 对嵌套类访问控制规则与常规类相同。

      类声明的位置决定了类的作用域可见性。

    • 类可见后,访问控制规则(公有、保护、私有、友元)将决定程序对嵌套类成员的访问权限。

15.2.2 模版中的嵌套

模版很适合用于实现容器类,将容器类定义转换为模版时候,不会因为它包含嵌套类而带来问题。下面城西演示如何进行这种转换。和类模版一样,改头文件也包含类模版和方法函数模版。

1、头文件

//
// Created by e on 2022/10/19.
//

#ifndef TEST_QUEUETP_H
#define TEST_QUEUETP_H

template<class Item>
class QueueTP{
private:
    enum {Q_size = 10};
    //Node is a nested class definition
    class Node{
    public:
        Item item;
        Node *next;
        Node(const Item &i):item(i),next(0){}
    };
    Node *front;        //pointer to front of Queue
    Node *rear;         //pointer to rear of Queue
    int items = 0;          //current number of items in Queue
    int qSize;    //maximum number of items in Queue
public:
    QueueTP(int qs = Q_size);
    QueueTP(const QueueTP &q);
    QueueTP & operator=(const QueueTP & q);//重载赋值运算符
    ~QueueTP();
    bool isEmpty()const{
        return items == 0;
    }

    bool isFull()const{
        return items == qSize;
    }

    int queueCount()const{
        return items;
    }

    bool enqueue(const Item &item); //add item to end
    bool dequeue(Item &item);       //remove item from front
};

// QueueTP methods
template<class Item>
QueueTP<Item>::QueueTP(int qs):qSize(qs){
    front = rear = 0;
    items = 0;
}

template <class Item>
QueueTP<Item>::~QueueTP(){
    Node *temp;
    while (front != 0){
        temp = front;
        front = front->next;
        delete temp;
    }
}


//add item to queue
template<class Item>
bool QueueTP<Item>::enqueue(const Item &item) {
    if(isFull()){
        return false;
    }
    Node *add = new Node(item);
    items++;
    if(front == 0){
        front = add;
    } else{
        rear->next = add;
    }
    rear = add;
    return true;
}


//place front item into item variable and remove from queue
template<class Item>
bool QueueTP<Item>::dequeue(Item &item) {
    if (front == 0){
        return false;
    }
    item = front->item;
    items--;
    Node *temp = front;
    front = front->next;
    delete temp;
    if (items == 0){
        rear = 0;
    }
    return true;
}

template<class Item>
QueueTP<Item>::QueueTP(const QueueTP &q){
    qSize = q.qSize;
    front = rear = 0;
    if (!q.isEmpty()) {
        Node *cur = q.front;
        while (cur) {
            Node *add = new Node(cur->item);
            items++;
            if (front == 0) {
                front = add;
            } else {
                rear->next = add;
            }
            rear = add;
            cur = cur->next;
        }
    }
}

//QueueTP & operator=(const QueueTP & q);//重载赋值运算符
template<class Item>
QueueTP<Item> & QueueTP<Item>::operator=(const QueueTP<Item> &q) {
    if(this == &q){
        return *this;
    }
    qSize = q.qSize;
    while (items){ //清理左值,避免内存泄漏
        items--;
        Node *temp = front;
        front = front->next;
        delete temp;
        if (items == 0){
            rear = 0;
        }
    }

    if (!q.isEmpty()) {
        Node *cur = q.front;
        while (cur) {
            Node *add = new Node(cur->item);
            items++;
            if (front == 0) {
                front = add;
            } else {
                rear->next = add;
            }
            rear = add;
            cur = cur->next;
        }
    }

    return *this;
}
#endif //TEST_QUEUETP_H

2、执行文件

#include <iostream>
#include <string>
#include "queuetp.h"

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

    QueueTP<string> cs(5);
    string temp;

    cout << "Now common function is working :\n";
    while (!cs.isFull()){
        cout << "Please enter your name.You will be served in the order of arrival.\n";
        cout << "Name: ";
        getline(cin,temp);
        cs.enqueue(temp);
    }
    cout << "The queue is full.Processing begins!\n";

    cout << "Now copy constructor is working :\n";
    QueueTP<string>cb(cs);
    while (!cb.isEmpty()){
        cb.dequeue(temp);
        cout << "Now processing " << temp << "...\n";
    }
    cout << endl;

    cout << "Now the assignment operator is working :\n";
    QueueTP<string>cd;
    cd = cs;
    while (!cd.isEmpty()){
        cd.dequeue(temp);
        cout << "Now processing " << temp << "...\n";
    }

    return 0;
}


输出:
Now common function is working :
Please enter your name.You will be served in the order of arrival.
Name: Kinsey Millhone
Please enter your name.You will be served in the order of arrival.
Name: Adam Dalgliesh
Please enter your name.You will be served in the order of arrival.
Name: Andrew Dalziel
Please enter your name.You will be served in the order of arrival.
Name: Kay Scarpetta
Please enter your name.You will be served in the order of arrival.
Name: Richard Jury
The queue is full.Processing begins!

Now copy constructor is working :
Now processing Kinsey Millhone...
Now processing Adam Dalgliesh...
Now processing Andrew Dalziel...
Now processing Kay Scarpetta...
Now processing Richard Jury...

Now the assignment operator is working :
Now processing Kinsey Millhone...
Now processing Adam Dalgliesh...
Now processing Andrew Dalziel...
Now processing Kay Scarpetta...
Now processing Richard Jury...

假如忘记里面的知识点,例如:重载赋值运算符(回到第11章和第12章进行复习)…

15.3 异常

异常是C++相对较新的功能,早期老编译器中可能会没有实现,但新的编译器中则是默认关闭了该特性。所以需要使用编译器选项来开启。

讨论异常前,先搞一个试验:计算两个数的调和平均数(即两个数倒数的平均值的倒数),表达式大概如下:

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

如果x与y互为相反数,那么上述公式的分母变为0了(上面的表达式是不被允许计算的)。对于被零整除的情况,许多新的编译器通过生成一个表示无穷大的特殊浮点值来处理,cout将这种值显示为Inf,inf,INF或类似的东西(其实还挺智能的);而其他老式的编译器可能会生产在发生被零除时崩溃的程序。

15.3.1 返回abort()

调用位于头文件cstdlib(或stdlib.h)的Abort()函数。

典型实现:想标准错误(即cerr使用的错误流)发送消息 abnormal program termination(程序异常终止) ,然后 终止程序 。还返回一个随实现而异的值,告知OS(如果程序是由另一个程序调用,则告诉父进程),处理失败。

  • abort() 是否刷新文件缓冲区(用于存储读写到文件中的数据的内存区域)取决于实现。
  • exit():会刷新文件缓冲区,则不显示消息。

一般情况下,显示程序的异常中断消息随编译器而不同。

下面是一个使用abort()的小程序:

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



输出:
Enter two numbers: 3 -3
untenable arguments to hmean()

进程已结束,退出代码为 134 (interrupted by signal 6: SIGABRT)

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

15.3.2 程序错误码

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

任何数值都是有效的返回值,所以不存在可用于指出问题的特殊值。

一般使用指针参数或者引用参数来将值返回调用程序,并使用函数的返回值来指出成功还是失败

下面就是一个使用这种方式的例子,它将hmean()的返回值重新定义为bool,让返回值指出成功了还是失败了,另外还给函数增加了第三个参数,用于提供答案。

#include <iostream>
#include <cfloat> // 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;
    }
}


Enter two numbers: 3 3
Harmonic mean of 3 and 3 is 3
Enter next set of numbers <q to quit>: 1 1
Harmonic mean of 1 and 1 is 1
Enter next set of numbers <q to quit>: 2 0
Harmonic mean of 2 and 0 is 0
Enter next set of numbers <q to quit>: 1 9
Harmonic mean of 1 and 9 is 1.8
Enter next set of numbers <q to quit>: 3 -3
One value should not be the negative of the other - try again.
Enter next set of numbers <q to quit>: q
Bye!

第三个参数可以是指针或者是引用。对内置类型的参数,很多人都倾向于使用指针,因为这样可以明显看出哪个参数用于提供答案。

15.3.3 异常机制

C++异常是对程序运行过程中发生的异常情况的一种响应。

对异常的处理有3个组成部分:

  • 引发异常

    • 关键字throw 表示引发异常,后面紧跟值用来指出异常特征(描述语句,假如是string)。
  • 使用处理程序捕获异常

    • 关键字catch 表示捕获异常,后面括号中紧跟类型声明来指出异常处理程序要响应的异常类型(对应throw的异常特征的类型,假如也是string)。其后的代码块则指出采取的措施。
  • 使用try块

    • 标识特定的异常可能被激活的代码块,后面紧跟一个多个catch块
  • 表面需要注意代码引起的异常。

#include <iostream>
double hmean(double a,double b);

int main()
{
    double x,y,z;
    std::cout << "Enter tow number :";
    while (std::cin >> x >> y)
    {
        try{
            z = hmean(x,y); // 如果程序输入的值不对,则会使用catch块来对异常进行处理
        }
        catch (const char *s){
            std::cout << s << std::endl;
            std::cout << "Enter a new pair of numbers :";
            continue; // 结束while循环的剩余部分,重新从while语句开始
        }
        std::cout <<"Harmonic mean of " << x << " and " << y << " is " << z <<std::endl;
        std::cout << "Enter next set of number <q to quit> : ";
    }
    std::cout << "Bye !! \n";
    return 0;
}

double hmean(double a ,doubel b)
{
    if (a == -b)
    // throw 用于执行返回语句,会终止函数的执行
    // throw不是将控制权返回给调用程序,而是导致程序沿函数调用序列后退,直到找到包含try块的函数
        throw "bad hmean() arguments : a = -b not allowed ";
    return 2.0 * a * b / (a + b);
}

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

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

关键字catch表明这是一个处理程序,而char *s则表明该处理程序与字符串异常匹配。s与函数参数定义极其类似,因为匹配的引发将被赋值给s。

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

下面的图是程序的执行流程图:

请添加图片描述

如果函数引发了异常,而没有try块或没有匹配的处理程序时,程序会默认调用abort()函数。

15.3.4 将对象用作异常类型

通常,引发异常的函数将传递一个对象,这样做有一个很重要的优点:可以使用不同的异常类型来区分不同函数在不同情况下引发的异常。另外,对象可以携带信息,开发人员可以根据这些信息来确定引发异常的原因。

下面是针对函数hmean()引发的异常而提供的一种设计:bad_hmean。将一个bad_hmean对象初始化**(这个初始化存储俩参数值)**为传递给函数hmean()的值,而方法mesg()可用于报告问题。

1、头文件

//
// Created by e on 2022/10/22.
//

#ifndef TEST_EXC_MEAN_H
#define TEST_EXC_MEAN_H
class bad_hmean{
private:
    double val_1;
    double val_2;
public:
    bad_hmean(double a = 0, double b = 0):val_1(a),val_2(b){}
    void mesg();
};

inline void bad_hmean::mesg() {
    std::cout << "hmean(" << val_1 << " , " << val_2 << "):"
                << "invalid arguments: a = -b\n";
}

class bad_gmean{
public:
    double val_1;
    double val_2;
    bad_gmean(double a = 0 , double b = 0):val_1(a),val_2(b){}
    const char *mesg();
};

inline const char * bad_gmean::mesg() {
    return "gmean() arguments should be >= 0\n";
}

#endif //TEST_EXC_MEAN_H

2、执行文件

#include <iostream>
#include <cmath>
#include "exc_mean.h"
//函数原型
double hmean(double a,double b);
double gmean(double a,double b);

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

    double x,y,z;

    cout << "Enter two numbers: ";
    while (cin >> x >> y){
        try {
            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;
        }
        catch (bad_hmean &bg) {
            bg.mesg();
            cout << "Try again.\n";
            continue;
        }
        catch (bad_gmean &hg) {
            cout << hg.mesg();
            cout << "Values used: " << hg.val_1 << ", "
                 << hg.val_2 << endl;
            cout << "Sorry,you dont get to play any more.\n";
            break;
        }
        cout << "Enter next set of numbers <q to quit>: ";
    }
    cout << "Bye.\n";
    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 sqrt(a*b);
}


输出:
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
Geometric mean of 5 and -2 is gmean() arguments should be >= 0
Values used: 5, -2
Sorry,you dont get to play any more.
Bye.

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

bad_hmean异常处理程序使用了一条continue语句,而bad_gmean异常处理程序使用了一条break语句。如果用户给函数hmaen()提供的参数不正确,将导致程序跳过循环中余下的代码,直接进入下次循环;而用户给函数gmean()提供的参数不正确时将结束循环。

15.3.5 异常规范和C++11

异常规范是C++98中的一项功能,在C++11中已摒弃。但也需要了解:

double harm(double a) throw(bad_thing); // 可能会抛出异常,只会抛出 bad_thing 类型的异常
double marm(double) throw(); // 不会抛出异常

throw() 部分是异常规范,可能出现在函数原型函数定义中,

可包含类型列表,也可不包含。

异常规范的两个作用:

  • 告知可能需要使用 try块
  • 让编译器添加执行运行阶段检查代码是否违反异常规范。

C++11中支持了一种特殊的异常规范:使用新增的 关键字noexcept 指出函数不会引发异常。

double marm() noexcept; // 

15.3.6 栈解退

提要:程序将调用函数的指令的地址(返回地址)放到栈中。当被调用的函数执行完毕后,程序将使用该地址来确定从哪里继续执行程序。在调用过程中,函数参数被视为自动变量,被放到栈中,如果自动变量是类对象,则其相应的析构函数会被调用,函数执行结束时,函数对应的自动变量也被释放。

当函数调用出现异常而终止,则程序释放栈中的内存,但不会在释放栈的第一个返回地址后停止,而是继续释放栈,直到找到一个位于try块中的返回地址,这个过程叫 栈解退。函数返回仅仅处理该函数放在栈中的对象(释放操作),而throw语句则处理try块和throw之间整个函数调用序列放在栈中的对象。

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

请添加图片描述

下面程序是一个栈解退的示例:

main()和means()都创建demo类型的对象,它指出什么时候构造函数和析构函数被调用。

函数main()中的try块能够捕获bad_hmean 和 bad_gmean异常,而函数means()中的try块只能捕获bad_hmean异常

1、头文件

//
// Created by e on 2022/10/22.
//

#ifndef TEST_EXC_MEAN_H
#define TEST_EXC_MEAN_H
class bad_hmean{
private:
    double val_1;
    double val_2;
public:
    bad_hmean(double a = 0, double b = 0):val_1(a),val_2(b){}
    void mesg();
};

inline void bad_hmean::mesg() {
    std::cout << "hmean(" << val_1 << " , " << val_2 << "):"
                << "invalid arguments: a = -b\n";
}

class bad_gmean{
public:
    double val_1;
    double val_2;
    bad_gmean(double a = 0 , double b = 0):val_1(a),val_2(b){}
    const char *mesg();
};

inline const char * bad_gmean::mesg() {
    return "gmean() arguments should be >= 0\n";
}

#endif //TEST_EXC_MEAN_H

2、执行文件

#include <iostream>
#include <cmath>
#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::endl;
    using std::cout;
    using std::cin;

    double x,y,z;
    {
        demo d1("found in block in main()");
        cout << "Enter two numbers: ";
        while (cin >> x >> y) {
            try {
                z = means(x, y);
                cout << "The mean of " << x << " and " << y
                     << " is " << z << endl;
                cout << "Enter next pair: ";
            }
            catch (bad_hmean &bh) {
                cout << bh.mesg();
                cout << "Try again.\n";
                continue;
            } catch (bad_gmean &bg) {
                bg.mesg();
                cout << "Values used: " << bg.val_1 << " and " << bg.val_2 << endl;
                cout << "Sorry,you dont get play any more.\n";
                break;
            }
        }
        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 sqrt(a*b);
}

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

输出:
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 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 and -8
Sorry,you dont get play any more.
demo found in block in main()lives!
demo found in block in main() destroyed
Bye!

分析上述程序过程:

首先在main()函数中创建一个demo对象。然后进入代码块。执行下面语句时候会调用means()函数

z = means(x,y);

1⃣️进入means()函数中,又创建了另一个demo对象。函数means()使用6和12来调用hmean()和gmean(),它们将结果返回给means(),后者计算一个结果并将其返回(由于没出现异常,所以执行完try块后直接跳过catch块)。在返回结果前调用d2.show();返回结果后,函数means()执行完毕,因此自动为d2调用析构函数。

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

2⃣️接下来将6与-6发送给函数means(),通过means()继续创建一个新的demo对象,然后进入try块计算hm与gm,可惜,这次就没那么顺利了,在调用hm时,引发了bad_hmean异常,该异常被means()中的catch块获取,下面输出语句说明这点:

demo found in means() created
hmean(6 , -6):invalid arguments: a = -b

当前的catch块中的throw语句使得means()函数终止执行,并将异常传回到main()函数中。然而尽管函数被终止执行,但是它仍为d2调用了析构函数(泪目)。

❗️❗️恰恰这就是本案例主要想展示的点:程序进行栈解退以回到能够捕获异常的地方,将释放栈中的自动存储型变量。同时,在means()中重新引发的异常被传递给main()中,又再次被适合的catch块给捕获了。

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

3⃣️最后将6和-8发送给函数means()。同样,means()创建一个新的demo对象,然后将6、-8传递给hmean(),后者在处理它们时没有出现问题,但是传给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 and -8
Sorry,you dont get play any more.

显示完消息后,并自动为d1调用析构函数:

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

为什么主函数中的对象会在函数结束前就调用析构函数呢?

✅:因为这个d1对象是在代码块中初始化的,所以它在代码块执行完毕前就要调用析构函数了。

15.3.7 其它异常特性

引发异常时编译器总是创建一个临时拷贝,即使异常规范和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 }

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

如果不知道异常的类型,方法是省略号来捕获任何异常。

catch () {/* statement */} // catch any type exception

如果可以预知一些异常类型,类似于switch语句的使用。

try{
  duper();
}
catch(bad_3 &be)
{
  // statement
}
catch(bad_2 &be)
{
  // statement
}
catch(bad_1 &be)
{
  // statement
}
catch(bad_hmean &h)
{
  //statement
}
catch(...)
{
  // statement
}

15.3.8 exception类

C++异常的主要目的:设计容错程序时避免一些错误处理方式。

在C++中 exception头文件中定义了 exception类,类中的 what() 虚拟成员函数,会返回一个字符串,字符串的特征随实现而异。

#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() << std::endl;
}

C++定义的基于exception的异常类型

  • stdexcept异常类

    • 头文件stdexcept定义的其它几个异常类,例如:logic_errorruntime_error类,都是从公有方式从exception派生而来。

      class logic_error : public exception {
        public:
          exception logic_error(const string& what_arg);
          ...
      };
      class domain_error : public exception {
        public:
          explicit domain_error(const string& what_arg);
          ...
      }
      
    • 这些类的构造函数接受一个string对象作为参数,参数提供了方法 what() 以 C风格字符串方式返回字符数据

      • logic_error
        

        派生出来用于报告错误类型的类还有:

        • 逻辑错误,任何阶段
        • domain_error:传递给函数的参数不在定义域内而引发异常,例如要传一个参数给函数sin(),如果参数不在[-1,1]中则会引发此错误。
        • invalid_error:传递了一个意料之外的值
        • length_error:指出没有足够的空间来执行所需的操作。
        • out_of_bounds:用于指示索引错误。,例如:定义一个类似于数组的类时,其operator()[]在使用的索引无效时引发out_of_bounds异常。
        try{
        	...
        }
        catch(out_of_bounds %oe){ //catch out_of_bounds error
        	...
        }
        catch(logic_error %oe){	//catch remaining logic_error family
          	...
        }
        catch(exception &oe){	// catch runtime_error,exception objects
          	...
        }
        
      • runtime_error 类型派生出来的类:

        • 错误发生在运行阶段
        • range_error:不在函数允许的范围内,和上溢、下溢无关。
        • overflow_error:上溢。整型浮点型都有可能。
        • underflow_error:下溢。要发生在浮点数计算
    • 每一个类都有自己的构造函数,使 what()方法 能够返回的字符串。

  • bad_alloc异常和new

    • new 请求的内存(简单地说就是:一方太贪心,一方给不起)分配失败,则会引发 bad_alloc的异常错误。
    • 数组中最为常见。
    #include <iostream>
    #include <new>
    #include <cstdlib> // for exit(),EXIT_FAILURE
    using namespace std;
    
    struct Big {
        double stuff[200000000];
    };
    
    int main() {
        Big *pb;
        try {
            cout << "Trying to get a big block of memory:\n";
            pb = new Big[100000];        // 160000000000000 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;
        return 0;
    }
    
    输出:
    Trying to get a big block of memory:
    Caught the exception!
    std::bad_alloc
    
  • 空指针和new

    • new 分配内存失败,则会返回一个空指针

      。也是从exception类

      派生而来。所以C++标准提供了用法:

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

15.3.9 异常、类和继承

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

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

1、头文件

#ifndef TEST_SALES_H
#define TEST_SALES_H

#include <stdexcept>
#include <string>

class Sales {
public:
    enum {
        MONTHS = 12
    };     // could be a static const
    class bad_index : public std::logic_error {
    private:
        int badIndex; //bad index value
    public:
        explicit bad_index(int index, const std::string &s = "Index error in Sales object\n");

        int bi_val() const { return badIndex; }

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

#endif //TEST_SALES_H

解释用意:

1⃣️bad_index类被嵌套在Sales的公有部分,这使得客户类的catch块可以使用这个类作为类型。这个类是从logic_error类派生出来的,能够存储和报告数组索引的超界值(out_of_bounds value)

2⃣️bad_index类被嵌套在LabeledSales的公有部分。它是从bad_index类派生而来的,存储和报告LabeledSales对象的标签功能。由于bad_index是从logic_error派生而来的,因此nbad_index归根到底也是从logic_error派生而来的。

3⃣️我们可以看到两个嵌套类都使用了异常规范throw(),这是因为它们归根结底是从基类exception派生而来的,而exception的虚构造函数使用了异常规范throw()“c++98有、c++11无”。

4⃣️如果基类中的一个成员被说明为虚函数,这就意味着该成员函数在派生类中可能有不同的实现。

2、源文件

//
// Created by e on 2022/10/23.
//

#include "sales.h"

using std::string;

Sales::bad_index::bad_index(int index, const std::string &s) : std::logic_error(s), badIndex(index) {}

Sales::Sales(int yy) {
    year = yy;
    for (int i = 0; i < MONTHS; ++i) { //首先初始化数组gross
        gross[i] = 0;
    }
}

Sales::Sales(int yy, const double *gr, int n) {
    year = yy;
    int lim = (n < MONTHS) ? n : MONTHS; //控制数组长度,gr是传入的数组,n为数组的长度
    int i;
    for (i = 0; i < lim; ++i) {
        gross[i] = gr[i];
    }
    for (; i < MONTHS; ++i) {   //假如传入的数组长度小于既定的数组长度MONTHS,那么其余的数组置为0
        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 std::string &lb, int index, const std::string &s) : Sales::bad_index(index, s) {
    lbl = lb;
}

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

LabeledSales::LabeledSales(const std::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);
}

源文件说明:

1⃣️初始化列表,子类初始化父类的私有成员,子类可以通过调用父类的成员方法,可以读取到先前初始化的父类私有成员的值。就好像下面初始化了一个子类LabeledSales,通过调用父类的成员方法 int Year()const{},间接读取到了所初始化的年份。

2⃣️ 如同1⃣️nbad_index类继承bad_index,通过初始化列表将bad_index中的私有成员badIndex初始化,利用父类成员方法间接访问。

3⃣️ what函数用来表示异常的具体信息的, bad_index与nbad_index通过对logic_error初始化为s,就有了对应的报错信息。line44,nbad_index是通过初始化列表对bad_index中的有参构造函数中s默认参数进行覆盖,变成了Index error in LabeledSales object。

3、执行文件

#include <iostream>
#include "sales.h"
int main(){
    using std::cout;
    using std::endl;
    using std::cin;

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

输出:
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

解释说明:

catch的抓捕异常的顺序是:派生类->基类,因为派生类能捕获自身对象或者派生类对象所触发的异常,而父类可以捕获自身或者自己派生的任何对象的任何异常。

15.3.10 异常丢失

异常被引发后,会导致问题的两种情况:

  • 意外异常(unexpected exception):在带异常规范的函数中引发,但必须与规范列表中的某种异常匹配。

    • 可以通过调用 terminate()(默认行为)、abort() 或者 exit() 来终止程序
    • 引发异常
  • 未捕获异常(uncaught exception):在没有try和catch块外抛出的异常。

    • 不会导致程序立即异常停止。程序将首先调用terminate(),在默认情况下调用abort()函数,我们可以指定terminate()所调用的函数(而不是abort()),为此可以调用set_terminate()函数
    //terminate_handler指向没有参数和返回值的函数指针
    typedef void (*terminate_handler);
    
    terminate_handler set_terminate(terminate_handler f) throw();	//c++98
    terminate_handler set_terminate(terminate_handler f) throw noexcept;	//c++11
    
    void terminate();//c++98
    void terminate() noexcept;//c++11
    
    • 下面举例

      #include <exception>
      using namespace std;
      
      void myQuit(){
        	cout << "Terminating due to uncaught exception\n";
        	exit(5);
      }
      
      //终止操作指定为调用该函数
      set_terminate(myQuit);
      
      try{
        	x = Argh(a,b);
      }
      catch(out_of_bounds &ex){
        	...
      }
      

    ‼️原则上,异常规范应包含函数调用的其他函数引发的异常。举个例子:如果Argh()调用了Duh()函数,而后者可能引发retort对象异常,则Argh()和Duh()的异常规范都应该包含retort,除非前者自己编写所有函数,

引发异常(第二种选择)的结果取决于unexpected_handler函数所引发的异常以及引发意外异常的函数的异常规范

  • 如果新引发的异常与原来的异常规范 匹配,则程序将从那里开始进行正常处理,即寻找与新引发的异常匹配的catch块。基本上是**用预期的异常取代意外异常**。
  • 如果新引发的异常和原来的异常规范 不匹配,切异常规范中没包括 std::bad_exception 类型,则程序调用 terminate()bad_exception 是从 exception类派生而来。声明位于头文件execption中。
  • 如果新引发的异常与原来 不匹配,且原来的异常规范中包含 std::bad_exception 类型,则不匹配的异常被 std::bad_exception 异常所取代。

如果要捕获所有的异常,则方法如下:

// 1. 确保已声明异常头文件
#include <exception>
using namespace std;

// 2. 设计替代函数,将意外异常转换为 bad_exception 异常
void myexception()
{
  throw std::bad_exception {};
}

// 3. 将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)
{
  ...
}

15.3.11 有关异常的注意事项

使用异常会增加程序代码,降低程序的运行速度。应在设计程序时候就加入异常处理功能,而不是以后再添加。

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

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

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

接下来看第二个例子:

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

⚠️这里有一个严重的问题:栈解退时,将删除栈中的变量ar(数组名为数组的首地址),但函数过早终止意味着函数末尾的delete[]语句被忽略了。现在指针消失了,它所指向的内存未被释放,所以这些内存被泄漏了。

当然这个问题是可以避免的,看如下:

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

尽管异常处理会降低程序的性能,但是不进行异常处理的代价对于某些大项目来说代价是非常高的。

15.4. RTTI

RTTI(Runtime Type Identification,运行阶段类型识别),C++11中新添加的新特性。

RTTI旨在为程序在运行阶段确定对象的类型提供一种标准方式。

RTTI只适用于包含虚函数的类

C++中支持RTTI的3个元素

  • dynamic_cast运算符:将使用一个指向基类的指针类生成一个指向派生类的指针,否则该运算返回0 ---- 空指针。
  • typeid运算符:返回一个指出对象的类型的值
  • type_info结构:存储有关特定类型的信息。

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

15.4.1 dynamic_cast 运算符

是最常用的RTTI架构,能够回答“是否可以安全地将对象的地址赋值给特定类型的指针”。

只有指针类型与对象的类型(或对象的直接或间接基类的类型)相同的类型转换才一定是安全的。

看下面的类层次结构:

class Grand {// has virtual methods};
class Superb:public Grand{..};
class Manificent:public Superb{...};
  
Grand *pg = new Grand;
Grand *ps = new Superb;
Grand *pm = new Magnificent;
  
//类型转换
Magnificent *p1 = (Magnificent *)pm; #1
Magnificent *p2 = (Magnificent *)pg; #2
Grand *p3 = (Magnificent *)pm; #3

只有那些指针类型与对象的类型(或者独享的直接或者间接基类的类型)相同的类型转换才一定是安全的。

1⃣️类型转换#1就是安全的,因为它将Magnificent类型的指针指向类型为Magnificent的对象。

2⃣️类型转换#2不安全,因为它将基类对象(Grand)的地址赋给派生类(Magnificent)的指针。(Magnificent对象可能包含一些Grand对象没有的数据成员。)

3⃣️类型转换#3安全的。因为它将派生对象的地址赋给基类指针。公有派生确保Magnificent对象同时也是一个Superb对象(直接基类)和一个Grand对象(间接基类)。

语法格式:

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

如果指针pg能够安全地被转换为Superb*,那么运算符将返回对象的地址,否则返回一个空指针。

结合下面语句从大范围地讲:如果指向的对象(*pt)的类型为Type或者从Type类直接或者间接派生而来的类型,则pt即可转为Type类型,反之,结果为0。

dynamic_cast<Type *>(pt);

下面演示一下dynamic_cast的用法:

#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();
        }
    }
    return 0;
}

Grand *GetOne(){
    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;
}

输出:
I am a Magnificent class!
I hold the character V and the integer 56!
I am a Magnificent class!
I hold the character X and the integer 53!
I am a Magnificent class!
I hold the character E and the integer 63!
I am a superb class!
I hold the superb value of 70!
I am a superb class!
I hold the superb value of 71!

下面的图为上述程序的结构图:

请添加图片描述

即使编译器支持RTTI,但在默认情况下,也可能是关闭该特性。

dynamic_cast 也可以用于引用。因为没有与空指针对应的引用值,所以无法使用特殊的引用值来指示失败。所以失败了,就会引发 bad_cast 的异常。

#include<typeinfo> // for bad_cast

try {
  Superb & rs = dynamic_cast<Superb &> (rg);
  ...
}
catch (bad_cast &) {
  ...
};

15.4.2 typeid 运算符 和 type_info

typeid 运算符使能够确定两个对象是否为同种类型。接受两种参数:

  • 类名
  • 结果为对象的表达式

typeid 运算符返回一个type_info对象的引用,其中 type_info头文件ypeinfo中定义的一个类。

typ_info类 重载 ==!= 运算符,以便于使用对类型进行比较。它包含一个name()成员,该函数返回一个随实现而异的的字符串:通常是类名

示例

typeid(Magnificent) == typeid(*pg) // 将pg指向一个 Magnifgicent 对象,表达式返回结果为True,否则为False。

typeid测试用来选择一种操作,因为操作不是类的方法,所以 不能通过类指针来调用它

‼️如果pg是一个空指针,那么程序将引发bad_typei.d异常,该异常类型在exception派生而来的,在头文件typeinfo中声明。

稍微修改一下15.4.1的示例,以让修改后的程序可以使用typeid()和name()

#include <iostream>
#include <cstdlib>
#include <ctime>
using std::cout;
using std::endl;

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();
        cout << "Now processing type " << typeid(*pg).name() << endl;
        pg->Speak();
        if (ps = dynamic_cast<Superb*>(pg)){
            ps->Say();
        }
        if (typeid(Magnificent) == typeid(*pg)){
            cout << "Yes , you're really Magnificent" << endl;
        }
    }
    return 0;
}

Grand *GetOne(){
    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;
}

输出:
Now processing type 11Magnificent
I am a Magnificent class!
I hold the character Z and the integer 58!
Yes , you're really Magnificent
Now processing type 11Magnificent
I am a Magnificent class!
I hold the character C and the integer 50!
Yes , you're really Magnificent
Now processing type 6Superb
I am a superb class!
I hold the superb value of 69!
Now processing type 5Grand
I am a grand class!
Now processing type 5Grand
I am a grand class!

15.4.3 误用RTTI的例子

对15.4.2的例子放弃dynamic_cast和虚函数则写出如下代码:

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

放弃上述二者,代码将变得又长又难看,如果当你想要从Magnificent再派生一个类出来的话,那么程序还得多加一个else if,并且新的派生类还需要重新定义Speak()和Say()函数。

✅如果发现程序中if else系列语句中使用typeid,从这个例子应该得到启发:是否需要使用dynamic_cast和虚函数进行优化。

15.5 类型转换运算符

假设 High 和 Low 是两个类。

通过4种类型转换运算符来使得转换过程增加规范。

  • dynamic_cast

    • 能够在类层次结构中进行向上转换,而不允许其他转换。

      dynamic_cast <type_name> (expression) // 判断expression是否可以转换成为 type_name 类型
      
  • const_cast

    • 用于执行只有一种用途的类型转换,即改变值为const或者volatile

      。语法和 dynamic_cast相同

      // 语法格式
      const_cast <type_name> (expression) // 删除const属性,使变成可修改对象
      // 示例
      High bar;
      const High *pbar = &bar;
      High *pb = const_cast <High *> (pbar); // 让*pb成为一个用于修改bar对象值的指针。删除const属性
      
    • 提供该运算符的原因:有时候可能需要这样一个值,它在大多数时候是常量,但有些时候又需要进行修改。在这种情况下,可以将其先声明为const,后面在需要修改它的时候,再使用const_cast。

    • const_cast不是万能的,下面看一个例子:

    #include <iostream>
    using std::endl;
    using std::cout;
    using std::cin;
    
    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;
        return 0;
    }
    
    
    void change(const int *pt,int n){
        int *pc;
        pc = const_cast<int *>(pt);
        *pc += n;
    }
    
    输出:
    pop1、pop2: 38383, 2000
    pop1、pop2: 38280, 2000
    

    程序说明:const_cast运算符可以删除const int *pt中的const,使得编译器能够接受change()函数。指针pt删除了pt的const的特征,因此可以用来修改指向的值,前提是指向的值不是const。因此pop1可修改,pop2不可修改。

    我们怎么样才可以同时修改两个值呢?

    ✅在const后加个volatile关键字,再配合const_cast进行强制转换。

    贴个链接:https://blog.csdn.net/qq_38877888/article/details/114190548

    上面的链接将编译器如何处理被const修饰的值的。

  • static_cast

    • type_name 和 expression互相隐式转换为其所属的类型时,转换才合法,否则将出错。

      static_cast <type_name> (expression)
      
    • 假设High类是Low类的基类,而Pond是一个无关的类,则从High到Low(向下转换)、Low到High(向上转换)的转换都是合法的,而从Low到Pond的转换是不允许的。

    • static_cast无需进行类型转换就可以进行另一个方向的转换,所以其向下转换也是合法的,

    • 也可以将枚举值转换为整型,将整型转换为枚举值……

  • reinterpret_cast

    • 转换适用于依赖于实现的底层编程技术,具有不可移植性。

    • 不做任何处理,也不能删除const属性

    • 不支持所有的类型转换

      • 可以将指针类型转换为足以存储指针表示的类型,但不能将指针转换为更小的整型或者浮点型。
      • 不能将函数指针转换为数据指针。
      reinterpret_cast <type_name> (expression)
      

      下面类型在C语言是合理的,但在C++是不允许的,因为char类型太小了,不能存储指针。

      char ch = char (&d);		//convert address to a char
      
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值