C++ 进阶基础之三

大纲

类型转换

类型转换的分类

静态类型转换

  • static_cast 可以用于类层次结构中基类(父类)和派生类(子类)之间指针或者引用的转换。
    • 进行上行转换(将派生类的指针或者引用转换成基类表示)是安全的。
    • 进行下行转换(将基类的指针或者引用转换成派生类表示),由于没有动态类型检查,所以是不安全的。
  • static_cast 可以用于基本数据类型之间的转换,比如将 int 转化成 char,或者将 char 转换成 int,这种类型转换的安全性需要开发人员来保证。

动态类型转换

  • dynamic_cast 可以用于类层次结构中的上行转换和下行转换,但是不支持基本数据类型的转换。
  • 在类层次结构中进行上行转换(将派生类的指针或者引用转换成基类表示)时,dynamic_caststatic_cast 的效果一样。
  • 在类层次结构中进行下行转换(将基类的指针或者引用转换成派生类表示)时,dynamic_cast 具有类型检查的功能,比 static_cast 更安全。

常量类型转换

  • const_cast 可以用于赋予或者去除类型的 const 只读属性。
    • 常量指针被转换成非常量指针后,仍然指向原来的对象,反之亦然。
    • 常量引用被转换成非常量引用后,仍然指向原来的对象,反之亦然。

特别注意

不能直接对非指针和非引用的类型使用 const_cast 操作符去直接赋予或者去除它的 const 只读属性。

重新解释类型

  • reinterpret_cast 用于不同类型之间进行强制类型转换,这是最不安全的一种类型转换机制,最可能出现问题,极少使用。
  • reinterpret_cast 可以将一种数据类型强制转换成另一种数据类型,比如可以将一个指针转换成整数,也可以将整数转换成指针。

类型转换的语法

  • C 语言风格的强制类型转换(Type Cast)很简单,不管什么类型的转换,语法都是:TYPE b = (TYPE) a

  • C++ 风格的类型转换,提供了 4 种类型转换操作符来应对不同的应用场景

    • const_cast:常量类型转换,用于赋予或者去除类型的 const 只读属性
    • reinterpreter_cast:重新解释类型(强制类型转换)
    • static_cast:静态类型转换,如 int 转换成 char
    • dynamic_cast:动态类型转换,如父类和子类之间的多态类型转换
  • C++ 4 种类型转换的语法

    • TYPE b = const_cast<TYPE> (a)
    • TYPE b = static_cast<TYPE> (a)
    • TYPE b = dynamic_cast<TYPE> (a)
    • TYPE b = reinterpreter_cast<TYPE> (a)

类型转换的使用

静态类型转换的使用

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
#include <iostream>

using namespace std;

class Father {

public:
Father(string name, int age) {
this->m_Name = name;
this->m_Age = age;
}

// 虚函数
virtual void print() {
cout << endl << "name = " << this->m_Name << ", age = " << this->m_Age << endl;
}

protected:
string m_Name;
int m_Age;

};

class Son : public Father {

public:
Son(string name, int age, string hobby) : Father(name, age) {
this->m_Name = name;
this->m_Age = age;
this->m_hobby = hobby;
}

void print() override {
cout << endl << "name = " << this->m_Name << ", age = " << this->m_Age << ", hobby = " << m_hobby << endl;
}

private:
string m_hobby;

};

// 基本数据类型的转换
void test01() {
char a = 'a';
double d = static_cast<double>(a);
cout << endl << d << endl;
}

// 继承关系引用互相转换
void test02() {
Father father("Father", 60);
Son son("Son", 25, "Game");

// 上行转换(将派生类的引用转换成基类表示),是安全的
Father &father1 = static_cast<Father &>(son);
father1.print(); // 发生多态

// 下行转换(将基类的引用转换成派生类表示),是不安全的
Son &son2 = static_cast<Son &>(father);
son2.print();
}

// 继承关系指针互相转换
void test03() {
// 上行转换(将派生类的指针转换成基类表示),是安全的
Son *son1 = new Son("Son", 25, "Game");
Father *father1 = static_cast<Father *>(son1);
father1->print(); // 发生多态

// 下行转换(将基类的指针转换成派生类表示),是不安全的
Father *father2 = new Father("Father", 60);
Son *son2 = static_cast<Son *>(father2);
son2->print();
}

int main() {
cout << endl << "-------- test01() --------";
test01();

cout << endl << "-------- test02() --------";
test02();

cout << endl << "-------- test03() --------";
test03();
return 0;
}

程序运行输出的结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
-------- test01() --------
97

-------- test02() --------
name = Son, age = 25, hobby = Game

name = Father, age = 60

-------- test03() --------
name = Son, age = 25, hobby = Game

name = Father, age = 60

动态类型转换的使用

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
#include <iostream>

using namespace std;

class Father {

public:
Father(string name, int age) {
this->m_Name = name;
this->m_Age = age;
}

// 虚函数
virtual void print() {
cout << endl << "name = " << this->m_Name << ", age = " << this->m_Age << endl;
}

protected:
string m_Name;
int m_Age;

};

class Son : public Father {

public:
Son(string name, int age, string hobby) : Father(name, age) {
this->m_Name = name;
this->m_Age = age;
this->m_hobby = hobby;
}

void print() override {
cout << endl << "name = " << this->m_Name << ", age = " << this->m_Age << ", hobby = " << m_hobby << endl;
}

private:
string m_hobby;

};

// 继承关系引用互相转换
void test01() {
Father father("Father", 60);
Son son("Son", 25, "Game");

// 上行转换(将派生类的引用转换成基类表示),是安全的
Father &father1 = dynamic_cast<Father &>(son);
father1.print();

// 下行转换(将基类的引用转换成派生类表示),是不安全的,无法进行类型转换(编译通过,但运行出错)
// Son &son2 = dynamic_cast<Son &>(father);
// son2.print();
}

// 继承关系指针互相转换
void test02() {
// 上行转换(将派生类的指针转换成基类表示),是安全的
Son *son1 = new Son("Son", 25, "Game");
Father *father1 = dynamic_cast<Father *>(son1);
father1->print();

// 下行转换(将基类的指针转换成派生类表示),是不安全的,无法进行类型转换(编译通过,但运行出错)
Father *father2 = new Father("Father", 60);
// Son *son2 = dynamic_cast<Son *>(father2);
// son2->print();
}

// 继承关系指针互相转换(发生多态的情况)
void test03() {
// 发生多态
Father *father = new Son("Son", 25, "Game");

// 下行转换(将基类的指针转换成派生类表示),可以进行类型转换
Son *son = dynamic_cast<Son *> (father);
son->print();
}

int main() {
cout << endl << "-------- test01() --------";
test01();

cout << endl << "-------- test02() --------";
test02();

cout << endl << "-------- test03() --------";
test03();

return 0;
}

程序运行输出的结果如下:

1
2
3
4
5
6
7
8
-------- test01() --------
name = Son, age = 25, hobby = Game

-------- test02() --------
name = Son, age = 25, hobby = Game

-------- test03() --------
name = Son, age = 25, hobby = Game

常量类型转换的使用

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
#include <iostream>

using namespace std;

void test01() {
const int *p = nullptr;
// 常量指针被转换成非常量指针
int *newP = const_cast<int *>(p);

int *p2 = nullptr;
// 非常量指针被转换成常量指针
const int *newP2 = const_cast<const int *> (p2);
}

void test02() {
int age = 20;
const int &ageRef = age;
// 常量引用被转换成非常量引用
int &ageRef2 = const_cast<int &>(ageRef);

int num = 10;
int &numRef = num;
// 非常量引用被转换成常量引用
const int &numRef2 = const_cast<const int &>(numRef);
}

void test03() {
const int a = 10;
// 不能直接对非指针和非引用的变量使用 const_cast 操作符去直接赋予或者去除它的 const 只读属性
// int a = const_cast<int>(a);
}

int main() {
test01();
test02();
test03();
return 0;
}

重新解释类型的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>

using namespace std;

void test01() {
int a = 10;
// 将整数强制转换成指针
int *p = reinterpret_cast<int *>(a);
}

int main() {
test01();
return 0;
}

类型转换的综合使用

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
#include <iostream>

using namespace std;

class Tree {

};

class Animal {

public:
virtual void cry() = 0;

};

class Dog : public Animal {

public:
void cry() override {
cout << "dog cry ..." << endl;
}

void watchHome() {
cout << "dog watch home" << endl;
}

};

class Cat : public Animal {

public:
void cry() override {
cout << "cat cry ..." << endl;
}

void playBall() {
cout << "cat play ball ..." << endl;
}

};

void playAnimal(Animal *animal) {
animal->cry();
// 动态类型转换,将父类转换为子类,运行时会做类型检查
Dog *dog = dynamic_cast<Dog *>(animal);
if (dog != NULL) {
dog->watchHome();
}
Cat *cat = dynamic_cast<Cat *>(animal);
if (cat != NULL) {
cat->playBall();
}
}

void printBuf(const char *buf) {
// const_cast 去除变量的 const 只读属性
char *m_buf = const_cast<char *>(buf);
m_buf[0] = 'b';
cout << buf << endl;
cout << m_buf << endl;
}

void printBuf2() {
// 定义指针指向一个常量,这里的常量的内存空间不可以更改
char* buf = "aaaaa";
// const_cast 去除变量的 const 只读属性
char* m_buf = const_cast<char*>(buf);
// 此时若更改指针所指向的内存空间,会带来灾难性的后果
m_buf[0] = 'b';
cout << buf << endl;
cout << m_buf << endl;
}

int main() {
char *p1 = "hello";
double pi = 3.1415926;

// 静态类型转换,编译的时候 C++ 编译器会做类型检查
int num1 = static_cast<int>(pi);
cout << "num1 = " << num1 << endl;

// 静态类型转换,基本类型都能转换,但是不能转换指针类型(多态除外)
// int* p2 = static_cast<int*>(p1); // 错误写法,C++ 编译器编译失败

// 重新解释类型,不同类型之间会进行强制类型转换,包括转换指针类型
int *p2 = reinterpret_cast<int *>(p1);
cout << "p2 = " << p2 << endl;

// 去除变量的 const 只读属性
char buf[] = "aaaaa";
printBuf(buf);
// printBuf2();

// 动态类型转换,基类和派生类之间转换,运行时会做类型检查
Dog dog;
Cat cat;
playAnimal(&dog);
playAnimal(&cat);

// 多态的其他使用场景
Animal *pAnimal = NULL;
pAnimal = &dog;
pAnimal = static_cast<Animal *>(&dog); // 编译通过
pAnimal->cry();
pAnimal = reinterpret_cast<Animal *>(&dog); // 编译通过
pAnimal->cry();

Tree tree;
// pAnimal = static_cast<Animal*>(&tree); // 错误写法,C++ 编译器编译失败
pAnimal = reinterpret_cast<Animal *>(&tree); // 编译通过

return 0;
}

程序运行输出的结果如下:

1
2
3
4
5
6
7
8
9
10
num1 = 3
p2 = 005661B8
baaaa
baaaa
dog cry ...
dog watch home
cat cry ...
cat play ball ...
dog cry ...
dog cry ...

使用总结:

  • 一般情况下,不建议进行类型转换,应该避免进行类型转换
  • 要清楚地知道:要转换的变量,类型转换前是什么类型,类型转换后是什么类型,转换后有什么后果

类型转换的总结

类型转换的概述:

  • (a) const_cast<>():常量类型转换,用于赋予或者去除类型的 const 只读属性
  • (b) reinterpret_cast<>():重新解释类型,不同类型之间会进行强制类型转换
  • (c) dynamic_cast<>():动态类型转换,安全的基类和派生类之间转换,运行时会做类型检查
  • (d) static_cast<>():静态类型转换,编译的时候 C++ 编译器会做类型检查,基本类型都能转换,但是不能转换指针类型(多态除外)

类型转换的总结:

  • (a) 在 C 语言中,不能隐式类型转换的,在 C++ 中可以用 reinterpret_cast<>() 进行强行类型解释
  • (b) 在 C 语言中,能隐式类型转换的,在 C++ 中可用 static_cast<>() 进行类型转换,因为 C++ 编译器在编译的时候,一般都可以顺利通过类型检查
  • (c) static_cast<>()reinterpret_cast<>() 基本上把 C 语言中的强制类型转换功能给覆盖了,但 reinterpret_cast<>() 很难保证代码的移植性

异常处理机制

Bjarne Stroustrup 说:提供异常的基本目的就是为了处理程序运行期间出现的问题。基本思想是让一个函数在出现了自己无法处理的错误时抛出(throw)一个异常,然后它的(直接或者间接)调用者能够处理这个问题。这也就是《C++ Primer》中说的:将问题检测和问题处理相分离。在所有支持异常处理的编程语言中(如 Java),要认识到的一个思想:在异常处理过程中,由问题检测代码可以抛出一个对象给问题处理代码,通过这个对象的类型和内容,实际上完成了两个部分的通信,通信的内容是 “出现了什么错误”,当然,各种语言对异常的具体实现有着或多或少的区别,但是这个通信的思想是不变的。一句话概括:异常处理就是处理程序中的错误。所谓错误是指在程序运行的过程中发生的一些异常事件(如:除 0 溢出、数组下标越界、所要读取的文件不存在、空指针、内存不足等等)。

异常处理的简单介绍

  • 异常的概述:
    • 异常是一种程序控制机制,与函数机制独立和互补。
    • 函数是一种以栈结构展开的上下函数衔接的程序控制系统,而异常是另一种控制结构,它依附于栈结构,却可以同时设置多个异常类型作为捕获条件,从而实现以类型匹配在栈机制中跳跃回馈。
  • 异常设计目的:
    • 异常设计出来之后,却发现在错误处理方面获得了最大的好处。
    • 栈机制是一种高度节律性的控制机制,面向对象编程却要求对象之间有方向、有目的的控制传动,从一开始,异常就是冲着改变程序控制结构,以适应面向对象程序更有效地工作这个主题,而不是仅为了进行错误处理。

异常处理的基本思想

传统错误处理机制

在 C 语言的世界中,对错误的处理总是围绕着两种方法:一是使用整型的返回值标识错误;二是使用 emo 宏(可以简单的理解为一个全局整型变量)去记录错误。当然,C++ 中仍然是可以使用这两种方法的。这两种方法最大的缺陷就是会出现不一致的问题。例如,有些函数返回 1 表示成功,返回 0 表示出错;而有些函数返回 0 表示成功,返回非 0 表示出错。还有一个缺点就是函数的返回值只有一个,通过函数的返回值表示错误代码,那么函数就不能返回其他的值。当然,也可以通过指针或者 C++ 的引用来返回另外的值,但是这样可能会令程序略微晦涩难懂。

异常处理的基本思想

cplus-plus-exception-1

  • 异常跨越了函数,并超脱于函数机制,决定了其对函数的跨越式回跳。
  • C++ 的异常处理机制使得异常的引发和异常的处理不必在同一个函数中,这样底层的函数可以着重解决具体问题,而不必过多的考虑异常的处理,上层调用者可以在适当的位置设计对不同类型异常的处理。
  • 异常是专门针对抽象编程中的一系列错误进行处理的,C++ 中不能借助函数机制,因为栈结构的本质是先进后出,依次访问,无法进行跳跃,但错误处理的特征却是遇到错误信息就想要转到若干级之上进行重新尝试,如图所示:

cplus-plus-exception-2

C++ 异常的基础使用

异常的基本使用语法

cplus-plus-exception-3

  • (a) 若有异常,则可以通过 throw 操作符创建一个异常对象并抛出。
  • (b) 将可能抛出异常的程序段嵌在 try 块之中,控制通过正常的顺序执行到达 try 语句,然后执行 try 代码块内的保护段。
  • (c) 如果在保护段执行期间没有引起异常,那么跟在 try 代码块后的 catch 子句就不会执行,程序从 try 代码块后跟随的最后一个 catch 子句后面的语句将继续执行下去。
  • (d) catch 子句按其在 try 代码块后出现的顺序被检查,匹配到的 catch 子句将捕获并处理异常(或继续抛掷异常)。
  • (e) 如果匹配的异常处理器未被找到,则函数 terminate() 将被自动调用,其缺省功能是调用函数 abort() 终止程序的运行。
  • (f) 处理不了的异常,可以在 catch 子句的最后一个分支,使用 throw 语法,向上抛掷异常。

异常的基础使用案例一

这里将演示在 C++ 中,如何抛出异常和捕获异常。

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
#include <iostream>

using namespace std;

int devide(int a, int b) {
if (b == 0) {
// 抛出 int 类型异常
throw -1;

// 抛出 double 类型异常
// throw 3.14;

// 抛出其他异常
// throw 'a';
}
return a / b;
}

int main() {
try {
int a = 10;
int b = 0;
int c = devide(a, b);
cout << "result: " << c << endl;
}
// 捕获 int 类型异常
catch (int) {
cout << "int 类型异常捕获,被除数不能为零" << endl;
}
// 捕获 double 类型异常
catch (double) {
cout << "double 类型异常捕获,被除数不能为零" << endl;
}
// 捕获其他异常
catch (...) {
// 抛出异常
throw "发生未知的异常 ...";
}

cout << "程序正常结束运行" << endl;
return 0;
}

程序运行输出的结果如下:

1
2
int 类型异常捕获,被除数不能为零
程序正常结束运行

异常的基础使用案例二

这里将演示在 C++ 中,如何抛出自定义异常和捕获自定义异常。

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
#include <iostream>

using namespace std;

// 自定义异常类
class MyException {

public:
MyException(string message) {
this->message = message;
}

void printError() {
cout << "Exception: " << this->message << endl;
}

private:
string message;

};

int devide(int a, int b) {
if (b == 0) {
// 抛出自定义异常
throw MyException("被除数不能为零"); // 匿名对象的写法
}
return a / b;
}


int main() {
try {
int result = devide(9, 0);
cout << "result = " << result << endl;
} catch (MyException e) {
e.printError();
}

cout << "程序正常结束运行" << endl;
return 0;
}

程序运行输出的结果如下:

1
2
Exception: 被除数不能为零
程序正常结束运行

异常的基础使用案例三

异常机制与函数机制互不干涉,但捕捉的方式是基于类型匹配。异常捕捉相当于函数返回类型的匹配,而不是函数参数的匹配,所以异常捕捉不用考虑一个抛掷中的多种数据类型匹配问题。C++ 的异常捕捉是严格按照类型匹配的,它的类型匹配之苛刻程度可以和模板的类型匹配相媲美。它不允许相容类型的隐式转换,比如,抛掷 char 类型的异常,用 int 类型就捕捉不到对应的异常。

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
#include <iostream>

using namespace std;

class A {
};

class B {
};

int main() {
try {
int a;
int i = 0;
double d = 2.3;
char chars[20] = "Hello";
string str = "World";

cout << "Please input a exception number: ";
cin >> a;

switch (a) {
case 1:
throw i;
case 2:
throw d;
case 3:
throw chars;
case 4:
throw str;
case 5:
throw A();
case 6:
throw B();
default:
cout << "No exception throws here.\n";
}
}
catch (int) {
cout << "int exception.\n";
}
catch (double) {
cout << "double exception.\n";
}
catch (char *) {
cout << "char* exception.\n";
}
catch (string) {
cout << "string exception.\n";
}
catch (A) {
cout << "class A exception.\n";
}
catch (B) {
cout << "class B exception.\n";
}

cout << "That's ok.\n";
return 0;
}

程序运行输出的结果如下:

1
2
3
Please input a exception number: 3
char* exception.
That's ok.

C++ 异常的进阶使用

栈解旋

异常被抛出后,从进入 try 代码块起,到异常被抛掷前,这期间在栈上构造的所有对象,都会被自动析构,而析构的顺序与构造的顺序相反,这一过程就称为 栈解旋(Unwinding)

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
#include <iostream>

using namespace std;

class Test {

public:
Test(int a, int b) {
this->a = a;
this->b = b;
cout << "构造函数被调用" << endl;
}

~Test() {
cout << "析构函数被调用" << endl;
}

private:
int a;
int b;
};

int divide(int x, int y) {
// 在栈上创建对象
Test t1(3, 4), t2(5, 6);

if (0 == y) {
throw y; // 抛出 int 类型的异常
}
return x / y;
}

int main() {
// divide(5, 0); 如果 divide() 函数的调用写在 try 代码块之外,那么 Test 类的析构函数不会自动被调用

try {
int result = divide(5, 0);
cout << "result = " << result << endl;
}
catch (int e) {
cout << e << ", 被除数不能为零" << endl;
}
catch (...) {
cout << "发生未知的异常";
}
return 0;
}

程序运行输出的结果如下:

1
2
3
4
5
构造函数被调用
构造函数被调用
析构函数被调用
析构函数被调用
0, 被除数不能为零

异常接口的声明

  • (a) 为了加强程序的可读性,可以在函数声明中列出可能抛出的所有异常类型,例如:void func() throw (A, B, C , D) {},这个函数 func() 能够且只能抛出类型 A、B、C、D 及其子类型的异常。
  • (b) 如果一个函数抛出了它的异常接口声明所不允许抛出的异常,unexpected() 函数会被调用,该函数的默认行为是调用 terminate() 函数中止程序。
  • (c) 一个不抛掷任何类型异常的函数,可以声明为:void func() throw() {}。使用这种写法时,如果函数内部抛出了异常,那么程序会终止运行。
  • (d) 如果在函数声明中没有包含异常接口声明,则此函数可以抛掷任何类型的异常,例如:void func() {}
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
#include <iostream>

using namespace std;

class A {

};

class B {

};

class C {

};

class D {

};

// 能够且只能抛出类型 A、B、C、D 及其子类型的异常
void func1() throw(A, B, C, D) {
throw A();
}

// 不能抛出任何类型的异常
void func2() throw() {

}

// 可以抛出任何类型的异常
void func3() {
throw B();
}

int main() {
try {
func1();
}
catch (A a) {
cout << "发生 A 异常" << endl;
}
catch (...) {
cout << "发生未知其他异常 ..." << endl;
}
return 0;
}

程序运行输出的结果如下:

1
发生 A 异常

异常的多态使用

使用案例一
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
#include <iostream>

using namespace std;

class BaseException {

public:
virtual void printError() {
cout << "Base Exception" << endl;
}

};

// 使用继承
class NullPointerException : public BaseException {

void printError() override {
cout << "Null Pointer Exception" << endl;
}

};

void doWrok() {
throw NullPointerException();
}

int main() {
try {
doWrok();
}
catch (BaseException &e) {
// 这里会发生多态,即父类的引用指向子类的对象
e.printError();
}
return 0;
}

程序运行输出的结果如下:

1
Null Pointer Exception
使用案例二
  • MyException.h
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
#pragma once

#include <iostream>

using namespace std;

// 异常抽象类
class SizeException {

public:
// 纯虚函数
virtual void printErr() = 0;

public:
int getSize() {
return this->size;
}

protected:
int size = 0;
};

class NegativeException : public SizeException {

public:
NegativeException(int size) {
this->size = size;
}

void printErr() {
cout << "数组大小不能小于零, 当前大小为 " << this->size << endl;
}

};

class TooBigException : public SizeException {

public:
TooBigException(int size) {
this->size = size;
}

void printErr() {
cout << "数组大小太大, 当前大小为 " << this->size << endl;
}

};

class ZeroException : public SizeException {

public:
ZeroException(int size) {
this->size = size;
}

void printErr() {
cout << "数组大小不允许为零" << endl;
}
};
  • MyArray.h
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
#pragma once

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

using namespace std;

class MyArray {

public:
// 构造函数
MyArray(int size) {
// 数组初始化大小检查,大小不合法则抛出异常
if (size < 0) {
throw NegativeException(size);
}
else if (size == 0) {
throw ZeroException(size);
}
else if (size > this->m_max_size) {
throw TooBigException(size);
}
this->m_size = size;
this->m_space = new int[size];
}

// 拷贝构造函数
MyArray(const MyArray& obj) {
// 深拷贝
this->m_size = obj.m_size;
this->m_space = new int[obj.m_size];
for (int i = 0; i < obj.m_size; i++) {
this->m_space[i] = obj.m_space[i];
}
}

// 析构函数
~MyArray() {
if (this->m_space) {
delete[] this->m_space;
this->m_space = NULL;
this->m_size = 0;
}
}

public:
// 使用类成员函数,重载运算符 "[]"
int& operator[](int index) {
return this->m_space[index];
}

// 使用类成员函数,重载运算符 "="
MyArray& operator=(const MyArray& obj) {
// 释放原本的内存空间
if (this->m_space) {
delete[] this->m_space;
this->m_space = NULL;
this->m_size = 0;
}
// 深拷贝
this->m_size = obj.m_size;
this->m_space = new int[obj.m_size];
for (int i = 0; i < obj.m_size; i++) {
this->m_space[i] = obj.m_space[i];
}
return *this;
}

// 使用友元函数,重载运算符 "<<"
friend ostream& operator<<(ostream& out, const MyArray& obj);

public:
int getsize() {
return m_size;
}

private:
int* m_space;
int m_size;
int m_max_size = 1000;
};

// 使用友元函数,重载运算符 "<<"
ostream& operator<<(ostream& out, const MyArray& obj) {
for (int i = 0; i < obj.m_size; i++) {
out << obj.m_space[i] << ", ";
}
return out;
}
  • main.cpp
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
#include "MyArray.h"

int main() {
try {
// 调用构造函数
MyArray array1(-6);
// MyArray array1(5);
// MyArray array1(0);
// MyArray array1(2000);

// 重载运算符 "[]"
for (int i = 0; i < array1.getsize(); i++) {
array1[i] = 20 + i;
}

// 重载运算符 "<<"
cout << array1 << endl;

// 调用拷贝构造函数
MyArray array2 = array1;
cout << array2 << endl;

MyArray array3(3);
array3[0] = 43;
array3[1] = 56;
array3[2] = 79;
cout << array3 << endl;

// 重载运算符 "="
array3 = array2;
cout << array3 << endl;

}
// 使用引用捕获异常(多态)
catch (SizeException& e) {
e.printErr();
}
catch (...) {
cout << "发生未知异常" << endl;
}
return 0;
}

程序运行输出的结果如下:

1
数组大小不能小于零, 当前大小为 -6

异常变量的生命周期

  • throw 的异常是有类型的,可以使用数字、字符串、类对象,catch 会严格按照类型进行匹配。
  • throw 出类对象类型的异常时:
    • 如果捕获异常的时候,使用一个异常变量,则拷贝构造该异常变量。
    • 如果捕获异常的时候,使用了引用,则会直接使用 throw 时候的那个对象。
    • 捕获异常的时候,指针可以和引用 / 元素同时出现,但是引用与元素不能同时出现。
    • 结论:如果抛出的是类对象类型的异常,则使用引用进行异常捕获比较合适。

异常变量的使用案例一

如果捕获异常的时候,直接使用一个异常变量,则会拷贝构造该异常变量。也就是说,会调用自定义异常类的拷贝构造函数,即会多一份数据开销。

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
#include <iostream>

using namespace std;

class MyException {

public:

MyException() {
cout << "调用构造函数" << endl;
}

MyException(const MyException &e) {
cout << "调用拷贝构造函数" << endl;
}

~MyException() {
cout << "调用析构函数" << endl;
}

};

void doWork() {
throw MyException(); // 匿名对象的写法
}

int main() {
try {
doWork();
}
// 如果捕获异常的时候,直接使用一个异常变量,则会拷贝构造该异常变量
// 也就是说,会调用自定义异常类的拷贝构造函数,即会多一份数据开销
catch (MyException e) {
cout << "捕获到自定义异常 ..." << endl;
}
catch (...) {
cout << "捕获到未知异常 ..." << endl;
}
return 0;
}

程序运行输出的结果如下:

1
2
3
4
5
调用构造函数
调用拷贝构造函数
捕获到自定义异常 ...
调用析构函数
调用析构函数

异常变量的使用案例二

如果捕获异常的时候,使用了引用,则会使用 throw 时候的那个对象。也就是说,捕获异常时使用引用,不会调用自定义异常类的拷贝构造函数,即会少一份数据开销。强烈推荐以后都使用引用这种方式来进行异常捕获,因为可以减少内存开销。

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
#include <iostream>

using namespace std;

class MyException {

public:

MyException() {
cout << "调用构造函数" << endl;
}

MyException(const MyException &e) {
cout << "调用拷贝构造函数" << endl;
}

~MyException() {
cout << "调用析构函数" << endl;
}

};

void doWork() {
throw MyException(); // 匿名对象的写法
}

int main() {
try {
doWork();
}
// 如果捕获异常的时候,使用了引用,则会使用 throw 时候的那个对象
// 也就是说,捕获异常时使用引用,不会调用自定义异常类的拷贝构造函数,即会少一份数据开销
catch (MyException &e) {
cout << "捕获到自定义异常 ..." << endl;
}
catch (...) {
cout << "捕获到未知异常 ..." << endl;
}

return 0;
}

程序运行输出的结果如下:

1
2
3
调用构造函数
捕获到自定义异常 ...
调用析构函数

异常变量的使用案例三

如果捕获异常的时候,使用了指针,那么尽量不要直接 throw 一个内存地址,否则在 catch 代码块内不能再使用对应的指针。因为在执行 catch 代码块里面的代码之前,对应的指针已经被释放(变成了野指针);如果继续使用对应的指针,虽然程序可能不会立刻终止掉,但是会存在极大的内存安全问题。

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
#include <iostream>

using namespace std;

class MyException {

public:

MyException() {
cout << "调用构造函数" << endl;
}

MyException(const MyException &e) {
cout << "调用拷贝构造函数" << endl;
}

~MyException() {
cout << "调用析构函数" << endl;
}

void printError() {
cout << "Exception Message: null" << endl;
}

};

void doWork() {
MyException exception; // 在栈内存上创建一个对象
throw &exception; // 抛出一个内存地址,不建议使用这种写法,相当于 throw &(MyException());
}

int main() {
try {
doWork();
}
// 在接收异常的时候,使用了指针
catch (MyException *e) {

// 执行到这里的时候,自定义异常类的析构函数已经被调用,即指针 e 已经被释放了(变成了野指针),不能再继续使用
// e->printError();

cout << "捕获到自定义异常 ..." << endl;
}
catch (...) {
cout << "捕获到未知异常 ..." << endl;
}

return 0;
}

程序运行输出的结果如下:

1
2
3
调用构造函数
调用析构函数
捕获到自定义异常 ...

异常变量的使用案例四

如果捕获异常的时候,使用了指针,那么尽量 throw 一个通过 new 操作符创建出来的指针,然后在 catch 代码块内使用完对应的指针后,手动对该指针执行 delete 操作,这样才会释放该指针指向的内存空间(比如会自动调用自定义异常类的析构函数来释放内存)。

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
#include <iostream>

using namespace std;

class MyException {

public:

MyException() {
cout << "调用构造函数" << endl;
}

MyException(const MyException &e) {
cout << "调用拷贝构造函数" << endl;
}

~MyException() {
cout << "调用析构函数" << endl;
}

void printError() {
cout << "Exception Message: null" << endl;
}

};

void doWork() {
throw new MyException(); // 在堆内存中 new 一个对象
}

int main() {
try {
doWork();
}
// 接收异常的时候,指针可以和引用/元素同时出现,但是引用与元素不能同时出现
catch (MyException *e) {

// 自定义异常类的指针 e 可以继续使用,因为自定义异常类的析构函数还没有被调用
e->printError();

// 释放指针指向的内存空间,会自动调用自定义异常类的析构函数来释放内存
delete e;

cout << "捕获到自定义异常 ..." << endl;
}
catch (...) {
cout << "捕获到未知异常 ..." << endl;
}

return 0;
}

程序运行输出的结果如下:

1
2
3
4
调用构造函数
Exception Message: null
调用析构函数
捕获到自定义异常 ...

C++ 提供的标准异常类

标准异常类的介绍

cplus-plus-exception-4
cplus-plus-exception-5

标准异常类的使用
基础使用案例一

这里将演示如何直接使用 C++ 提供的标准异常类。

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
#include <iostream>
#include <stdexcept>

using namespace std;

class Person {

public:
Person(string name, int age) {
if (age < 0 || age > 150) {
// 抛出 C++ 的标准异常类
throw out_of_range("年龄超出范围");
}

if (name.length() <= 0 || name.length() > 32) {
throw length_error("姓名长度错误");
}

this->m_Name = name;
this->m_Age = age;
}

private:
string m_Name;
int m_Age;

};

int main() {
try {
Person person("Tom", 200);
}
catch (out_of_range &e) {
cout << e.what() << endl;
}
catch (length_error &e) {
cout << e.what() << endl;
}
catch (...) {
cout << "发生未知类型的异常!" << endl;
}

return 0;
}

程序运行输出的结果如下:

1
年龄超出范围
基础使用案例二

这里将演示如何继承 C++ 的标准异常类,以此来定义自己的异常类。

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
#include <iostream>
#include <stdexcept>

using namespace std;

// 继承自 C++ 的标准异常类
class MyOutOfRangeException : public exception {

public:
explicit MyOutOfRangeException(const string &str) {
this->m_Error = str.c_str();
}

virtual ~MyOutOfRangeException() {

}

virtual const char *what() {
return this->m_Error;
}

private:
const char *m_Error;

};

class Person {

public:
Person(string name, int age) {
if (age < 0 || age > 150) {
// 抛出自定义异常类
throw MyOutOfRangeException("发生自定义异常:年龄超出范围");
}

this->m_Name = name;
this->m_Age = age;
}

private:
string m_Name;
int m_Age;

};

int main() {
try {
Person person("Tom", 200);
}
catch (MyOutOfRangeException &e) {
cout << e.what() << endl;
}
catch (...) {
cout << "发生未知类型的异常!" << endl;
}

return 0;
}

程序运行输出的结果如下:

1
发生自定义异常:年龄超出范围

C++ 默认的异常处理器

terminate () 函数

在 C++ 中,异常是不可以忽略的,当异常找不到匹配的 catch 子句时,会调用系统的库函数 terminate()(在头文件中);默认情况下,terminate() 函数会调用标准 C 库函数 abort() 使程序终止而退出。当调用 abort() 函数时,程序不会调用正常的终止函数,也就是说,全局对象和静态对象的析构函数不会执行,这就可能会导致内存泄漏。值得一提的是,在多线程程序中,各个 terminate() 函数是互相独立的,每个线程都有自己的 terminate() 函数。

set_terminate () 函数

在 C++ 中,通过使用标准的 set_terminate() 函数,可以设置自己的 terminate() 函数。自定义的 terminate() 函数不能有参数,而且返回值类型必须为 void。另外,terminate() 函数不能抛出异常,它必须终止程序。如果 terminate() 函数被调用,这就意味着问题已经无法解决了。

设置默认的异常处理器
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
#include <iostream>

using namespace std;

// 自定义 terminate() 函数
void myTerminate() {
cout << "函数 myTerminate() 被 terminate() 调用!" << endl;
exit(-1);
}

int divide(int x, int y) {
return x / y;
}

int main() {
// 设置默认的异常处理器
set_terminate(myTerminate);

int x = 10, y = 0, result;
try {
if (y == 0) {
throw "被除数为零!"; //抛出异常,由 terminate() 函数捕获
}
else {
result = x / y;
}
}
// 不会被整型异常捕获
catch (int e) {
cout << "捕获到整型异常!" << endl;
}

cout << "程序正常结束运行!" << endl;
return 0;
}

程序运行输出的结果如下:

1
函数 myTerminate() 被 terminate() 调用!