C++ 进阶基础之三

类型转换

类型转换的语法

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

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

    • const_cast:去除变量的 const 只读属性
    • reinterpreter_cast:重新解释类型(强制类型转换)
    • static_cast:静态类型转换,如 int 转换成 char
    • dynamic_cast:动态类型转换,如父类和子类之间的多态类型转换
  • C++ 4 种类型转换的语法:TYPE B = static_cast<TYPE> (a)

类型转换的一般性介绍

一般性介绍:

  • 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<>() 很难保证代码的移植性

类型转换的简单使用案例

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 ...

使用总结:

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

异常处理机制

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

异常处理的基本思想

传统错误处理机制

传统的程序错误处理机制,是通过函数返回值来处理错误。

异常处理的基本思想

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 语法,向上抛掷异常

异常的简单使用案例一

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

using namespace std;

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

int main() {
try {
int result = divide(5, 0);
cout << "result = " << result << endl;
}
catch (int e) {
cout << e << ", 被除数不能为零" << endl;
}
// 会捕获所有未被捕获的异常,必须最后出现
catch (...) {
throw "发生未知的异常 ...";
}

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

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

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

异常的简单使用案例二

异常机制与函数机制互不干涉,但捕捉的方式是基于类型匹配。异常捕捉相当于函数返回类型的匹配,而不是函数参数的匹配,所以异常捕捉不用考虑一个抛掷中的多种数据类型匹配问题。异常捕捉是严格按照类型匹配的,它的类型匹配之苛刻程度可以和模板的类型匹配相媲美。它不允许相容类型的隐式转换,比如,抛掷 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
#include <iostream>

using namespace std;

class A {};
class B {};

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

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

switch (a) {
case 1:
throw i;
case 2:
throw d;
case 3:
throw str;
case 4:
throw A();
case 5:
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 (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.

异常在继承中的使用案例

  • 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

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
#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() {}
  • d) 一个不抛掷任何类型异常的函数,可以声明为:void func() 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
#include <iostream>

using namespace std;

class A {};
class B {};
class C {};
class D {};
class F {};

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

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

}

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

int main() {
try {
funcA();
}
catch (...) {
cout << "发生异常 ..." << endl;
}
return 0;
}

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

1
发生异常 ...

默认的异常处理器

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() 调用!

C++ 提供的标准异常库

标准异常库的介绍

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

标准异常库的使用案例
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 Teacher {
public:
Teacher(int age) {
if (age > 100) {
// 抛出标准库内的异常
throw out_of_range("年龄太大");
}
this->age = age;
}

private:
int age;

};

// 继承标准库内的异常
class MyException : public exception {
public:
MyException(const char *p) {
this->m_p = p;
}

virtual const char *what() {
cout << "MyException 类型的异常 : " << m_p << endl;
return m_p;
}

private:
const char *m_p;
};

int main() {
try {
// Teacher teacher(105);
throw MyException("发生自定义异常!");
}
catch (out_of_range e) {
cout << "out_of_range 类型的异常 : " << e.what() << endl;
}
catch (MyException &e) {
e.what();
}
catch (...) {
cout << "发生未知类型的异常!" << endl;
}
return 0;
}

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

1
MyException 类型的异常 : 发生自定义异常!

异常类型和异常变量的生命周期

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

using namespace std;

class BadSrcType {
};

class BadDestType {
};

class BadProcessType {
public:
BadProcessType() {
cout << "BadProcessType的构造函数被调用" << endl;
}


BadProcessType(const BadProcessType& obj) {
cout << "BadProcessType的拷贝构造函数被调用" << endl;
}

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

};

void myStrcpy(char* to, char* from) {
if (to == NULL) {
throw BadDestType();
}
if (from == NULL) {
throw BadSrcType();
}

if (*from == 'a') {
throw BadProcessType();
}
if (*from == 'b') {
// 不建议使用这种写法
throw& (BadProcessType());
}
if (*from == 'c') {
throw new BadProcessType;
}

while (*from != '\0') {
*to = *from;
to++;
from++;
}
*to = '\0';
}

int main() {
int ret = 0;
char buf1[] = "cbbcdefg";
char buf2[1024] = { 0 };

try {
myStrcpy(buf2, buf1);
}
catch (BadSrcType e) {
cout << " BadSrcType 类型异常" << endl;
}
catch (BadDestType e) {
cout << " BadDestType 类型异常" << endl;
}
/*
// 结论1: 如果接收异常的时候,使用一个异常变量,则拷贝构造该异常变量
catch (BadProcessType e)
{
cout << " BadProcessType 类型异常" << endl;
}

// 结论2: 如果接收异常的时候,使用了引用,则会使用throw时候的那个对象
catch (BadProcessType& e)
{
cout << " BadProcessType 类型异常" << endl;
}

// 结论3: 接收异常的时候,指针可以和引用/元素同时出现,但是引用与元素不能同时出现
catch (BadProcessType* e)
{
cout << " BadProcessType 类型异常" << endl;
delete e;
}

// 结论4: 如果抛出的是类对象类型的异常,则使用引用进行异常捕获比较合适

*/
catch (...) {
cout << "未知 类型异常" << endl;
}
return 0;
}