目录
一、前言
二、函数模板
2.1 - 基本概念和原理
2.2 - 定义格式
2.3 - 实例化详解
2.3.1 - 隐式实例化
2.3.2 - 显示实例化
2.4 - 模板参数的匹配原则
三、类模板
3.1 - 定义格式
3.2 - 实例化
参考资料:
C++函数模板(模板函数)详解 (biancheng.net)。
C++函数模板5分钟入门教程 (biancheng.net)。
C++类模板5分钟入门教程 (biancheng.net)。
一、前言
问题:如何实现一个通用的交换函数?
// 交换两个 int 类型变量的值
void Swap(int& x, int& y)
{int tmp = x;x = y;y = tmp;
}
// 交换两个 double 类型变量的值
void Swap(double& x, double& y)
{double tmp = x;x = y;y = tmp;
}
// 交换两个 char 类型变量的值
void Swap(char& x, char& y)
{char tmp = x;x = y;y = tmp;
}
// ... ...
虽然我们可以重载很多个函数,以满足交换不同类型变量的需求,但是这种方法不太高明:
-
重载的函数除了形参
x
和y
以及函数体中临时变量tmp
的数据类型不同,其他的代码都一样。 -
代码的可维护性也比较低,一个出错可能所有的重载均出错。
那么能否只写一遍 Swap 函数,就能用来交换各种类型变量的值呢?因此 "模板" 的概念就应运而生了。
众所周知,有了 "模子" 后,用 "模子" 来批量制造陶瓷、塑料、金属制品就变得容易了。程序设计语言中的模板就是用来批量生成功能和形式都几乎相同的代码的。有了模板,编译器就能在需要的时候,根据模板自动生成程序的代码。从同一个模板自动生成的代码,形式几乎是一样的。
模板是泛型编程的基础,泛型编程即以一种独立于任何特定类型的方式编写代码。
二、函数模板
2.1 - 基本概念和原理
所谓函数模板(Function Template),实际上是建立一个通用函数,它所用到的数据类型(包括形参类型、局部变量类型、返回值类型)可以不具体指定,而是用一个虚拟的类型来代替(实际上是用一个标识符来占位)。编译的时候,编译器推导实参的数据类型,根据实参的数据类型和函数模板,生成特定的函数定义。
在函数模板中,数据的值(value)和类型(type)都被参数化了。
生成函数定义的过程被称为实例化。
2.2 - 定义格式
template<typename T1. typename T2, ...>
返回值类型 函数名(参数列表)
{函数体
}
注意:
-
T1、T2、... 是类型参数(也可以说是虚拟的类型,或者说是类型占位符),类型参数的命名规则跟其他标识符的命名规则一样,不过使用 T、T1、T2、Type 等已经成为了一种习惯。
-
其中
typename
关键字也可以用class
关键字替换,它们没有任何区别。C++ 早期对模板的支持并不严谨,没有引入新的关键字,而是用
class
来指明类型参数,但是class
关键字本来已经用在类的定义中了,这样做显得不太友好,所以后来 C++ 又引入了一个新的关键字typename
,专门用来定义类型参数。不过至今仍然有很多代码在使用 class 关键字,包括 C++ 标准库、一些开源程序等。
#include <iostream>
using namespace std;
// template<typename T>
// 或者
template<class T>
void Swap(T& x, T& y)
{T tmp = x;x = y;y = tmp;
}
int main()
{int a = 10, b = 20;Swap(a, b);cout << a << " " << b << endl; // 20 10double c = 1.1, d = 2.2;Swap(c, d);cout << c << " " << d << endl; // 2.2 1.1
char e = 'm', f = 'n';Swap(e, f);cout << e << " " << f << endl; // n mreturn 0;
}
编译器会根据 Swap 模板自动生成多个 Swap 函数,用来交换不同类型变量的值。
函数模板中定义的类型参数不仅可以用在函数定义中,还可以用在函数声明中。
#include <iostream>
using namespace std;
template<typename T>
void Swap(T& x, T& y);
int main()
{int a = 10, b = 20;Swap(a, b);cout << a << " " << b << endl; // 20 10return 0;
}
template<typename T>
void Swap(T& x, T& y)
{T tmp = x;x = y;y = tmp;
}
2.3 - 实例化详解
2.3.1 - 隐式实例化
让编译器根据实参类型推演模板参数的实际类型。
#include <iostream>
using namespace std;
template<typename T>
T Add(const T& x, const T& y)
{return x + y;
}
int main()
{int a = 10, b = 20;cout << Add(a, b) << endl; // 30
double c = 1.1, d = 2.2;cout << Add(c, d) << endl; // 3.3return 0;
}
注意:
cout << Add(a, c) << endl;
// error:"T Add(const T &,const T &)": 模板 参数 "T" 不明确
上面的语句不能通过编译,是因为在编译期间,编译器通过实参 a 将 T 推演为
int
类型,通过实参 c 将 T 推演为double
类型,而模板参数列表中只有一个 T,编译器无法确定此处到底该将 T 确定为int
还是double
类型而报错。在模板中,编译器一般不会进行类型转换操作,因为一旦转换出问题,编译器就需要 "背黑锅"。
解决方案一:
cout << Add((double)a, c) << endl; // 11.1
// 或者
cout << Add(a, (int)c) << endl; // 11
// 以 (int)c 为例,c 强转类型转换后传参的过程大概可以分为以下几个步骤:
// 1. 在另外的地方找一个内存构造一个临时变量 tmp
// 2. 将 c 的整数部分赋值给 tmp
// 3. 用 tmp 传参
// 4. 销毁 tmp
// 因为临时变量具有常性,所以 Add 函数的形参 x 和 y 必须用常引用
解决方案二:
#include <iostream>
using namespace std;
template<typename T1, typename T2>
T1 Add(const T1& x, const T2& y)
{return x + y;
}
int main()
{int a = 10;double c = 1.1;cout << Add(a, c) << endl; // 11// 若 Add 函数的返回值类型为 T2,则输出的结果为 11.1return 0;
}
解决方案三:显示实例化。
2.3.2 - 显示实例化
在函数名后的 <> 中指定模板参数的实际类型。
cout << Add<double>(a, c) << endl; // 11.1
cout << Add<int>(a, c) << endl; // 11
如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器就会报错。
更常见的使用场景:
#include <iostream>
using namespace std;
template<typename T>
T* Alloc(int n)
{return new T[n];
}
int main()
{int* arr = Alloc<int>(5);for (int i = 0; i < 5; ++i){arr[i] = i;}for (int i = 0; i < 5; ++i){cout << arr[i] << " ";}cout << endl;// 0 1 2 3 4delete[] arr;return 0;
}
2.4 - 模板参数的匹配原则
-
一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数:
#include <iostream> using namespace std; void Swap(int& x, int& y) {int tmp = x;x = y;y = tmp; } template<typename T> void Swap(T& x, T& y) {T tmp = x;x = y;y = tmp; } int main() {int a = 10, b = 20;Swap(a, b); // 与模板函数匹配,编译器不需要特化cout << a << " " << b << endl; // 10 20 Swap<int>(a, b); // 调用编译器特化的 Swap 版本cout << a << " " << b << endl; // 20 10return 0; }
-
对于非模板函数和同名的函数模板,如果其他条件都相同,在调用时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数,那么将选择模板。
#include <iostream> using namespace std; int Add(int x, int y) {return x + y; } template<typename T1, typename T2> T1 Add(const T1& x, const T2& y) {return x + y; } int main() {int a = 10, b = 20;cout << Add(a, b) << endl; // 30// 与非模板函数完全匹配,不需要函数模板实例化double c = 1.1;cout << Add(a, c) << endl; // 11// 模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的 Add 函数return 0; }
三、类模板
3.1 - 定义格式
C++ 除了支持函数模板,还支持类模板(Class Template)。函数模板中定义的类型参数可以用在函数声明和函数定义中,类模板中定义的类型参数也可以用在类声明和类实现中。类模板的目的同样是将数据的类型参数化。
声明类模板的格式为:
template<class T1, class T2, ...>
class 类名
{// TODO;
};
例如:
template<class T>
class Stack
{
public:Stack(int default_capacity = 5): _data(new T[default_capacity]), _top(0), _capacity(default_capacity){}~Stack();
private:T* _data;int _top;int _capacity;
};
上面的代码仅仅是类的声明,我们还需要在类外定义成员函数。在类外定义成员函数时仍然需要带上模板头,格式为:
template<class T>
Stack<T>::~Stack()
{if (_data)delete[] _data;_top = _capacity = 0;
}
注意:除了 template 关键字后面要指明类型参数,类名 Stack 后面也要带上类型参数,只是不加
class
或typename
关键字了。
3.2 - 实例化
与函数模板不同的是,类模板实例化需要在类模板名字后面跟 <>,然后将实例化的类型放在 <> 中,即类模板必须显示实例化,因为编译器不能根据给定的数据推演出数据类型。
类模板名字不是真正的类,而实例化的结果才是真正的类。
Stack<int> st1;
Stack<double> st2;
Stack<int>* p1 = new Stack<int>(10);
Stack<double>* p2 = new Stack<double>(15);
对于普通类,类名和类型是一样的;对于模板类,类名和类型不一样,在上面的例子中,类名是
Stack
,类型则是Stack<int>/ Stack<double>
。