C++ 进阶基础之二
大纲
函数模板和类模板
C++ 提供了函数模板(function template)。所谓函数模板,实际上是建立一个通用函数,其函数类型和形参类型不具体指定,用一个虚拟的类型来代表,这个通用函数就称为函数模板。凡是函数体相同的函数都可以用这个模板来代替,不必定义多个函数,只需在模板中定义一次即可。在调用函数时,系统会根据实参的类型来取代模板中的虚拟类型,从而实现不同函数的功能。
C++ 提供两种模板机制:函数模板、类模板
- 模板又称之为
泛型编程
- 模板把函数或类要处理的数据类型参数化,表现为参数的多态性,称为类属
- 模板用于表达逻辑结构相同,但具有数据元素类型不同的数据对象的通用行为
- 类属 —— 类型参数化,又称参数模板,使得程序(算法)可以从逻辑功能上抽象,把被处理的对象(数据)类型作为参数传递
函数模板
函数模板的定义
- 模板声明的语法为:
template < 类型形式参数表 >
,例如template <typename T>
- 类型形式参数表的语法为:
typename T1 , typename T2 , …… , typename Tn
或者class T1 , class T2 , …… , class Tn
函数模板的调用
myswap(a, b);
:自动数据类型推导myswap<float>(a, b);
:显示类型调用(推荐)
函数模板的简单使用
1 |
|
程序运行输出的结果如下:
1 | x = 2, y = 1 |
函数模板做函数参数
1 |
|
程序运行输出的结果如下:
1 | 排序之前: 32 16 29 9 43 53 23 |
函数模板与普通函数
函数模板和普通函数的区别:
- a) 函数模板不允许自动类型转化
- b) 普通函数能够进行自动类型转换
函数模板和普通函数的调用规则:
- a) C++ 编译器优先考虑使用普通函数
- b) 如果函数模板可以产生一个更好的匹配,那么编译器会选择函数模板
1 |
|
程序运行输出的结果如下:
1 | a = 10, b = z |
函数模板与函数重载
- a) 函数模板可以像普通函数一样被重载
- b) 通过空模板实参列表的语法,可以限制编译器只使用函数模板匹配
- c) 如果函数模板可以产生一个更好的匹配,那么编译器会选择函数模板
1 |
|
程序运行输出的结果如下:
1 | int Max(int a, int b) |
函数模板底层原理剖析
- 编译器并不是根据函数模板,产生能够处理任意参数的函数
- 编译器本质上是根据具体的调用类型,从函数模板产生不同的函数
- 编译器会对函数模板进行两次编译,在声明的地方对函数模板代码本身进行第一次编译,在调用的地方对参数替换后的函数模板代码进行第二次编译
类模板
类模板与函数模板的定义和使用类似,在实际项目开发中,经常有两个或多个类,其功能是相同的,仅仅是数据类型不同,为了不重复定义功能相同的类,可以使用类模板来解决这类问题。
类模板的定义
- 类模板用于实现类所需数据的类型参数化
- 类模板在表示如数组、表、图等数据结构显得特别重要,这些数据结构的表示和算法不受所包含的元素类型的影响
- 在下述的所有代码中,
template <typename T>
等价于template <class T>
类模板的简单使用
值得一提的是,在类模板中如果使用了构造函数,则必须遵守 C++ 类的构造函数的调用规则
1 |
|
程序运行输出的结果如下:
1 | 100 |
类模板与派生类的使用
普通类继承类模板
在 C++ 中,类模板可以被普通类继承,普通类继承类模板时,需要声明父类具体的数据类型。
1 |
|
程序运行输出的结果如下:
1 | 100 |
类模板继承类模板
1 |
|
程序运行输出的结果如下:
1 | 3 |
类模板函数的三种写法
值得一提的是,企业项目开发中,建议使用第一种或者第三种方式,STL 库一般都采用第一种方式。
所有的类模板函数写在类的内部(第一种)
1 |
|
程序运行输出的结果如下:
1 | a = 1, b = 4 |
所有的类模板函数写在类的外部(第二种)
所有的类模板函数写在类的外部(写在同一个 .cpp
文件),当使用友元函数重载了 <<
、>>
运算符时,需要特别注意声明友元函数的写法 friend ostream& operator<< <T>(ostream& out, Complex& c1);
。特别注意,除了重载运算符 <<
、>>
必须使用友元函数之外,其他运算符的重载尽量都使用类成员函数。千万不要滥用友元函数,尤其类模板与友元函数一起使用的时候,这是因为需要使用怪异的语法来解决 C++ 编译器出现的错误,且不同的 C++ 编译器表现行为不一定一致。假设在类模板中滥用了友元函数,解决 C++ 编译问题的语法详见 图解分析。
1 |
|
程序运行输出的结果如下:
1 | a = 3, b = 8 |
所有的类模板函数写在类的外部(第三种)
所有的类模板函数写在类的外部(分开写在 .h
和 .cpp
中),这里除了重载运算符 <<
、>>
必须使用友元函数之外,千万不要滥用友元函数;因为 C++ 编译器会出现编译错误,且没有很好的解决方法。
- complex.h
1 |
|
- complex.hpp,这里的
.hpp
文件与.cpp
文件本质上没有区别,为了方便区分意图,只是文件的后缀不一样而已
1 |
|
- main.cpp,特别注意,这里引入的是
.hpp
或者.cpp
文件,而不是.h
头文件,否则 C++ 编译器会编译失败
1 |
|
程序运行输出的结果如下:
1 | a = 6, b = 13 |
类模板中的 static 关键字
- 从类模板实例化的每种数据类型模板类都有自己的类模板数据成员,该数据类型的模板类的所有对象共享同一个
static
数据成员 - 和非模板类的
static
数据成员一样,模板类的static
数据成员也应该在源文件范围内定义和初始化 - 每种数据类型的模板类都有自己单独一份的类模板的
static
数据成员副本,详见 图解分析
1 |
|
程序运行输出的结果如下:
1 | m_total = 2 |
数组模板类的实战案例
下面将编写数组模板类,模拟 STL 容器的实现,同时贯穿上面所讲的 C++ 模板知识点。
★点击显示完整的案例代码★
- MyVector.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
using namespace std;
template <class T>
class MyVector {
public:
MyVector(int size = 0);
~MyVector();
MyVector(const MyVector& obj);
public:
int getSize();
public:
T& operator[](int index);
MyVector& operator=(const MyVector& obj);
friend ostream& operator<< <T>(ostream& out, MyVector& obj);
private:
T* m_space; // 指向数组的指针
int m_size;
};
- MyVector.hpp
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
// 构造函数
template <typename T>
MyVector<T>::MyVector(int size) {
this->m_size = size;
// 分配内存空间
this->m_space = new T[size];
}
// 析构函数
template <typename T>
MyVector<T>::~MyVector() {
if (this->m_space) {
// 释放内存空间
delete[] this->m_space;
this->m_size = 0;
this->m_space = NULL;
}
}
// 拷贝构造函数
template <typename T>
MyVector<T>::MyVector(const MyVector<T>& obj) {
// 深拷贝
this->m_size = obj.m_size;
this->m_space = new T[obj.m_size];
for (int i = 0; i < obj.m_size; i++) {
this->m_space[i] = obj.m_space[i];
}
}
// 普通类成员函数
template <typename T>
int MyVector<T>::getSize() {
return this->m_size;
}
// 使用类成员函数,重载运算符 "[]"
template <typename T>
T& MyVector<T>::operator[](int index) {
return this->m_space[index];
}
// 使用类成员函数,重载运算符 "="
template <typename T>
MyVector<T>& MyVector<T>::operator=(const MyVector<T>& obj) {
if (this->m_space) {
// 释放原本的内存空间
delete[] this->m_space;
this->m_size = 0;
this->m_space = NULL;
}
// 深拷贝
this->m_size = obj.m_size;
this->m_space = new T[obj.m_size];
for (int i = 0; i < obj.m_size; i++) {
this->m_space[i] = obj.m_space[i];
}
return *this;
};
// 使用友元函数,重载运算符 "<<"
template <typename T>
ostream& operator<<(ostream& out, MyVector<T>& obj) {
for (int i = 0; i < obj.m_size; i++) {
cout << obj.m_space[i] << ", ";
}
return out;
}
- Teacher.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
using namespace std;
class Teacher {
public:
Teacher();
Teacher(int age, const char* name);
Teacher(const Teacher& obj);
~Teacher();
public:
Teacher& operator=(const Teacher& obj);
friend ostream& operator<<(ostream& out, Teacher& obj);
public:
int getAge();
char* getName();
void setAge(int age);
void setName(const char* name);
private:
int m_age;
char* m_name;
};
- Teacher.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
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
// 构造函数
Teacher::Teacher() {
this->m_age = 0;
this->m_name = (char*)malloc(1);
if (this->m_name) {
strcpy(this->m_name, "");
}
}
// 构造函数
Teacher::Teacher(int age, const char* name) {
this->m_age = age;
this->m_name = (char*)malloc(strlen(name) + 1);
if (this->m_name) {
strcpy(this->m_name, name);
}
}
// 拷贝构造函数
Teacher::Teacher(const Teacher& obj) {
// 深拷贝
this->m_age = obj.m_age;
this->m_name = (char*)malloc(strlen(obj.m_name) + 1);
if (this->m_name) {
strcpy(this->m_name, obj.m_name);
}
}
// 析构函数
Teacher::~Teacher() {
if (this->m_name) {
free(this->m_name);
}
}
// 使用类成员函数,重载运算符 "="
Teacher& Teacher::operator=(const Teacher& obj) {
// 释放原本的内存空间
if (this->m_name) {
free(this->m_name);
this->m_name = NULL;
}
// 深拷贝
this->m_age = obj.m_age;
this->m_name = (char*)malloc(strlen(obj.m_name) + 1);
if (this->m_name) {
strcpy(this->m_name, obj.m_name);
}
return *this;
}
// 使用友元函数,重载运算符 "<<"
ostream& operator<<(ostream& out, Teacher& obj) {
cout << "age = " << obj.m_age << " name = " << obj.m_name;
return out;
}
int Teacher::getAge() {
return this->m_age;
}
char* Teacher::getName() {
return this->m_name;
}
void Teacher::setAge(int age) {
this->m_age = age;
}
void Teacher::setName(const char* name) {
// 释放原本的内存空间
if (this->m_name) {
free(this->m_name);
this->m_name = NULL;
}
// 深拷贝
this->m_name = (char*)malloc(strlen(name) + 1);
if (this->m_name) {
strcpy(this->m_name, name);
}
}
- main.cpp,值得一提的是,这里需要引入
Teacher.cpp
和 MyVector.hpp
,而不是 Teacher.h
和 MyVector.h
头文件,否则 C++ 编译器会编译失败,本质原因是由于 C++ 编译器会对模板进行两次编译导致的,详见 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
int main() {
// 自动调用构造函数
MyVector<int> v(5);
// 重载运算符 "[]"
for (int i = 0; i < v.getSize(); i++) {
v[i] = i + 1;
}
// 重载运算符 "<<"
cout << v << endl;
// 自动调用拷贝构造函数
MyVector<int> v2 = v;
cout << v2 << endl;
// 重载运算符 "="
MyVector<int> v3(2);
v3 = v2;
cout << v3 << endl;
// 容器存放类对象
MyVector<Teacher> teachers(3);
for (int i = 0; i < teachers.getSize(); i++) {
Teacher t(i + 20, "Jim");
teachers[i] = t;
}
cout << teachers << endl;
// 容器存放指针
MyVector<Teacher*> points(4);
for (int i = 0; i < points.getSize(); i++) {
points[i] = new Teacher(25 + i, "Tom");
}
for (int i = 0; i < points.getSize(); i++) {
Teacher* obj = points[i];
cout << "age = " << obj->getAge() << " name = " << obj->getName() << ", ";
}
return 0;
}
程序运行输出的结果如下:
1
2
3
4
5
1, 2, 3, 4, 5,
1, 2, 3, 4, 5,
1, 2, 3, 4, 5,
age = 20 name = Jim, age = 21 name = Jim, age = 22 name = Jim,
age = 25 name = Tom, age = 26 name = Tom, age = 27 name = Tom, age = 28 name = Tom,
函数模板与类模板的使用总结
- 模板是 C++ 类型参数化的多态工具,C++ 为此提供了函数模板和类模板
- 模板定义以模板声明开始,类属参数必须在模板定义中至少出现一次
- 同一个类属参数可以用于多个模板
- 类属参数可用于函数的参数类型、返回值类型和声明函数中的变量
- 模板由编译器根据实际的数据类型进行实例化,生成可执行代码
- 模板中的函数称为模板函数,实例化的类模板称为模板类
- 类模板可以在类层次中使用(即可以被继承)
- 函数模板可以使用多种方式重载