C++ 进阶基础之二

大纲

函数模板和类模板

C++ 提供了函数模板(function template)。所谓函数模板,实际上是建立一个通用函数,其函数类型和形参类型不具体指定,用一个虚拟的类型来代表,这个通用函数就称为函数模板。凡是函数体相同的函数都可以用这个模板来代替,不必定义多个函数,只需在模板中定义一次即可。在调用函数时,系统会根据实参的类型来取代模板中的虚拟类型,从而实现不同函数的功能。

cplus-plus-template-1

C++ 提供两种模板机制:函数模板、类模板。

  • 模板又称之为 泛型编程
  • 模板将函数或类要处理的数据类型参数化,表现为参数的多态性,称为类属。
  • 模板用于表达逻辑结构相同,但具有数据元素类型不同的数据对象的通用行为。
  • 类属 —— 类型参数化,又称参数模板,使得程序(算法)可以从逻辑功能上抽象,将被处理的对象(数据)类型作为参数传递。

函数模板

函数模板的定义

cplus-plus-template-2

  • 模板声明的语法为:template < 类型形式参数表 >,例如 template <typename T>
  • 类型形式参数表的语法为:typename T1 , typename T2 , …… , typename Tn 或者 class T1 , class T2 , …… , class Tn
  • typename 关键字和 class 关键字都可以用来定义模板参数类型,也就是说 template <typename T>template <class T> 的功能和效果是完全相同的,C++ 标准允许这两种方式是为了向后兼容,并提供灵活性。

函数模板的调用

  • myswap(a, b);:自动数据类型推导
  • myswap<float>(a, b);:显式指定类型调用(推荐)

函数模板的简单使用

函数模板使用案例一

cplus-plus-template-3

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;

// 模板声明
template <typename T>

// 函数定义
void myswap(T &a, T &b) {
T temp;
temp = a;
a = b;
b = temp;
}

int main() {

// 自动数据类型推导
int x = 1, y = 2;
myswap(x, y);
printf("x = %d, y = %d\n", x, y);

// 自动数据类型推导
double n = 0.5, m = 0.8;
myswap(n, m);
printf("n = %f, m = %f\n", n, m);

// 显示指定类型调用(推荐)
char i = 'h', j = 'e';
myswap<char>(i, j);
printf("n = %c, m = %c\n", i, j);

return 0;
}

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

1
2
3
x = 2, y = 1
n = 0.800000, m = 0.500000
n = e, m = 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
#include <iostream>

using namespace std;

// 使用函数模板,实现数组排序
template <typename T1>

void arraySort(T1* array, int size, bool asc = true) {
if (array == NULL || size == 0) {
return;
}

T1 tmp;
for (int i = 0; i < size; i++) {
for (int j = i + 1; j < size; j++) {
// 升序排序(从小到大)
if (asc) {
if (array[i] > array[j]) {
tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
}
// 降序排序(从大到小)
else {
if (array[i] < array[j]) {
tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
}
}
}
}

// 使用函数模板,打印数组
template <typename T2>

void printArray(T2* array, int size) {
for (int i = 0; i < size; i++) {
cout << array[i] << " ";
}
cout << endl;
}

int main() {
int array[] = { 32, 16, 29, 9, 43, 53, 23 };
int size = sizeof(array) / sizeof(*array);

cout << "排序之前: ";
printArray<int>(array, size);

arraySort<int>(array, size, false);

cout << "排序之后: ";
printArray<int>(array, size);

cout << "------------------------------" << endl;

char array2[] = { 'c', 'z', 'h', 'i', 'q', 'm' };
int size2 = sizeof(array2) / sizeof(*array2);

cout << "排序之前: ";
printArray<char>(array2, size2);

arraySort<char>(array2, size2);

cout << "排序之后: ";
printArray<char>(array2, size2);

return 0;
}

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

1
2
3
4
5
排序之前: 32 16 29 9 43 53 23
排序之后: 53 43 32 29 23 16 9
------------------------------
排序之前: c z h i q m
排序之后: c h i m q z

函数模板与普通函数

函数模板和普通函数的区别:

  • (a) 普通函数调用时,可以发生自动类型转换(隐式类型转换)。
  • (b) 函数模板调用时,如果利用自动类型推导,不会发生自动类型转换(隐式类型转换)。

函数模板和普通函数的调用规则:

  • (a) 当函数模板和普通函数都符合调用时,C++ 编译器优先选择调用普通函数
  • (b) 如果函数模板可以产生一个更好的匹配,那么 C++ 编译器会选择调用函数模板
  • (c) 如何希望强制调用函数模板,可以使用空模板参数列表(空参数列表)的语法来限制 C++ 编译器只使用函数模板匹配,比如 mySwap<>(a, b),完整的示例代码如下:
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
#include <iostream>

using namespace std;

// 函数模板
template <typename T>
void myswap(T& a, T& b) {
T tmp;
tmp = a;
a = b;
b = tmp;
cout << "模板函数被调用" << endl;
}

// 普通函数
void myswap(int a, char b) {
cout << "a = " << a << ", b = " << b << endl;
cout << "普通函数被调用" << endl;
}

int main() {
int a = 65;
char c = 'z';
myswap(a, c); // 调用普通函数
myswap(c, a); // 调用普通函数,会进行隐式类型转换
myswap(a, a); // 调用函数模板(本质是类型参数化),将严格地按照类型进行匹配,不会进行隐式类型转换
myswap<>(a, a); // 强制调用函数模板,可以使用空模板参数列表的语法来限制 C++ 编译器只使用函数模板匹配
return 0;
}

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

1
2
3
4
5
6
a = 65, b = z
普通函数被调用
a = 122, b = A
普通函数被调用
模板函数被调用
模板函数被调用

函数模板与函数重载

  • (a) 函数模板可以像普通函数一样被重载
  • (b) 如何出现重载,C++ 编译器优先选择调用普通函数
  • (c) 如果函数模板可以产生一个更好的匹配,那么 C++ 编译器会选择调用函数模板
  • (d) 如何希望强制调用函数模板,可以使用空模板参数列表(空参数列表)的语法来限制 C++ 编译器只使用函数模板匹配,比如 mySwap<>(a, b),完整的示例代码如下:
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
#include "iostream"

using namespace std;

// 普通函数
int Max(int a, int b) {
cout << "int Max(int a, int b)" << endl;
return a > b ? a : b;
}

// 函数模板
template<typename T>
T Max(T a, T b) {
cout << "T Max(T a, T b)" << endl;
return a > b ? a : b;
}

// 函数模板重载
template<typename T>
T Max(T a, T b, T c) {
cout << "T Max(T a, T b, T c)" << endl;
return Max(Max(a, b), c);
}

int main() {
int a = 1;
int b = 2;

Max(a, b); // 当函数模板和普通函数都符合调用时,优先选择调用普通函数
Max('a', 100); // 调用普通函数,可以进行隐式类型转换
Max<>(a, b); // 通过空模板参数列表的语法,可以限制编译器只使用函数模板匹配
Max(3.0, 4.0); // 如果函数模板产生更好的匹配,编译器会使用函数模板

cout << "--------------------" << endl;

Max(5.0, 6.0, 7.0); // 函数模板的重载

return 0;
}

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

1
2
3
4
5
6
7
8
int Max(int a, int b)
int Max(int a, int b)
T Max(T a, T b)
T Max(T a, T b)
--------------------
T Max(T a, T b, T c)
T Max(T a, T b)
T Max(T a, T b)

函数模板的局限性

假设有如下的函数模板:

1
2
3
4
5
template <class T>

void f(T a, Tb) {
...
}

如果代码实现时定义了赋值操作 a = b,但是 T 为数组,那么这种假设就不成立了。同样,如果代码里面的语句为判断语句 if(a > b),但 T 如果是结构体,该假设也不成立。另外,如果传入的是数组,由于数组名是地址,因此它比较的是地址,而这也不是预期所希望的操作。总之,编写的函数模板很可能无法处理某些类型。另一方面,有时候通用化是有意义的,但 C++ 语法不允许这么做。为了解决这种局限性问题,可以提供模板的重载,为这些特定的类型提供具体化的模板。

具体化模板的使用语法

  • 语法:template<> 返回值 函数名<具体类型>(函数参数)
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 Person {

public:

Person(string name, int age) {
this->m_name = name;
this->m_age = age;
}

string m_name;
int m_age;

};

template<class T>
bool myCompare(T &a, T &b) {
return a == b;
}

void test01() {
int a = 10;
int b = 20;
bool resutlt = myCompare(a, b); // 传入基础数据类型
cout << resutlt << endl;
}

// 通过具体化自定义数据类型,解决函数模板的局限性问题
// 如果具体化能够优先匹配,那么就选择具体化
template<> bool myCompare<Person>(Person &a, Person &b) {
return a.m_age == b.m_age;
}

void test02() {
Person p1("Tom", 23);
Person p2("Jim", 23);

bool resutlt = myCompare(p1, p2); // 传入自定义数据类型
cout << resutlt << endl;
}

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

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

1
2
0
1

函数模板的底层原理

  • 编译器并不是根据函数模板,产生能够处理任意参数的函数。
  • 编译器本质上是根据具体的调用类型,从函数模板产生不同的函数(通常称为模板函数)。
  • 编译器会对函数模板进行两次编译,在声明的地方对函数模板代码本身进行第一次编译,在调用的地方对参数替换后的函数模板代码进行第二次编译。

类模板

类模板与函数模板的定义和使用类似,在实际项目开发中,经常有两个或多个类,其功能是相同的,仅仅是数据类型不同,为了不重复定义功能相同的类,可以使用类模板来解决这类问题。

类模板的定义

  • 类模板用于实现类所需数据的类型参数化。
  • 类模板在表示如数组、链表、图等数据结构显得特别重要,这些数据结构的表示和算法不受所包含的元素类型的影响。
  • 在下述的所有代码中,template <typename T> 等价于 template <class T>,二者的功能和效果是完全相同。

类模板的简单使用

值得一提的是,在类模板中如果使用了构造函数,则必须遵守 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
#include <iostream>

using namespace std;

// 模板声明
template <typename T>

// 类定义
class A {

public:

A(T t) {
this->t = t;
}

T& getT() {
return this->t;
}

private:
T t;

};

// 类模板做函数参数
void printA(A<int>& a) {
cout << a.getT() << endl;
}

int main() {
A<int> a(100); // 模板类是抽象的,且不支持自动类型推导,需要声明具体的类型(模板参数列表)这里的 <int> 不能省略
cout << a.getT() << endl;

A<int> a2(50);
printA(a2);

return 0;
}

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

1
2
100
50

类模板作为函数参数

类模板作为函数参数有三种使用方式,包括:

  • 第一种传参方式:指定传入类型
  • 第二种传参方式:参数模板化(函数模板 + 类模板)
  • 第三种传参方式:整体模板化(函数模板)
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
#include <iostream>
#include <typeinfo>

using namespace std;

// 类模板声明
template<class T1, class T2>

class Person {

public:

Person(T1 name, T2 age) {
this->m_Name = name;
this->m_Age = age;
}

void show() {
cout << "name = " + this->m_Name + ", age = " << this->m_Age << endl;
}

T1 m_Name;
T2 m_Age;

};

// 第一种传参方式:指定传入类型
void doWork1(Person<string, int> &p) {
p.show();
}

void test01() {
Person<string, int> p("Jim", 18);
doWork1(p);
}

// 第二种传参方式:参数模板化(函数模板 + 类模板)
template<class T1, class T2>
void doWork2(Person<T1, T2> &p) {
p.show();
// 在运行时查看对象的类型信息
// cout << typeid(T1).name() << endl;
// cout << typeid(T2).name() << endl;
}

void test02() {
Person<string, int> p("Tom", 20);
doWork2(p);
}

// 第三种传参方式:整体模板化(函数模板)
template<class T>
void doWork3(T &p) {
p.show();
}

void test03() {
Person<string, int> p("Peter", 22);
doWork3(p);
}

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

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

1
2
3
name = Jim, age = 18
name = Tom, age = 20
name = Peter, age = 22

类模板与继承的使用

普通类继承类模板

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

using namespace std;

// 模板声明
template <typename T>

// 类定义
class A {

public:

A(T a) {
this->a = a;
}

T& getA() {
return this->a;
}

public:
T a;

};

// 普通类继承类模板,需要声明具体的类型(模板参数列表),这里的 <int> 不能省略
class B : public A<int> {

public:
B(int a, int b) : A<int>(a) {
this->b = b;
}

void printB() {
cout << "a = " << a << ", b = " << b << endl;
}

public:
int b;

};

int main() {
A<int> a(100);
cout << a.getA() << endl;

B b(1, 3);
b.printB();

return 0;
}

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

1
2
100
a = 1, b = 3
类模板继承类模板
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
#include <iostream>

using namespace std;

// 模板声明
template <typename T>

// 类定义
class A {

public:

A(T a) {
this->a = a;
}

T& getA() {
return this->a;
}

public:
T a;

};

// 模板声明
template <typename T>

// 类模板继承类模板
class B : public A<T> {

public:
B(T a, T b) : A(a) {
this->b = b;
}

T& getB() {
return this->b;
}

private:
T b;

};

int main() {
A<int> a(3);
cout << a.getA() << endl;

B<double> b(3.2, 4.5);
cout << b.getA() << endl;
cout << b.getB() << endl;

return 0;
}

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

1
2
3
3
3.2
4.5

类模版与友元函数的使用

特别注意

除了重载运算符 <<>> 必须使用友元函数之外,其他运算符的重载尽量都使用类成员函数来实现。千万不要滥用友元函数,尤其类模板与友元函数一起使用的时候,这是因为需要使用怪异的语法来解决 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
#include <iostream>

using namespace std;

template<class T1, class T2>
class Person {

public:

Person(T1 name, T2 age) {
this->m_Name = name;
this->m_Age = age;
}

// 在类内实现友元函数(相当于全局函数)
friend void printPerson(Person<T1, T2> &p) {
cout << "name: " + p.m_Name + ", age: " << p.m_Age << endl;
}

private:
T1 m_Name;
T2 m_Age;

};

void test01() {
Person<string, int> p("Tom", 20);
printPerson(p);
}

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

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

1
name: Tom, age: 20
友元函数的类外实现

在使用类模板和友元函数时,如果友元函数是在类外实现,则需要使用怪异的语法来解决 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
#include <iostream>

using namespace std;

/********** START 解决类模板与友元函数的编译问题 *********/

// 让编译器提前看到 Person 类的声明
template<class T1, class T2> class Person;

// 让编译器提前看到 printPerson 函数的声明
template<class T1, class T2> void printPerson(Person<T1, T2> &p);

/********** END 解决类模板与友元函数的编译问题 *********/

template<class T1, class T2>
class Person {

public:

Person(T1 name, T2 age) {
this->m_Name = name;
this->m_Age = age;
}

// 定义友元函数,并使用空参数列表来让编译器强制调用函数模板
friend void printPerson<>(Person<T1, T2> &p);

private:
T1 m_Name;
T2 m_Age;

};

// 在类外实现友元函数(相当于全局函数)
template<class T1, class T2>
void printPerson(Person<T1, T2> &p) {
cout << "name: " + p.m_Name + ", age: " << p.m_Age << endl;
}

int main() {
Person<string, int> p("Tom", 20);
printPerson(p);
return 0;
}

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

1
name: Tom, age: 20

类模板函数的三种写法

值得一提的是,企业项目开发中,建议使用第一种或者第三种方式,STL 库一般都采用第一种方式。

所有的类模板函数写在类的内部(第一种)
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>

using namespace std;

template <typename T>

class Complex {

public:

// 构造函数
Complex(T a, T b) {
this->a = a;
this->b = b;
}

// 类成员函数
void print() {
cout << "a = " << this->a << ", b = " << this->b << endl;
}

// 类成员函数,重载运算符 "+"
Complex operator+(Complex& c2) {
Complex tmp(this->a + c2.a, this->b + c2.b);
return tmp;
}

// 友元函数,重载运算符 "<<"
friend ostream& operator<<(ostream& out, Complex& c1) {
cout << "a = " << c1.a << ", b = " << c1.b;
return out;
}

// 友元函数
friend Complex sub(Complex& c1, Complex& c2) {
Complex tmp(c1.a - c2.a, c1.b - c2.b);
return tmp;
}

private:
T a;
T b;

};

int main() {
Complex<int> c1(1, 4);
Complex<int> c2(3, 6);
c1.print();
c2.print();

Complex<int> c3 = c1 + c2;
cout << c3 << endl;

Complex<int> c4 = sub(c1, c2);
cout << c4 << endl;
return 0;
}

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

1
2
3
4
a = 1, b = 4
a = 3, b = 6
a = 4, b = 10
a = -2, b = -2
所有的类模板函数写在类的外部(第二种)

所有的类模板函数写在类的外部(写在同一个 .cpp 源文件中),当使用友元函数重载了 <<>> 运算符时,需要注意声明友元函数的写法 friend ostream& operator<< <T>(ostream& out, Complex& c1);特别注意,除了重载运算符 <<>> 必须使用友元函数之外,其他运算符的重载尽量都使用类成员函数来实现。千万不要滥用友元函数,尤其类模板与友元函数一起使用的时候,这是因为需要使用怪异的语法来解决 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
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
#include <iostream>

using namespace std;

/********** START 解决类模板与友元函数滥用(非重载左移与右移运算符)时出现的编译问题 *********/

template <typename T> class Complex;

template <typename T> Complex<T> sub(Complex<T>& c1, Complex<T>& c2);

/********** END 解决类模板与友元函数滥用(非重载左移与右移运算符)时出现的编译问题 *********/

template <typename T>
class Complex {

public:
// 构造函数
Complex(T a, T b);
// 类成员函数
void print();
// 类成员函数,重载运算符 "+"
Complex operator+(Complex& c2);
// 友元函数(滥用友元函数)
friend Complex sub<T>(Complex& c1, Complex& c2);
// 友元函数,重载运算符 "<<"
friend ostream& operator<< <T>(ostream& out, Complex& c1);

private:
T a;
T b;

};

// 构造函数
template <typename T>
Complex<T>::Complex(T a, T b) {
this->a = a;
this->b = b;
}

// 类成员函数
template <typename T>
void Complex<T>::print() {
cout << "a = " << this->a << ", b = " << this->b << endl;
}

// 类成员函数,重载运算符 "+"
template <typename T>
Complex<T> Complex<T>::operator+(Complex<T>& c2) {
Complex<T> tmp(this->a + c2.a, this->b + c2.b);
return tmp;
}

// 友元函数,重载运算符 "<<"
template <typename T>
ostream& operator<<(ostream& out, Complex<T>& c1) {
cout << "a = " << c1.a << ", b = " << c1.b;
return out;
}

// 友元函数(滥用友元函数)
template <typename T>
Complex<T> sub(Complex<T>& c1, Complex<T>& c2) {
Complex<T> tmp(c1.a - c2.a, c1.b - c2.b);
return tmp;
}

int main() {
Complex<int> c1(3, 8);
Complex<int> c2(9, 5);
c1.print();
c2.print();

Complex<int> c3 = c1 + c2;
cout << c3 << endl;

Complex<int> c4 = sub(c1, c2);
cout << c4 << endl;

return 0;
}

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

1
2
3
4
a = 3, b = 8
a = 9, b = 5
a = 12, b = 13
a = -6, b = 3
所有的类模板函数写在类的外部(第三种)

所有的类模板函数写在类的外部(分开写在 .h.hpp 源文件中),这里除了重载运算符 <<>> 必须使用友元函数之外,千万不要滥用友元函数;因为 C++ 编译器会出现编译错误,且没有很好的解决方法。

  • complex.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#pragma once

#include <iostream>

using namespace std;

template <typename T>

class Complex {

public:
Complex(T a, T b);
void print();
Complex operator+(Complex& c2);
friend ostream& operator<< <T>(ostream& out, Complex& c1);

private:
T a;
T b;

};
  • complex.hpp,这里的 .hpp 文件与 .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
#include "complex.h"

// 构造函数
template <typename T>
Complex<T>::Complex(T a, T b) {
this->a = a;
this->b = b;
}

// 类成员函数
template <typename T>
void Complex<T>::print() {
cout << "a = " << this->a << ", b = " << this->b << endl;
}

// 类成员函数,重载运算符 "+"
template <typename T>
Complex<T> Complex<T>::operator+(Complex<T>& c2) {
Complex<T> tmp(this->a + c2.a, this->b + c2.b);
return tmp;
}

// 友元函数,重载运算符 "<<"
template <typename T>
ostream& operator<<(ostream& out, Complex<T>& c1) {
cout << "a = " << c1.a << ", b = " << c1.b;
return out;
}
  • main.cpp,特别注意,这里引入的是 .hpp 或者 .cpp 文件,而不是 .h 头文件,否则 C++ 编译器会编译失败
1
2
3
4
5
6
7
8
9
10
11
12
13
#include "complex.hpp"

int main() {
Complex<int> c1(6, 13);
Complex<int> c2(23, 34);
c1.print();
c2.print();

Complex<int> c3 = c1 + c2;
cout << c3 << endl;

return 0;
}

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

1
2
3
a = 6, b = 13
a = 23, b = 34
a = 29, b = 47

上面提到的,引入的是 .hpp 或者 .cpp 文件,而不是 .h 头文件,否则 C++ 编译器会编译失败。这是因为由于类模版的成员函数是在运行阶段才去动态创建的,也就是说使用 #include 指令包含 .h 头文件时,编译器不会创建成员函数的具体实现,最终导致编译器出现无法解析外部命令的错误。常用的解决方案有以下两种,约定俗成使用第一种方案(推荐)。

  • 解决方案一:类模板函数的声明和实现分开写,但都写在同一个源文件中,并将源文件的后缀名改为 .hpp,然后在需要使用的地方通过 #include 指令包含 .hpp 源文件,推荐使用此方式。
  • 解决方案二:类模板函数的声明和实现分开写,而且是分开写在不同的源文件中,然后在需要使用的地方通过 #include 指令包含 .cpp 源文件,而不是包含 .h 头文件,不推荐使用此方式。

类模板中的 static 关键字

  • 从类模板实例化的每种数据类型模板类都有自己的类模板数据成员,该数据类型的模板类的所有对象共享同一个 static 数据成员
  • 和非模板类的 static 数据成员一样,模板类的 static 数据成员也应该在源文件范围内定义和初始化
  • 每种数据类型的模板类都有自己单独一份的类模板的 static 数据成员副本,详见 图解分析
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
#include <iostream>

using namespace std;

const double pi = 3.14;

template <typename T> class Circle {

public:
Circle(T radius = 0) {
this->m_radius = radius;
this->m_total++;
}

void setRadius(T radius) {
this->m_radius = radius;
}

T getRadius() {
return this->m_radius;
}

double getGirth() {
return 2 * pi * this->m_radius;
}

double getArea() {
return pi * this->m_radius * this->m_radius;
}

// 类模板的静态成员函数
static int getTotal() {
return m_total;
}

private:
T m_radius;

// 类模板的静态数据成员
static int m_total;

};

// 初始化类模板的静态数据成员
template <typename T> int Circle<T>::m_total = 0;

int main() {
// 每种数据类型的模板类都有自己单独一份的类模板的 static 数据成员副本

Circle<int> c1(4), c2(6);
cout << "m_total = " << Circle<int>::getTotal() << endl;
cout << "radius = " << c1.getRadius() << ", girth = " << c1.getGirth() << ", area = " << c1.getArea() << endl;
cout << "radius = " << c2.getRadius() << ", girth = " << c2.getGirth() << ", area = " << c2.getArea() << endl;

Circle<float> c3(3.2), c4(4.3), c5(6.2);
cout << "m_total = " << Circle<float>::getTotal() << endl;
cout << "radius = " << c3.getRadius() << ", girth = " << c3.getGirth() << ", area = " << c3.getArea() << endl;
cout << "radius = " << c4.getRadius() << ", girth = " << c4.getGirth() << ", area = " << c4.getArea() << endl;
cout << "radius = " << c5.getRadius() << ", girth = " << c5.getGirth() << ", area = " << c5.getArea() << endl;

return 0;
}

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

1
2
3
4
5
6
7
m_total = 2
radius = 4, girth = 25.12, area = 50.24
radius = 6, girth = 37.68, area = 113.04
m_total = 3
radius = 3.2, girth = 20.096, area = 32.1536
radius = 4.3, girth = 27.004, area = 58.0586
radius = 6.2, girth = 38.936, area = 120.702

类模板与函数模板的区别

  • 类模板支持默认类型,而函数模板不支持默认类型。
  • 类模板不支持自动类型推导,而函数模板支持自动类型推导。
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;

// 类模板声明(支持默认类型)
template<class T1, class T2 = int>

class Person {

public:

Person(T1 name, T2 age) {
this->m_Name = name;
this->m_Age = age;
}

void show() {
cout << "name = " + this->m_Name + ", age = " << this->m_Age;
}

T1 m_Name;
T2 m_Age;

};

int main() {
Person<string, int> p1("Jim", 20);
p1.show();

// 类模板支持默认类型
Person<string> p2("Tom", 20);
p2.show();

return 0;
}

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

1
name = Jim, age = 20

数组模板类的实战案例

下面将编写数组模板类,模拟 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
#pragma once

#include <iostream>

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
#include "MyVector.h"

// 构造函数
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
#pragma once

#include <iostream>

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.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
71
72
73
74
75
76
77
78
79
80
81
82
83
#include "Teacher.h"

// 构造函数
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.hppMyVector.hpp,而不是 Teacher.hMyVector.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
#include "Teacher.hpp"
#include "MyVector.hpp"

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++ 为此提供了函数模板和类模板
  • 模板定义以模板声明开始,类属参数必须在模板定义中至少出现一次
  • 同一个类属参数可以用于多个模板
  • 类属参数可用于函数的参数类型、返回值类型和声明函数中的变量
  • 模板由编译器根据实际的数据类型进行实例化,生成可执行代码
  • 模板中的函数称为模板函数,实例化的类模板称为模板类
  • 类模板可以在类层次中使用(即可以被继承)
  • 函数模板可以使用多种方式重载