一、泛型编程
假设我们想实现一个交换函数,并且支持不同类型的参数实现,我们可以用 typedef 将类型进行重命名,例如以下代码:
// 将 int 起别名为 DataType typedef int DataType; void Swap(DataType& x, DataType& y) { DataType tmp = x; x = y; y = tmp; } int main() { DataType x = 0, y = 6; Swap(x, y); return 0; }
这样我们每次需要更换类型的时候,只需要更改 int 为其他类型即可;
以上是一种方法,还有一种方法可以使用函数重载实现,例如:
void Swap(int& left, int& right) { int temp = left; left = right; right = temp; } void Swap(double& left, double& right) { double temp = left; left = right; right = temp; } void Swap(char& left, char& right) { char temp = left; left = right; right = temp; }
以上两种方法虽然可以实现通用的交换函数,但是有以下几个不好的地方:
- 重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增加对应的函数或修改类型。
- 代码的可维护性比较低,一个出错可能所有的重载均出错。
那能否告诉编译器一个模板,让编译器根据不同的类型利用该模板来生成代码呢?答案是可以的,在这里就需要引入泛型编程,泛型编程: 编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。
模板分为函数模板和类模板。
二、函数模板
1. 函数模板的概念
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
2. 函数模板格式
在定义函数模板之前,我们需要引入一个关键字:template,它是定义模板的关键字;
使用格式:template<typename T1, typename T2,......,typename Tn>
,在 template 关键字后面要用尖括号括住模板参数,模板参数的数量可以是任意的,但是需要使用 typename 关键字来定义模板参数,也可以使用 class(切记:不能使用struct代替class)。
例如交换函数的函数模板:
template<typename T> void Swap(T& t1, T& t2) { T tmp = t1; t1 = t2; t2 = tmp; }
3. 函数模板的原理
函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器。
例如下图就很好地体现了这一个过程:
在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。
比如:当用 double 类型使用函数模板时,编译器通过对实参类型的推演,将 T 确定为 double 类型,然后产生一份专门处理 double 类型的代码,对于字符类型也是如此,即编译器用模板实例化生成对应的Swap 函数。
4. 函数模板的实例化
用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化。
隐式实例化: 让编译器根据实参推演模板参数的实际类型,例如以下这个 Add 的函数模板,实现两个数的相加:
template<class T> T Add(T a, T b) { return a + b; } int main() { int a = 10, b = 20; double c = 1.11, d = 2.22; cout << "sum = " << Add(a, b) << endl; cout << "sum = " << Add(c, d) << endl; return 0; }
上面的两个调用实例化都没有问题,编译器进行了隐式实例化,运行的结果如下:
但是如果这样调用会编译通过吗:Add(a, d)
,答案是不行的,通过实参 a 将 T 推演为 int,通过实参 d 将 T 推演为 double 类型,但模板参数列表中只有一个 T, 编译器无法确定此处到底该将 T 确定为 int 或者 double 类型而报错。
所以此时有两种解决方法:
- 用户自己来强制转化
- 使用显式实例化
如果自己来强制转化,就可以使用以下方法:
int main() { int a = 10, b = 20; double c = 1.11, d = 2.22; cout << "sum = " << Add((double)a, d) << endl; cout << "sum = " << Add(a, (int)d) << endl; return 0; }
我们可以在调用 Add 函数时,将 a 强转为 double,或者将 d 强转为 int 。
显式实例化: 在函数名后的<>中指定模板参数的实际类型。
例如上面的问题中,我们使用显式实例化解决,代码如下:
int main() { int a = 10, b = 20; double c = 1.11, d = 2.22; cout << "sum = " << Add(a, b) << endl; cout << "sum = " << Add(c, d) << endl; cout << "sum = " << Add<double>(a, d) << endl; cout << "sum = " << Add<int>(a, d) << endl; return 0; }
我们在函数名的后面用尖括号指定了模板参数的类型,这就是显式实例化。
注意:如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错。
5. 模板参数的匹配原则
对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板。
例如以下两段代码:
// 专门处理int的加法函数 int Add(int a, int b) { cout << "int Add(int a, int b)" << endl; return a + b; } // 通用加法函数 template<class T> T Add(T a, T b) { cout << "T Add(T a, T b)" << endl; return a + b; } int main() { // 与非函数模板类型完全匹配,不需要函数模板实例化 Add(1, 2); // 模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的 Add 函数 Add<int>(1, 2); return 0; }
三、类模板
假设我们我们需要实现一个通用的栈,我们可以使用 typedef 关键字对类型起别名,每次需要改变类型的时候,只需要在 typedef 更改即可,例如以下的 Stack 类:
typedef int DataType; class Stack { public: Stack(size_t capacity = 4) { _array = new DataType[capacity]; _capacity = capacity; _size = 0; } ~Stack() { cout << "~Stack()" << endl; delete[] _array; _array = nullptr; _size = _capacity = 0; } private: // 内置类型 DataType* _array; int _capacity; int _size; };
虽然以上的 Stack 类不同的类型只需要改变 typedef 的类型即可,但是如果我同时需要两个栈,一个栈的参数是 int ,另一个栈的参数是 double 呢,上面的方法就不能很好地满足了,所以我们引入类模板。
类模板的使用如下,以 Stack 类为例:
template<class T> class Stack { public: Stack(size_t capacity = 4) { _array = new T[capacity]; _capacity = capacity; _size = 0; } ~Stack() { delete[] _array; _array = nullptr; _size = _capacity = 0; } private: T* _array; int _capacity; int _size; };
实例化对象如下:
int main() { Stack<int> st1; Stack<double> st2; return 0; }
注意,Stack 是类名,Stack<int>
和 Stack<double>
才是类型;template 的作用范围是 Stack 这个类。
这样我们就同时实现了两个栈,一个栈存放的参数是 int,另外一个存放的是 double。