# C++基础15-友元和异常

本文是《C++ Primer Plus》的笔记,本文中的案例均自己实践过,如需转发请在转发开头贴上原文地址,谢谢

# 友元

可以将类作为另一个类的友元,这样友元类可以访问原始类的私有成员和保护成员。当然也可以做更严格的限制,只把特定的成员函数指定为另一个类的友元。而哪些类或成员函数为友元是由类定义的,因此友元被授予从外部访问类的私有部分的权限,但是它们并不与面向对象的编程思想违背,还提高了公有接口的灵活性。

# 友元类

在什么场景中倾向于使用友元类呢?比如电视机和遥控器,他们肯定不是is-a关系,也不是has-a关系,但是他们确实相互联系,遥控器可以改变电视机的状态,电视机自身虽然带调节目,调音量,但是在节目之间切换显然没有遥控器更方便,电视机的节目切换和音量控制可以被遥控器复用,此时用友元类就比较合适了,把遥控器作为电视机的友元类,让遥控器能复用电视机的方法。看实例:

Television.h 文件内容:
#pragma once
class Television
{
    friend class RemoteControl;
public:
    enum TVState {on,off};
private :
    int volume;
    int channel;
    int Mode;
    TVState State;
    void ChangeMode();
    void ChangeState();

public:
    Television();
    ~Television();
    void VolUp();
    void VolDown();
    void ChannelLeft();
    void ChannelRight();
};

Television.cpp 文件内容:
#include "pch.h"
#include "Television.h"


Television::Television()
{
    volume = 0;
    channel = 0;
    Mode = 0;
    State = on;
    cout << "generate a tv" << endl;
}


Television::~Television()
{
    cout << "delete a tv" << endl;
}

void Television::VolUp()
{
    volume++;
    cout << "volume up: "<< volume << endl;
}

void Television::VolDown()
{
    volume--;
    cout << "volume down: "<< volume << endl;
}

void Television::ChannelLeft()
{
    channel++;
    cout << "changed to channel:" << channel << endl;
}

void Television::ChannelRight()
{
    channel--;
    cout << "changed to channel:" << channel << endl;
}

void Television::ChangeMode()
{
    Mode ^= 1;
    cout << "Mode is:" << Mode << endl;
}

void Television::ChangeState()
{
    State = State == on ? off : on;
    cout << "State is :" << State << endl;
}

RemoteControl.h 文件内容:
#pragma once
#include "Television.h"
class RemoteControl
{
private:
    int mode;
public:
    Television tv;
    RemoteControl(Television & tvT) :tv(tvT){}
    ~RemoteControl();

    void ChangeTVState();
    void ChangeTVMode();
};

RemoteControl.cpp 文件内容:
#include "pch.h"
#include "RemoteControl.h"

RemoteControl::~RemoteControl()
{
}

void RemoteControl::ChangeTVState()
{
    tv.ChangeState();
}

void RemoteControl::ChangeTVMode()
{
    tv.ChangeMode();
}

main 函数中:
int main()
{
    Television t1;
    RemoteControl rc(t1);
    rc.ChangeTVMode();
    rc.ChangeTVMode();
    rc.ChangeTVMode();
    rc.ChangeTVState();
    rc.ChangeTVState();
    rc.ChangeTVState();
    //rc.tv.ChangeMode();	//不可访问
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128

从上面发现,RemoteControl能够访问Television的私有方法,这可以在不改变Television私有规则的前提下复用它的方法。

# 友元成员函数

让一整个类访问本类可能会有些勉强在有些时候,只让某个类的某个方法来访问本类可能更好点,这便是友元成员函数的应用场景。

Television.h 文件内容:

#pragma once
class Television
{
//	friend class RemoteControl;
    friend void RemoteControl::ChangeTVMode(Television & tvT);
    friend void RemoteControl::ChangeTVState(Television & tvT);
public:
    enum TVState {on,off};
private :
    int volume;
    int channel;
    int Mode;
    TVState State;
    void ChangeMode();
    void ChangeState();

public:
    Television();
    ~Television();
    void VolUp();
    void VolDown();
    void ChannelLeft();
    void ChannelRight();
};

RemoteControl.h 文件内容:

#pragma once

class Television;

class RemoteControl
{
private:
    int mode;
public:
    RemoteControl(Television & tvT);
    ~RemoteControl();

    void ChangeTVState(Television & tvT);
    void ChangeTVMode(Television & tvT);
};

#include "Television.h"

Television.cpp 文件内容:

#include "pch.h"
#include "RemoteControl.h"


Television::Television()
{
    volume = 0;
    channel = 0;
    Mode = 0;
    State = on;
    cout << "generate a tv" << endl;
}


Television::~Television()
{
    cout << "delete a tv" << endl;
}

void Television::VolUp()
{
    volume++;
    cout << "volume up: "<< volume << endl;
}

void Television::VolDown()
{
    volume--;
    cout << "volume down: "<< volume << endl;
}

void Television::ChannelLeft()
{
    channel++;
    cout << "changed to channel:" << channel << endl;
}

void Television::ChannelRight()
{
    channel--;
    cout << "changed to channel:" << channel << endl;
}

void Television::ChangeMode()
{
    Mode ^= 1;
    cout << "Mode is:" << Mode << endl;
}

void Television::ChangeState()
{
    State = State == on ? off : on;
    cout << "State is :" << State << endl;
}

RemoteControl.cpp 文件内容:
#include "pch.h"
#include "RemoteControl.h"



RemoteControl::RemoteControl(Television & tvT){
}

RemoteControl::~RemoteControl()
{
}

void RemoteControl::ChangeTVState(Television & tvT)
{
    tvT.ChangeState();
}

void RemoteControl::ChangeTVMode(Television & tvT)
{
    tvT.ChangeMode();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126

注意使用友元成员函数时,类的声明定义的顺序很重要。Television中用到了RemoteControl里的成员函数,所以Television的定义应该在RemoteControl后面。但是RemoteControl也中使用了Television对象,这就会导致循环依赖的定义,解决这种情况是使用前向声明(forward declaration)。

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

# 其他友元关系

现在有另一个场景,比如要在遥控机在电视节目里交互,要回答某个问题,如果回答错误,遥控会出现震动。这个情况可以让Television也是RemoteControl的友元。也就是他们相互称为对方的友元函数。

还有一种情景就是函数需要访问两个类的私有数据。从逻辑上讲,这样的函数应该是每个类的成员函数,但是不可能。它可以是一个类的成员,同时是另一个类的友元,但是将函数作为两个类的友元可能会更合理。

# 嵌套类

在另一个类中声明的类被称为嵌套类(nested class),它通过提供新的类作用域来避免名称混乱。对类进行嵌套和包含并不同,包含意味着将类对象作为另一个类的成员,而嵌套定义了一种类型,该类型仅在包含的类中有效。

嵌套类同其他成员一样,在类的不同部分声明(公有,私有,保护),被访问的级别是一样的。嵌套类是某个类内部实现的一项特性,对外部世界是不可见的。类对嵌套类的访问控制规则与常规类相同,没有特权,相应的嵌套类访问类也没有特权。看实例:

Animal.h 文件内容:

#pragma once
class Animal
{
private:
    int age;
public:
    Animal();
    ~Animal();
    class Bird {
    private:
        int id;
        static int num;
    public:
        Bird();
        Bird(int idT);
        ~Bird();
    };
    Bird birdT;
};

Animal.cpp 文件内容:

#include "pch.h"
#include "Animal.h"


Animal::Animal()
{
}


Animal::~Animal()
{
}

Animal::Bird::Bird()
{
    cout << "generate a bird!" << endl;
}

Animal::Bird::Bird(int idT):id(idT)
{
    cout << "id :" << idT << endl;
}

Animal::Bird::~Bird()
{
}

main 函数内容:
int main()
{
    Animal anim1;
    Animal::Bird anim(10);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

# 嵌套类与模板

将上面的实例改成模板:

Animal.h 文件内容:
#pragma once
template<typename T>
class Animal
{
private:
    T age;
public:
    Animal();
    ~Animal();
    class Bird {
    private:
        T id;
        static int num;
    public:
        Bird();
        Bird(T idT);
        ~Bird();
    };
    Bird birdT;
};

template<typename T>
Animal<T>::Animal()
{
}


template<typename T>
Animal<T>::~Animal()
{
}

template<typename T>
Animal<T>::Bird::Bird()
{
    cout << "generate a bird!" << endl;
}

template<typename T>
Animal<T>::Bird::Bird(T idT):id(idT)
{
    cout << "id :" << idT << endl;
}

template<typename T>
Animal<T>::Bird::~Bird()
{
}

main 函数内容:

int main()
{
    Animal<int> anim1;
    Animal<float>::Bird anim(10);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

# 异常

程序有时会遇到运行阶段错误,导致程序无法正常运行,例如,程序试图打开一个不可用的文件,分母为0。为了防止这些意外情况,C++异常提供了一种功能强大而灵活的工具。看下处理异常的几种方法:

int main()
{
    int a =1,b=0;
    //方法一:
    if (b == 0) {
        abort();
    }
    //方法二:
    if (b == 0) {
        exit(10);
    }
    a = a / b;
}
1
2
3
4
5
6
7
8
9
10
11
12
13

Abort最典型的实现是向标准错误流发送abnormal program termination(程序异常终止),然后终止程序,还返回一个值告诉操作系统处理失败,但是它不一定刷新文件缓冲区。而exit刷新文件缓冲区,但不显示消息。

除了使用异常,还可以让函数返回值来指出问题,之前返回的值让指针参数返回,函数返回状态来表明操作的状态。

# 异常机制

C++异常是对程序运行过程中发生的异常情况的一种响应,对引发异常的代码,使用处理程序(exception handler)捕获异常。来看实例:

    int a =1,b=0;

    try {
        if (b == 0)throw "b is zero as denominator!";
        a = a / b;
    }
    catch (const char * s) {
        cout << s << endl;
    }
1
2
3
4
5
6
7
8
9

执行throw语句类似于执行return语句,因为它将终止函数的执行;但throw不是将控制权返回给调用程序,而是导致程序沿函数调用序列后退,直到找到包含try块的函数。catch语句有点类似函数定义,但并不是,它表明这是一个处理程序,而const char * 表明该处理程序与字符串异常匹配。

执行完try块中的语句后,如果没有引发任何异常,则跳过try块后面的catch块,直接执行处理程序后面的第一条语句。如果引发了异常,但该代码不在try块中,默认情况下程序最终调用abort()函数。

# 对象作为异常类型

通常,引发异常的函数将传递一个对象,这样做的好处是可以使用不同的异常类型来区分不同的函数在不同的情况下引发的异常。对象可以携带信息,后面的catch块可以根据这些信息来确定引发异常的原因,并采取相应的措施。看如下实例:

TypeError.h 文件内容:

#pragma once
class TypeError
{
    string msgError;
public:
    template<typename T, typename U>
    TypeError(T v1,U v2);
    ~TypeError();
    void GetError();
};


template<typename T, typename U>
TypeError::TypeError(T v1, U v2)
{
    if (typeid(v1) != typeid(v2)) {
        msgError = "two number type is not correct!";
    }
}

TypeError.cpp 文件内容:

#include "pch.h"
#include "TypeError.h"

TypeError::~TypeError()
{
}

void TypeError::GetError()
{
    cout << msgError << endl;
}

ValueError.h 文件内容:

#pragma once
class ValueError
{
    string msgError;
public:
    ValueError();
    ValueError(int a,int b);
    ~ValueError();
    void GetError();

};

ValueError.cpp 文件内容:

#include "pch.h"
#include "ValueError.h"


ValueError::ValueError()
{
}

ValueError::ValueError(int a, int b)
{
    if (b == 0) {
        msgError = "b is zero as denominator!";
    }
}


ValueError::~ValueError()
{
}

void ValueError::GetError()
{
    cout << msgError << endl;
}

main 函数内容:

int main()
{
    int v1,v2;
    double v3;

    while (cin >> v1 >> v2) {
        try {
            if (v2 == 0)throw ValueError(v1,v2);
            v1 = v1 / v2;
            cout << "value:" << v1 << endl;
            break;
        }
        catch (ValueError & e1) {
            e1.GetError();
        }
    }
    cout << "Test type error!" << endl;

    while (cin >> v1 >> v3) {
        try {
            throw TypeError(v1,v3);
            v1 = v1 / v3;
            break;
        }
        catch (TypeError & e1) {
            e1.GetError();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108

# 异常规范

异常规范(exception specification)是C++98增加的一项功能,但在C++11中摒弃了。它的形式如下:

void TestFunc() throw(TypeError);
void TestFunc() noexcept;
1
2

异常规范的作用一是告诉用户这个函数可能会抛出异常,但是这个用注释也能替代,作用二是让编译器添加运行阶段检查的代码,检查是否违反异常规范,这很难检查,比如这个函数本身可能不会引发异常,但是它调用的一个函数引发了异常。但是C++11支持一种特殊的异常规范,使用关键字noexcept指出函数不会引发异常。

# 栈解退

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

C++调用函数的指令压到栈中,被调用函数的参数压到栈中,在函数中创建的自动变量被压到栈中,当函数结束时,程序流程将跳到该函数被调用时存储的地址处,同时栈顶元素被释放。先假设函数由于出现异常而终止,则程序将释放栈中的内存,但不会释放栈的第一个返回地址后停止,而是继续释放栈,直到找到一个位于try块中的返回地址。随后,控制权将转到异常处理程序,而不是函数调用后面的第一条语句。这个过程叫做栈解退。看如下实例:

int DivideTwo(int v1, int v2) {

if (v2 == 0)throw ValueError(v1,v2);
    return v1 / v2;

}

void DealTwo() {
    int v1,v2;
    while (cin >> v1 >> v2) {
            cout << "value:" << DivideTwo(v1,v2) << endl;
            cout << "DealTwo next oper!" << endl;
        //try {
        //cout << "value:" << DivideTwo(v1,v2) << endl;
        //break;
        //}
        //catch (ValueError & e1) {
        //e1.GetError();
        //cout << "DealTwo inner oper!" << endl;
        //}
    }
}
int main()
{
    try{
        DealTwo();
        cout << "Main func next oper!" << endl;
    }
    catch (ValueError & e1) {
        e1.GetError();
        cout << "main function inner!" << endl;
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

如果把while内的两条语句注释掉,而之前注释的代码块取消注释,则在DealTwo内捕获异常并进行处理。

# 异常其他特性

先看实例:

Exception.h 文件内容:
#pragma once
class ExceptionA
{
public:
    ExceptionA();
    ExceptionA(const ExceptionA &);
    ~ExceptionA();
    void OutputInfo();
};

class ExceptionB :
    public ExceptionA
{
public:
    ExceptionB();
    ExceptionB(const ExceptionB &);
    ~ExceptionB();
    void OutputInfo();
};

class ExceptionC :
    public ExceptionB
{
public:
    ExceptionC();
    ExceptionC(const ExceptionC &);
    ~ExceptionC();
    void OutputInfo();
};

Exception.cpp 文件内容:

#include "pch.h"
#include "Exception.h"

ExceptionA::ExceptionA()
{
    cout << "exception A is generated!" << endl;
}

ExceptionA::ExceptionA(const ExceptionA &)
{
    cout << "exception A is copied!" << endl;
}


ExceptionA::~ExceptionA()
{
    cout << "exception A is destroyed!" << endl;
}

void ExceptionA::OutputInfo()
{
    cout << "exception A is throwed!" << endl;
}

ExceptionB::ExceptionB()
{
    cout << "exception B is generated!" << endl;
}

ExceptionB::ExceptionB(const ExceptionB &)
{
    cout << "exception B is copied!" << endl;
}


ExceptionB::~ExceptionB()
{
    cout << "exception B is destroyed!" << endl;
}

void ExceptionB::OutputInfo()
{
    cout << "exception B is throwed!" << endl;
}


ExceptionC::ExceptionC()
{
    cout << "exception C is generated!" << endl;
}

ExceptionC::ExceptionC(const ExceptionC &)
{
    cout << "exception C is copied!" << endl;
}


ExceptionC::~ExceptionC()
{
    cout << "exception C is destroyed!" << endl;
}

void ExceptionC::OutputInfo()
{
    cout << "exception C is throwed!" << endl;
}

Main 函数内容:

int main()
{
    int a = 0;
    while (cin >> a) {
        try
        {
            if (a == 1)throw ExceptionA();
            if (a == 2)throw ExceptionB();
            if (a == 3)throw ExceptionC();
        }
        catch (ExceptionC & e)
        {
            e.OutputInfo();
        }
        catch (ExceptionB & e)
        {
            e.OutputInfo();
        }
        catch (ExceptionA & e)
        {
            e.OutputInfo();
        }
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127

注意,在代码中使用引用一是节省性能,避免异常对象被复制,二因为基类可以引用派生类,假设有一组通过继承关联起来的异常类型,在异常规范中只需列一个基类引用,它将与任何派生类对象匹配。当同时存在派生类对象引用和自身对象引用时,会匹配最契合的那个,如果没有自身对象引用,那么会选择它的基类对象引用捕获。

# exception类

C++异常的目的是为设计容错程序提供语言级支持,即异常使得在程序设计中包含错误处理功能更容易。异常的灵活性和相对方便性激励着程序员在条件允许的情况下,在程序设计中加入错误处理功能。较新的C++编译器将异常合并到语言中,exception头文件定义了exception类,可以把它用作其他异常类的基类,它定义了一个what的虚方法,可以在派生类中重新定义。看下面实例:

MyException.h 文件内容:

#pragma once
#include <exception>

class MyException :public std::exception {
public:
    const char *  what() const noexcept {
        return "This is My exception";
    }
};

Main 函数内容:

int main()
{
    int t = 0;
    try {
        if (t == 0)throw MyException();
    }
    catch (std::exception & e) {
        cout << e.what() << endl;
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# stdexcept异常类

头文件stdexcept定义了其他几个异常类,比如logic_error和runtime_error,它们都以公有方式从exception派生而来,这些类构造函数接受一个string对象作为参数,然后方法what()以C-风格字符串方式返回这些数据。

以logic_error为基类的派生类描述了典型的逻辑错误,通过合理的编程是可以避免这些错误的。比如domain_error,当一个函数参数不在其定义域上时触发。invalid_argument,指出给函数传递了一个意料外的值。length_error,没有足够空间执行所需操作时触发。out_of_bounds,对一个数组使用索引无效时触发。

以runtime_error为基类的派生类描述了可能在运行期间发生难以预计和防范的错误。比如underflow_error,浮点型数据比较容易出现下溢,即计算数值比要能表示的最小非零值还小时导致下溢错误。overflow_error,当数据比要能表示的最大值还大时出现上溢。range_error,当计算结果不在函数允许范围内,但是没有引发上溢或者下溢时,触发该异常。

# bad_alloc异常

对于new导致的内存分配问题,C++让new引发bad_alloc异常。看实例:

int main()
{
    struct Big
    {
        double stuff[20000];
    };
    Big * pb;
    try {
        pb = new Big[10000];
    }
    catch (bad_alloc & e) {
        cout << e.what() << endl;
        exit(EXIT_FAILURE);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 空指针和new

很多代码在都new在失败返回空指针时编写的。为了处理这种情况,有些编译器提供了一个选择,即让这种情况不抛出异常而自行处理。将上面实例改造下:

int main()
{
    struct Big
    {
        double stuff[20000];
    };
    Big * pb;
    try {
        pb = new (std::nothrow) Big[10000];
        if (pb == 0) {
            cout << "Hi,cannot allocate memory!" << endl;
            exit(EXIT_FAILURE);
        }
    }
    catch (bad_alloc & e) {
        cout << e.what() << endl;
        exit(EXIT_FAILURE);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 异常和继承

以一个普通整数数组和一个Double数组为例,当超出下标时抛出相应的异常,注意他们以类的方式继承方式。

NumSet.h 文件内容:

#pragma once
#include <exception>
#define MaxIndex 10
class NumSet
{
private:
    int num[MaxIndex];
public:
    class NumOutOfBound :public std::logic_error {
    int outBoundIndex;
    public:
        NumOutOfBound(int index, const char * msg);
        int GetIndex() const {
            return outBoundIndex;
        }
        ~NumOutOfBound();
    };
    NumSet();
    ~NumSet();
    int operator[](int i);
};


class DoubleSet:NumSet
{
private :
    double doubleNum[MaxIndex];
public:
    class DoubleOutOfBound :public NumSet::NumOutOfBound{
    public:
        DoubleOutOfBound(int index, const char * msg);
        ~DoubleOutOfBound();
    };

    DoubleSet();
    ~DoubleSet();
    double operator[](int i);
};


NumSet.cpp 文件内容:

#include "pch.h"
#include "NumSet.h"

NumSet::NumOutOfBound::NumOutOfBound(int index, const char * msg = "NumSet out of num bound"):logic_error(msg),outBoundIndex(index)
{
    cout << "Num out of bound is called!" << endl;
}

NumSet::NumOutOfBound::~NumOutOfBound()
{
}


NumSet::NumSet()
{
    for (int i = 0; i < MaxIndex; i++) {
        num[i] = i;
    }
}


NumSet::~NumSet()
{
}

int NumSet::operator[](int i)
{
    if (i < 0 || i >= MaxIndex) {
        throw NumOutOfBound(i);
    }
    return num[i];
}

DoubleSet::DoubleSet()
{
    for (int i = 0; i < MaxIndex; i++) {
        doubleNum[i] = i*3;
    }

}

DoubleSet::~DoubleSet()
{
}


DoubleSet::DoubleOutOfBound::DoubleOutOfBound(int index, const char * msg  = "DoubleSet out of num bound"):NumSet::NumOutOfBound(index,msg){
    cout << "Double out of bound is called!" << endl;
}

DoubleSet::DoubleOutOfBound::~DoubleOutOfBound()
{
}

double DoubleSet::operator[](int i)
{
    if (i < 0 || i >= MaxIndex) {
        throw DoubleOutOfBound(i);
    }
    return doubleNum[i];
}


Main 函数内容:

int main()
{
    try {
        NumSet s1;
        DoubleSet d1;
        cout << s1[12] << endl;
//cout << d1[13] << endl;
    }
    catch (DoubleSet::DoubleOutOfBound & e) {
        cout << "Out Of Bound Double index :" << e.GetIndex() << endl;
    }
    catch (NumSet::NumOutOfBound & e) {
        cout << "Out Of Bound Num index :" << e.GetIndex() << endl;
    }

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126

# 异常注意事项

异常被引发后,在两种情况下会导致问题。其一是在带异常规范的函数中引发的,则必须与规范列表中某种异常匹配(在继承中,指定对象类型与这个类及其派生类对象匹配),否则称为意外异常(unexpected exception)。其二是如果异常不是在函数中发出的(或函数没有异常规范),必须捕获它,如果没有被捕获(没有在try块或没有匹配的catch块),则异常被称为未捕获异常(uncaught exception)。在默认情况下这两种都将导致程序异常终止,但是我们可以修改对意外异常和未捕获异常的反应。

未捕获异常不会导致程序立刻异常终止,程序会先调用函数terminate(),默认情况下,terminate()调用abort(),我们可以修改terminate()的执行内容比如不调用abort(),来改变这种行为。看如下示例:

void CustomQuit() {
    cout << "Terminating with my self uncaught exception quit!" << endl;
    exit(5);
}

int main()
{
    set_terminate(CustomQuit);
    throw "Test My Self Quit!";
}
1
2
3
4
5
6
7
8
9
10

意外异常与未捕获异常及其相似,当发生意外异常时,程序调用unexpected()函数,这个函数调用terminate(),默认情况下它调用abort()。如下面示例,注意该代码在C++ 17中未能运行如期结果,不知道在C++11中运行结果怎样。

void CustomQuit() {
    cout << "Terminating with my self quit!" << endl;
    exit(5);
}

void TestUnexpectedException() throw(std::exception & ) {
    throw "Hi,Test unexpected exception!";
}

int main()
{
    //set_terminate(CustomQuit);
    set_unexpected(CustomQuit);
    try {
        TestUnexpectedException();
    }
    catch (const char * e) {
        cout << e << endl;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

异常还有一些缺点,比如会增加程序代码,降低程序运行速度。异常规范不适用于模板,因为模板函数引发的异常可能随特定的具体化而异。异常和动态内存分配并非总能协同工作。看下面实例:

void testException() {
    char * ch = new char[20];
    throw "hi,world!";
    delete[] ch;
}

int main()
{
    try {
        testException();
    }
    catch (const char * e) {
        cout << e << endl;
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

由于引发了异常,不会执行delete,由于退出函数,ch指针消失了,但是它所指向的内存块未释放,这些内存被泄露了。避免出现这种情况可以使用智能指针模板,或者在catch块中释放指针,但是这增大了编程的工作量。

# RTTI

RTTI是运行阶段类型识别(Runtime Type Identification)的简称。这是C++的新特性之一,有一些编译器可能会对该特性有开关设置,RTTI旨在为程序在运行阶段确定对象的类型提供一种标准方式。很多类库已经为其类对象提供了实现这种功能的方式,但由于C++内部并不支持,因此各个厂商的机制通常不兼容,希望能创建一种RTTI语言标准使得未来的库能彼此兼容。

# RTTI的实现

  1. dynamic_cast 是用一个指向基类的指针来生成一个指向派生类的指针,否则运算符返回0(空指针)。
  2. typeid 返回指出对象类型的值。
  3. type_info 结构存储了有关特定类型的信息。

# dynamic_cast

这个运算符是最常用的RTTI组件,它不能回答"指针指向的是哪类对象"这样的问题,但是能够回答"是否可以安全地将对象的地址赋给特定类型的指针"这样的问题。看下面的举例:

ClassA.h 文件内容:

#pragma once
class ClassA
{
public:
    ClassA();
    ~ClassA();
    virtual void GetValue();
};

class ClassB:public ClassA
{
public:
    ClassB();
    ~ClassB();
    virtual void GetValue();
};

class ClassC:public ClassB
{
public:
    ClassC();
    ~ClassC();
    virtual void GetValue();
};

ClassA.cpp 文件内容:

#include "pch.h"
#include "ClassA.h"


ClassA::ClassA()
{
}


ClassA::~ClassA()
{
}

void ClassA::GetValue()
{
    cout << "this is class A" << endl;
}

ClassB::ClassB()
{
}

ClassB::~ClassB()
{
}

void ClassB::GetValue()
{
    cout << "this is class B" << endl;
}

ClassC::ClassC()
{
}

ClassC::~ClassC()
{
}

void ClassC::GetValue()
{
    cout << "this is class C" << endl;
}

Main 函数内容:

template<typename T>
void GetValue(T * pt) {
    if (pt)pt->GetValue();
    else cout << "object is null" << endl;
}

int main()
{
    ClassA * pa= new ClassA;
    ClassA * pb= new ClassB;
    ClassA * pc= new ClassC;

    ClassC * p1 = dynamic_cast<ClassC *>(pc);
    ClassC * p2 = dynamic_cast<ClassC *>(pa);
    ClassB * p3 = dynamic_cast<ClassC *>(pc);
    GetValue(p1);
    GetValue(p2);
    //如果转换失败,则其值为NULL。
    GetValue(p3);

    ClassA pd;
    try {
        ClassC & p2 = dynamic_cast<ClassC &>(pd);
    }
    catch(bad_cast & e){
        cout << "cast ref error!" << endl;
        cout << e.what() << endl;
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105

dynamic_cast 用于引用时和指针有点不同,没有空引用值,因此无法使用特殊的引用值来指示失败。当请求不正确时,dynamic_cast将引发类型为bad_cast异常,它是从exception类派生而来,在头文件typeinfo中定义的。

# typeid运算符

typeid运算符使得能够确定两个对象是否为同种类型,它返回一个type_info对象的引用。其中type_info在头文件typeinfo中定义,它重载了==和!=运算符。typeid如果遇到空指针,则会引发bad_typeid异常,该异常从exception类派生,是在typeinfo中声明。type_info类包含一个name()成员,该函数返回其自身类型的字符串。接着上面的例子来继续看:

    try {
        if (typeid(ClassB) == typeid(*pb)) {
        cout << "pb is ClassB" << endl;
        }
        if (typeid(ClassB) == typeid(*p2)) {
        cout << "Test Exception" << endl;
        }
    }
    catch(bad_typeid & e){
        cout << "typeid error!" << endl;
        cout << e.what() << endl;
    }
1
2
3
4
5
6
7
8
9
10
11
12

注意应该避免上面这种*pb的typeid的使用,假如p2是ClassC类型的,那还要再加一个是否等于ClassC,这是有很大问题的,所以尽量使用dynamic_cast。

# 类型转换符

C++的类型转换太过随意,比如

char * a = (char *)pa;
1

虽然看上去毫无意义,但是在C++中都是允许的。为了更严格的限制允许的类型转换,添加了四个类型转换运算符,是转换过程更规范。

  1. dynamic_cast 该运算符使得能够在类层次结构中进行向上转换,而不允许其他转换。在前面中也有相应实例。

  2. const_cast 改变变量的const或volatile标识符,比如将const转换为非const。其格式为

    const_cast(expression)

    注意type_name和expression的类型必须相同。提供该运算符的原因是,有时候需要一个值,它大多数时候是常量,但是又可以是修改的。

  3. static_cast 它的语法同上,仅当type_name可被隐式转换为expression所属类型或expression可被隐式转换为type_name类型时,才进行转换。比如High是Low的基类,而Money是一个无关的类,从High到Low或从Low到High都是合法的,但是从Low到Money是不合法的。

  4. reinterpret_cast 用于比较危险的转换,它不允许删除const,但是会做一些依赖语法实现的操作。比如

    struct date { short a; short b; };
    long value = 0xA118B120;
    date * pd = reinterpret_cast<date *>(&value);
    cout << hex << pd->a;
    
    1
    2
    3
    4

    reinterprete_cast并不支持所有的类型转换,可以将指针类型转换为足以存储指针表示的整形,但不能转换为更小的整形。不能将函数指针转换为数据指针。