【C++】模板(初阶)
一、模板与泛型编程
我们先来思考一下:如何实现一个通用的交换函数?
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;
}
......
可以看到一个交换函数可以有很多个函数重载,虽然这样我们可以用,但是这样写有几个问题:
- 重载函数的仅仅是类型不同,它们的逻辑都是一样的,代码复用率低,只要有新类型出现,就还需要再重新写一个函数。
- 代码的可维护性比较低,一个出错就可能导致其他所有的重载出错。
那我们能否给编译器一个模板,让编译器根据不同的类型然后用这个模板生成对应的代码呢?
答案是肯定的,在 C++ 中是有这样的模板供我们使用的。而所谓的泛型编程就是编写与类型无关的通用代码,是代码复用的一种手段,模板则是泛型编程的基础。
模板分为函数模板和类模板。
二、函数模板
1. 函数模板的概念
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
2. 函数模板的使用
函数模板的使用格式如下:
/*
template<typename T1, typename T2,......,typename Tn>
返回值类型 函数名(参数列表)
{// ...
}
*/
注:
typename
是用来定义模板参数关键字,也可以写class
。
所以我们的 Swap
函数就可以写出下面的一个模板形式:
template<typename T>
// template<class T>
void Swap(T& left, T& right)
{T temp = left;left = right;right = temp;
}
注:这里的
T
只是一个名称,你可以写成任意的名字,比如x
、y
等等。
3. 函数模板的原理
当我们写好了一个 Swap
函数的模板的时候,我们可以直接去调用这个函数,并且支持不同的类型。
#include<iostream>using namespace std;template<typename T>
// template<class T>
void Swap(T& left, T& right)
{T temp = left;left = right;right = temp;
}int main()
{int a = 1, b = 2;Swap(a, b);double x = 1.1, y = 2.2;Swap(x, y);cout << a << " " << b << endl;cout << x << " " << y << endl;return 0;
}
那么它的原理是什么呢?实际上,编译接收到这几个函数之后,它会根据你已经写好的模板推演出所需要的类型然后生成对应的函数,这个过程叫做推演实例化。本质就是把我们干的活交给了编译器。
4. 函数模板的实例化
用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化。
(1) 隐式实例化
template<typename T>
T Add(const T& a, const T& b)
{return a + b;
}int main()
{int x1 = 1, x2 = 2;double y1 = 1.1, y2 = 2.2;cout << Add(x1, x2) << endl;cout << Add(y1, y2) << endl;return 0;
}
在上面的代码中,我们调用函数的时候并没有给出 x1
、x2
和 y1
、y2
的类型,而是让编译器自己去推演所需要的类型生成对应的函数,这个方式就是隐式实例化。
隐式实例化:让编译器根据实参推演模板参数的实际类型。
但是这样写有一个致命的问题,如果我们两个参数的类型不一致,编译就不通过。比如说我们写的是 Add(x1, y1)
,那么左边的参数类型是 int,右边的是 double,但是我们的模板中只有一个 T,因此编译器不知道这个 T 到底是 int 还是 double。
对于这种情况,我们有两种解决方案,第一种方式是进行强制转换。
template<typename T>
T Add(const T& a, const T& b)
{return a + b;
}int main()
{int x1 = 1, x2 = 2;double y1 = 1.1, y2 = 2.2;cout << Add(x1, (int)y1) << endl;cout << Add((double)x1, y1) << endl;return 0;
}
第二种方式就是使用显式实例化。
(2) 显式实例化
显式实例化:在函数名后的
<>
中指定模板参数的实际类型。
int main()
{int x1 = 1, x2 = 2;double y1 = 1.1, y2 = 2.2;cout << Add<int>(x1, y1) << endl;cout << Add<double>(x1, y1) << endl;return 0;
}
在有的时候无法推演出模板种参数 T
的类型的时候,我们就可以使用显式实例化,比如下面这种场景。
template<class T>
T* func(int n)
{T* ptr = new T[n];return ptr;
}int main()
{// func(10); // 这里就完全不知道 T 该是什么func<int>(10); // 因此使用显式实例化return 0;
}
5. 模板参数的匹配原则
一个函数的模板可以与它的同名的非模板的函数同时存在,相当于一个模板可以与它实例化出来的函数同时存在,并且这个模板函数还是还可以被实例化为这个非模板函数。
template<typename T>
T Add(const T& a, const T& b)
{return a + b;
}int Add(const int& a, const int& b) // OK
{return a + b;
}
那么我们在调用的时候到底调用的是哪一个函数呢?
template<typename T>
T Add(const T& a, const T& b)
{return a + b;
}int Add(const int& a, const int& b)
{return a + b;
}int main()
{int x = 1, y = 2;Add(x, y); // 调用的是非模板的 AddAdd<int>(x, y); // 调用的是模板实例化出来的 Addreturn 0;
}
- 也就是说我们有现成的函数(已经写好的)就用现成的,像用这种
<int>
显式实例化的方式就相当于我们指定了要用模板,所以就调用模板实例化出来的函数。
- 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。但是如果模板可以产生一个具有更好匹配的函数, 那么将选择模板。
template<typename T1, typename T2>
T2 Add(const T1& a, const T2& b)
{return a + b;
}// 专用于 int 类型的加法
int Add(const int& a, const int& b)
{return a + b;
}int main()
{int x = 1;double y = 2.2;cout << Add(x, y); // 会调用模板实例化出来的 Addreturn 0;
}
这是因为虽然这里调用现成的 Add
和模板的 Add
都可以,但是如果调用现成的 Add
还要自动类型转换。所以在这种情况下,会优先匹配一个更好的,也就是模板。
三、类模板
1. 类模板的使用
template<class T1, class T2, ..., class Tn> // class 写 typename 也可以
class 类模板名
{// ...
};
举个例子,栈模板的实现如下:
template<typename T>
class Stack
{
public:// ...
private:T* _array;size_t _capacity;size_t _size;
};int main()
{Stack<int> st1; // 存 int 的数据Stack<double> st2; // 存 double 的数据 // 注意: Stack是类名,Stack<int> 和 Stack<double> 才是类型// ...return 0;
}
这和我们以前使用的 typedef
有点像,我们可以把这里的 T
换成在外面用 typedef
来指定一个类型。但是模板的做法明显更好,因为一个模板就可以实现不同的栈存储不同的数据类型,但是 typedef
就做不到,除非写两份。
当在同一个文件中,类模板中函数的声明和定义分离时我们需要在函数定义处重新声明模板和指定类域。
template<class T>
class Stack
{
public:// ...
private:T* _array;size_t _capacity;size_t _size;
};template<class T>
void Stack<T>::Push(const T& data)
{// ...
}
不建议把类模板中函数的声明和定义分离到两个文件,因为这样会报链接错误,具体原因在模板进阶讲。