0%

模板(Template)并不是真正的函数或类,它仅仅是编译器用来生成函数或类的一张 "图纸"。模板不会占用内存,最终生成的函数或者类才会占用内存。由模板生成函数的过程叫做模板的实例化,相应地,针对某个类型生成的特定版本的函数或类叫做模板的一个实例。

在学习模板以前,如果想针对不同的类型使用相同的算法,就必须定义多个及其相似的函数或类,这样不但做了很多重复性的工作,还导致代码维护困难,用于交换两个变量的值的 Swap() 函数就是一个典型代表。而有了模板后,这些工作都可以交给编译器了,编译器会帮助我们自动地生成这些代码。从这个角度理解,模板也可以看做是编译器的一组指令,它命令编译器生成我们想要的代码。

模板的实例化是按需进行的,用到哪个类型就生成针对哪个类型的函数或类,不会提前生成过多的代码。也就是说,编译器会根据传递给类型参数的实参(也可以是编译器自己推演出来的实参)来生成一个特定版本的函数或类,并且相同的类型只生成一次。实例化的过程也很简单,就是将所有的类型参数用实参代替。

例如,给定下面的函数模板和函数调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template<typename T> void Swap(T &a, T &b) {
T temp = a;
a = b;
b = temp;
}

int main()
{
int n1 = 100, n2 = 200, n3 = 300, n4 = 400;
float f1 = 12.5, f2 = 56.93;

Swap(n1, n2); // T 为 int,实例化出 void Swap(int &a, int &b);
Swap(f1, f2); // T 为 float,实例化出 void Swap(float &a, float &b);
Swap(n3, n4); // T 为 int,调用刚才生成的 void Swap(int &a, int &b);

return 0;
}

编译器会根据不同的实参实例化出不同版本的 Swap() 函数。对于 Swap(n1, n2) 调用,编译器会生成并编译一个 Swap() 版本,其中 T 被替换为 int:

1
2
3
4
5
void Swap(int &a, int &b) {
int temp = a;
a = b;
b = temp;
}

对于 Swap(f1, f2) 调用,编译器会生成另一个 Swap 版本,其中 T 被替换为 float。对于 Swap(n3, n4) 调用,编译器不会再生成新版本的 Swap() 了,因为刚才已经针对 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
#include <iostream>
using namespace std;

template<typename T1, typename T2>
class Point {
public:
Point(T1 x, T2 y): m_x(x), m_y(y) {}

public:
T1 getX() const {return m_x;}
void setX(T1 x) { m_x = x; }
T2 getY() const {return m_y;}
void setY(T2 y) { m_y = y; }
void display() const ;

private:
T1 m_x;
T2 m_y;
};

template<typename T1, typename T2>
void Point<T1, T2>::display() const {
cout << "x=" << m_x << ", y=" << m_y << endl;
}

int main()
{
Point<int, int> p1(10, 20);
p1.setX(40);
p1.setY(50);
cout<<"x="<<p1.getX()<<", y=" <<p1.getY()<<endl;

Point<char*, char*> p2("东经180度", "北纬210度");
p2.display();

return 0;
}

输出:

1
2
x=40, y=50
x=东经180度, y=北纬210度

p1 调用了所有的成员函数,整个类会被完整地实例化。p2 只调用了构造函数和 display() 函数,剩下的 get 函数和 set 函数不会被实例化。

值得提醒的是,Point<int, int> 和 Point<char, char> 是两个相互独立的类,它们的类型是不同的,不能相互兼容,也不能自动地转换类型,所以诸如 p1 = p2; 这样的语句是错误的,除非重载了 = 运算符。

模板是一种泛型技术,目的是将数据的类型参数化,以增强 C++ 语言(强类型语言)的灵活性。C++ 对模板的支持非常自由,模板中除了可以包含类型参数,还可以包含非类型参数,例如:

1
2
template<typename T, int N> class Demo{ };
template<class T, int N> void func(T (&arr)[N]);

T 是一个类型参数,它通过 classtypename 关键字指定。N 是一个非类型参数,用来传递数据的值,而不是类型,它和普通函数的形参一样,都需要指明具体的类型。类型参数和非类型参数都可以用在函数体或者类体中。

当调用一个函数模板或者通过一个类模板创建对象时,非类型参数会被用户提供的、或者编译器推断出的值所取代。

在函数模板中使用非类型参数

在 《C++函数模板的重载》一节中,我们通过 Swap() 函数来交换两个数组的值,其原型为:

1
template<typename T> void Swap(T a[], T b[], int len);

形参 len 用来指明要交换的数组的长度,调用 Swap() 函数之前必须先通过 sizeof 求得数组长度再传递给它。

这是因为,数组作为参数传递给函数形参的时候,会被转换为指针类型。而单纯通过这个指针无法获取数组长度。

多出来的形参 len 给编码带来了不便,我们可以借助模板中的非类型参数将它消除,请看下面的代码:

1
2
3
4
5
6
7
8
template<typename T, unsigned N> void Swap(T (&a)[N], T(&b)[N]) {
T temp;
for (int i=0; i<N; i++) {
temp = a[i];
a[i] = b[i];
b[i] = temp;
}
}

T (&a)[N] 表明 a 是一个引用,它引用的数据类型是 T[N],也即一个数组;T (&b)[N] 也是类似的道理。分析一个引用和分析一个指针的方法类似,编译器总是从它的名字开始读取,然后按照优先级顺序依次解析。

调用 Swap() 函数时,需要将数组名字传递给它:

1
2
3
int a[5] = {1, 2, 3, 4, 5};
int b[5] = {10, 20, 30, 40, 50};
Swap(a, b);

编译器会使用数组类型 int 来代替类型参数 T,使用数组长度 5 来代替非类型参数 N。

下面是一个完整的示例:

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;

template<class T> void Swap(T &a, T &b); // 模板1:交换基本类型的值
template<typename T, unsigned N> void Swap(T (&a)[N], T (&b)[N]); // 模板2:交换两个数组

template<typename T, unsigned N> void printArray(T (&arr)[N]); // 打印数组元素

int main()
{
// 交换基本类型的值
int m = 10, n = 99;
Swap(m, n); // 匹配模板1
cout << m << ", " << n << endl;

// 交换两个数组
int a[5] = {1, 2, 3, 4, 5};
int b[5] = {10, 20, 30, 40, 50};
Swap(a, b); // 匹配模板2
printArray(a);
printArray(b);

return 0;
}

template<class T> void Swap(T &a, T &b) {
T temp = a;
a = b;
b = temp;
}

template<typename T, unsigned N> void Swap(T (&a)[N], T (&b)[N]) {
T temp;
for (int i = 0; i < N; ++i) {
temp = a[i];
a[i] = b[i];
b[i] = temp;
}
}

template<typename T, unsigned N> void printArray(T (&arr)[N]) {
for (int i = 0; i < N; ++i) {
if (i == N - 1) {
cout << arr[i] << endl;
} else {
cout << arr[i] << ", ";
}
}
}

在模板中使用非类型参数

CPP 规定,数组一旦定义后,它的长度就不能改变了;换句话说,数组容量不能动态地增大或者减小。这样的数组称为静态数组(static array)。静态数组有时候会给编写代码带来不便,我们可以通过自定义的 Array 类来实现动态数组(dynamic array)。所谓动态数组,是指数组容量能够在使用的过程中随时增大或减小。

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
#include <iostream>
#include <cstring>
#include <cstdlib>
using namespace std;

template<typename T, int N>
class Array {
public:
Array();
~Array();

public:
T& operator[](int i); // 重载下标运算符 []
int length() const { return m_length; } // 获取数组长度
bool capacity(int n); // 改变数组容量
void print();
private:
int m_length; // 数组的当前长度
int m_capacity; // 当前内存的容量(能容纳的元素的个数)
T *m_p; // 指向数组内存的指针
};

template<typename T, int N>
Array<T, N>::Array() {
m_p = new T[N];
m_capacity = m_length = N;
}

template<typename T, int N>
Array<T, N>::~Array() {
delete []m_p;
}

template<typename T, int N>
T & Array<T, N>::operator[](int i) {
if (i < 0 || i > m_length -1) {
cout << "Exception: index out of range." << endl;
}

return m_p[i];
}

template<typename T, int N>
bool Array<T, N>::capacity(int n) {
if (n > 0) {
// 容量增长
int len = m_length + n; // 增大后的数组长度
if (len < m_capacity) { // 现有内存足以容纳增大后的数组
m_length = len;
return true;
} else { // 现有内存不能容纳增大后的数组
T *pTemp = new T[m_length + 2 * n * sizeof(T)]; // 增加的内存足以容纳 2*n 个元素
if (pTemp == NULL) { // 内存分配失败
cout << "Exception: Failed to allocate memory" << endl;
return false;
} else { // 内存分配成功
memcpy(pTemp, m_p, m_length * sizeof(T));
delete []m_p;
m_p = pTemp;
m_capacity = m_length = len;
return true;
}
}
} else {
// 容量减小
int len = m_length - abs(n); // 收缩后的数组长度
if (len < 0) {
cout<<"Exception: Array length is too small!"<<endl;
return false;
} else {
m_length = len;
return true;
}
}
}

template<typename T, int N>
void Array<T, N>::print() {
for (int i = 0; i < m_length; ++i) {
cout << m_p[i] << " ";
}
cout << endl;
}

int main()
{
Array<int, 5> arr;

// 为数组元素赋值
for (int i = 0; i < arr.length(); ++i) {
arr[i] = 2 * i;
}

// 第一次打印数组
arr.print();

// 扩大容量并为增加的元素赋值
arr.capacity(8);
for (int j = 5; j < arr.length(); ++j) {
arr[j] = 2 * j;
}

// 第二次打印数组
arr.print();

// 收缩容量
arr.capacity(-4);
// 第三次打印数组
arr.print();
}

运行结果:

1
2
3
0 2 4 6 8
0 2 4 6 8 10 12 14 16 18 20 22 24
0 2 4 6 8 10 12 14 16

Array 是一个类模板,它有一个类型参数 T 和一个非类型参数 N,T 指明了数组元素的类型,N 指明了数组长度。

capacity() 成员函数是 Array 类的关键,它使得数组容量可以动态地增加或者减小。传递给它一个正数时,数组容量增大;传递给它一个负数时,数组容量减小。

之所以能通过 [] 来访问数组元素,是因为在 Array 类中以成员函数的形式重载了 [] 运算符,并且返回值是数组元素的引用。如果直接返回数组元素的值,那么将无法给数组元素赋值。

非类型参数的限制

非类型参数不能随意指定,它受到了严格的限制,只能是一个整数,或者是一个指向对象或函数的指针(也可以是引用)。引用和指针在本质上是一样的。

  1. 当非类型参数是一个整数时,传递给它的实参,或者由编译器推导出的实参必须是一个常量表达式,例如 10、 2 * 30 等,但不能是 n、10 + n 等(n 是变量)。

对于上面的 Swap() 函数,下面的调用就是错误的:

1
2
3
4
5
int len;
cin >> len;
int a[len];
int b[len];
Swap(a, b);

对上面的 Array 类,以下创建对象的方式是错误的:

1
2
3
int len;
cin >> len;
Array<int, len> arr;

这两种情况,编译器推导出来的实参是 len,是一个变量,而不是常量。

  1. 当非类型参数是一个指针(引用)时,绑定到该指针的实参必须具有静态的生存期;换句话说,实参必须存储在虚拟地址空间中的静态数据区。局部变量位于栈区,动态创建的对象位于堆区,它们都不能用作实参。

在将函数用于多文件编程时,我们通常是将函数定义放在源文件(.cpp 文件)中,将函数声明放在头文件(.h文件)中,使用函数时引入(#include 命令)对应的头文件即可。

编译是针对单个源文件的,只要有函数声明,编译器就能知道函数调用是否正确;而将函数调用和函数定义对应起来的过程,可以延迟到链接时期。正是有了连接器的存在,函数声明和函数定义的分离才得以实现。

将类应用于多文件编程也是类似的道理,我们可以将类的声明和类的实现分别放到头文件和源文件中。类的声明已经包含了所有成员变量的定义和所有成员函数的声明(也可以是 inline 形式的定义),这样就知道如何创建对象了,也知道如何调用成员函数了,只是还不能将函数调用与函数实现对应起来,但是这又有什么关系呢,反正连接器可以帮助我们完成这项工作。

总的来说,不管是函数还是类,声明和定义(实现)的分离其实是一回事,都是将函数 定义放到其他文件中,最终要解决的问题也只有一个,就是把函数调用和函数定义对应起来(找到函数定义的地址,并填充到函数调用处),而保证完成这项工作的就是链接器。

基于传统的编程思维,初学者往往也会将模板(函数模板和类模板)的声明和定义分散到不同的文件中,以期达到模块化编程的目的。但事实证明这种做法是不对的,程序员惯用的做法是将模板的声明和定义都放到头文件中。

模板并不是真正的函数或类,它仅仅是用来生成函数或类的一张 "图纸",在这个生成过程中有三点需要明确:

  • 模板的实例化是按需进行的,用到哪个类型就生成针对哪个类型的函数或类,不会提前生成过多的代码

  • 模板的实例化是由编译器完成的,而不是由链接器完成的

  • 在实例化过程中需要知道模板的所有细节,包含声明和定义(只有一个声明是不够的,可能会在链接阶段才发现错误)

将函数模板的声明和定义分散到不同的文件

func.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 交换两个数的值
template<typename T> void Swap(T &a, T &b) {
T temp = a;
a = b;
b = temp;
}

// 冒泡排序算法
void bubble_sort(int arr[], int n) {
for (int i = 0; i < n - 1; ++i) {
bool isSorted = true;
for (int j = 0; j < n - 1 - i; ++j) {
if (arr[j] > arr[j + 1]) {
isSorted = false;
Swap(arr[j], arr[j + 1]); // 调用 Swap() 函数
}
}
if (isSorted) break;
}
}

func.h

1
2
3
4
5
6
7
#ifndef _FUNC_H
#define _FUNC_H

template<typename T> void Swap(T &a, T &b);
void bubble_sort(int arr[], int n);

#endif

main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
#include "func.h"

int main()
{
int n1 = 10, n2 = 20;
Swap(n1, n2);

double f1 = 23.8, f2 = 92.6;
Swap(f1, f2);

return 0;
}

该工程包含了两个源文件和一个头文件,func.cpp 中定义了两个函数,func.h 中对函数进行了声明,main.cpp 中对函数进行了调用,这是典型的将函数的声明和实现分离的编程模式。

编译一下:

1
gcc func.cpp main.cpp

编译产生了一个链接错误,意思是无法找到 void Swap<double>(double&, double&) 这个函数。主函数 main() 中共调用了两个版本的 Swap() 函数,它们的原型分别是:

1
2
void Swap<int>(int&, int&);
void Swap<double>(double&, double&);

为什么针对 int 的版本能够找到定义,而针对 double 的版本就找不到呢?

我们首先来说针对 double 的版本为什么找不到定义。当编译器编译 main.cpp 时,发现使用到了 double 版本的 Swap() 函数,于是尝试生成一个 double 版本的实例,但是由于只有声明没有定义,所以生成失败。不过这个时候编译器不会报错,而是针对该函数的调用做一个记录,希望等到链接程序时在其他目标文件(.obj 或者 .o 文件)中找到该函数的定义。很明显,本例需要到 func.obj 中寻找。但是遗憾的是,func.cpp 中没有调用 double 版本的 Swap() 函数,编译器不会生成 double 版本的实例,所以连接器最终也找不到 double 版本的函数定义,只能抛出一个链接错误,让程序员修改代码。

那么,针对 int 的版本为什么能够找到定义呢?因为在 bubble_sort() 函数中调用了 int 版本的 Swap,这样做的结果是:编译生成的 func.obj 中会有一个 int 版本的 Swap() 函数定义。编译器在编译 main.cpp 时虽然找不到 int 版本的实例,但是等到链接程序时,连接器在 func.obj 中找到了,所以针对 int 版本的调用就不会出错。

将类模板的声明和实现分散到不同的文件

我们再看一个类模板的反面教材,它将类模板的声明和实现分别放到了头文件和源文件。

point.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#ifndef _POINT_H
#define _POINT_H

template<class T1, class T2>
class Point {
public:
Point(T1 x, T2 y): m_x(x), m_y(y) {}

public:
T1 getX() const { return m_x; }
void setX(T1 x) { m_x = x; }
T2 getY() const { return m_y; }
void setY(T2 y) { m_y = y; }
void display() const;
private:
T1 m_x;
T2 m_y;
};

#endif

point.cpp

1
2
3
4
5
6
7
8
#include <iostream>
#include "point.h"
using namespace std;

template<class T1, class T2>
void Point<T1, T2>::display() const {
cout << "x=" << m_x << ", y=" << m_y << endl;
}

main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include "point.h"
using namespace std;

int main()
{
Point<int, int> p1(10, 20);
p1.setX(40);
p1.setY(50);
cout << "x=" << p1.getX() << ", y=" << p1.getY() << endl;

Point<char*, char*> p2("东经180度", "北纬210度");
p2.display();

return 0;
}

该工程包含了两个源文件和一个头文件,point.h 中声明了类模板,point.cpp 中对类模板进行了实现,main.cpp 中通过类模板创建了对象,并调用了成员函数,这是典型的将类的声明和实现分离的编程模式。

运行上面的程序,会产生一个链接错误,意思是无法通过 p2 调用 Point<char*, char*>::display() const 这个函数。

类模板声明位于 point.h 中,它包含了所有成员变量的定义以及构造函数、get 函数、set 函数的定义,这些信息足够创建出一个完整的对象了,并且可以通过对象调用 get 函数和 set 函数,所以 main.cpp 的前 11 行代码都不会报错。而第 12 行代码调用了 display() 函数,该函数的定义位于 point.cpp 文件中,并且 point.cpp 中也没有生成对应的实例,所以会在链接期间抛出错误。

总结

通过上面两个反面教材可以总结出,不能将模板的声明和定义分散到多个文件中的根本原因是:模板的实例化是由编译器完成的,而不是由链接器完成的,这可能会导致在链接期间找不到对应的模板实例。

前面讲到的模板的实例化是在调用函数或者创建对象时由编译器自动完成的,不需要程序员引导,因此称为 隐式实例化。相对应的,我们也可以通过代码明确地告诉编译器需要针对哪个类型进行实例化,这称为 显式实例化

编译器在实例化的过程中需要知道模板的所有细节:对于函数模板,也就是函数定义;对于类模板,需要同时知道类声明和类定义。我们必须将显式实例化的代码放在包含了模板定义的源文件中,而不是仅仅包含了模板声明的文件中。

函数模板的显式实例化

以上节讲到的 Swap() 函数为例,针对 double 类型的显式实例化代码为:

1
template void Swap(double &a, double &b);

这行代码由两部分组成,前边是一个 template 关键字(后面不带 <>),后面是一个普通的函数原型,组合在一起的意思是:将模板实例化成和函数原型对应的一个具体版本。

将该代码放到 func.cpp 文件的最后,再运行程序就不会再出错了。

另外,还可以在包含了函数调用的源文件(main.cpp)中再增加下面的一条语句:

1
extern template void Swap(double &a, double &b);

该语句在前面增加了 extern 关键字,它的作用是明确地告诉编译器,该版本的函数实例在其他文件中,请在链接期间查找。不过这条语句是多余的,即使不写,编译器发现当前文件中没有对应的模板定义,也会自动去其他文件中查找。

上节我们展示了一个反面教材,告诉大家不能把函数模板的声明和定义分散到不同的文件中,但是现在有了显式实例化,这一点就可以做到了,下面就上节的代码进行修复。

func.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//交换两个数的值
template<typename T> void Swap(T &a, T &b){
T temp = a;
a = b;
b = temp;
}

//冒泡排序算法
void bubble_sort(int arr[], int n){
for(int i=0; i<n-1; i++){
bool isSorted = true;
for(int j=0; j<n-1-i; j++){
if(arr[j] > arr[j+1]){
isSorted = false;
Swap(arr[j], arr[j+1]); //调用Swap()函数
}
}
if(isSorted) break;
}
}

template void Swap(double &a, double &b); // 显式实例化定义

func.h

1
2
3
4
5
6
7
#ifndef _FUNC_H
#define _FUNC_H

template<typename T> void Swap(T &a, T &b);
void bubble_sort(int arr[], int n);

#endif

main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include "func.h"
using namespace std;

//显示实例化声明(也可以不写)
extern template void Swap(double &a, double &b);
extern template void Swap(int &a, int &b);

int main(){
int n1 = 10, n2 = 20;
Swap(n1, n2);

double f1 = 23.8, f2 = 92.6;
Swap(f1, f2);
return 0;
}

显式实例化也包括声明和定义,定义要放在模板定义(实现)所在的源文件,声明要放在模板声明所在的头文件(当然也可以不写)。

类模板的显式实例化

类模板的显式实例化和函数模板类似。以上节的 Point 类为例,针对 char* 类型的显式实例化(定义形式)代码为:

1
template class Point<char*, char*>;

相应地,它的声明形式为:

1
extern template class Point<char*, char*>;

不管是声明还是定义,都要带上 class 关键字,以表明这是针对类模板的。

另外需要注意的是,显式实例化一个类模板时,会一次性实例化该类的所有成员,包括成员变量和成员函数。

有了类模板的显式实例化,就可以将类模板的声明和定义分散到不同的文件中了,下面我们就来修复上节的代码。

point.cpp

1
2
3
4
5
6
7
8
9
10
11
12
#include<iostream>
#include "point.h"
using namespace std;

template<class T1, class T2>
void Point<T1, T2>::display() const {
cout << "x=" << m_x << ", y=" << m_y << endl;
}

// 显式实例化定义
template class Point<char*, char*>;
template class Point<int, int>;

point.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#ifndef _POINT_H
#define _POINT_H

template<class T1, class T2>
class Point{
public:
Point(T1 x, T2 y): m_x(x), m_y(y){ }
public:
T1 getX() const{ return m_x; }
void setX(T1 x){ m_x = x; }
T2 getY() const{ return m_y; };
void setY(T2 y){ m_y = y; };
void display() const;
private:
T1 m_x;
T2 m_y;
};

#endif

main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include "point.h"
using namespace std;

//显式实例化声明(也可以不写)
extern template class Point<char*, char*>;
extern template class Point<int, int>;

int main(){
Point<int, int> p1(10, 20);
p1.setX(40);
p1.setY(50);
cout<<"x="<<p1.getX()<<", y="<<p1.getY()<<endl;

Point<char*, char*> p2("东京180度", "北纬210度");
p2.display();

return 0;
}

总结

函数模板和类模板的实例化语法是类似的,我们不妨对它们做一下总结:

1
2
extern template declaration; // 实例化声明
template declaration; // 实例化定义

对于函数模板来说,declaration 就是一个函数原型;对于类模板来说,declaration 就是一个类声明。

显式实例化的缺陷

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
template<class T1, class T2>
class A {
T1 v1;
T2 v2;
};

template<class T1, class T2>
class B : public A<T2, T1> {
T1 v3;
T2 V4;
};

template<class T>
class C : public B<T, T> {
T v5;
};

int main() {
B<int, double> obj1;
C<int> obj2;

return 0;
}

编译到第 30 行,编译器用 int 替换类模板 B 中的 T1,用 double 替换 T2,生成 B<int, double> 类如下:

1
2
3
4
5
class B<int, double>: public A <double, int>
{
int v3;
double v4;
};

B<int, double> 的基类是 A<double, int>。于是编译器就要用 double 替换类模板 A 中的 T1,用 int 替换 T2,生成 A<double, int> 类如下:

1
2
3
4
class A<double, int> {
double v1;
int v2;
};

编译到第 31 行,编译器生成类 C,还有 C 的直接基类 B<int, int>,以及 B<int, int> 的基类 A<int, int>。

类模板从模板类派生

1
2
3
4
5
6
7
8
9
10
11
12
13
template<class T1, class T2>
class A {
T1 v1;
T2 v2;
};
template<class T>
class B: public A<int, double>{
T v;
};
int main() {
B<char> obj1;
return 0;
}

A<int, double> 是一个具体类的名字,而且它是一个模板类,因此说模板 B 是从模板类派生而来的。

编译器编译到 B<char> obj1; 时会自动生成两个模板类:A<int, double>B<char>

类模板从普通类派生

1
2
3
4
5
6
7
class A{ int v1; };
template<class T>
class B: public A {T v;};
int main() {
B<char> obj1;
return 0;
}

普通类从模板类派生

1
2
3
4
5
6
7
8
9
10
11
12
template<class T>
class A{
T v1;
int n;
};
class B: public A<int> {
double v;
};
int main() {
B obj1;
return 0;
}