前言:为什么 C++ 要有模板?
我们可以从一个非常简单的 Swap交换函数 开始。
如果没有模板,为了交换不同类型的数据,我们可能写:
cpp
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;
}
你会发现,这几个函数除了:
cpp
int
double
char
不同之外,算法完全一样:
cpp
temp = left
left = right
right = temp
这就产生两个问题
第一,代码复用率低。每来一个新类型,就要再写一遍
第二,可维护性差。如果交换逻辑需要修改,就可能每一个重载版本都要改
所以我们真正想要的是:
cpp
我只写一次"交换算法"
↓
至于到底交换 int、double、char
↓
让编译器自己生成对应版本
这就是模板思想
一 什么是泛型编程?
在具体讲解模板之前,我们先了解一下泛型编程
泛型编程是编写与类型无关的通用代码,是代码复用的一种手段。模板就是泛型编程的基础。
以前我们写:
cpp
int Add(int x, int y);
这个函数只能处理 int。
而泛型编程想表达的是:关心的是"怎么加",而不是"加的是哪种具体类型"
于是我们把"类型"先抽象出来。
你可以把模板想象成模具":
同一个模具
↓
放入不同材料
↓
得到不同成品
在 C++ 里就是:
同一个模板
↓
传入不同类型
↓
生成不同类型的代码
二 函数模板到底是什么?
我们先了解最基本的函数模板:
cpp
template<typename T>
void Swap(T& left, T& right)
{
T temp = left;
left = right;
right = temp;
}
函数模板代表一个函数家族,它与具体类型无关,使用时根据实参类型产生对应的具体函数版本
注意这里:
cpp
T
现在并不是某个真正的具体类型。
它只是一个:
类型占位符。
你可以理解成:
T = "以后再告诉我是什么类型"
于是:
cpp
T temp = left;
不是说存在一种真正叫 T 的数据类型,而是说:
cpp
如果以后 T 是 int
这里就是 int temp
如果 T 是 double
这里就是 double temp
如果 T 是 char
这里就是 char temp
三 template<typename T> 又是什么?
拆开来看:
cpp
template<typename T>
意思可以理解成:
我接下来定义的是一个模板,其中有一个类型参数,名字叫
T
然后:
cpp
void Swap(T& left, T& right)
表示:
left 和 right 的类型先不确定,由以后调用时再决定。
模板参数也可以有多个:
cpp
template<typename T1, typename T2>
比如:
cpp
template<typename T1, typename T2>
T1 Add(T1 x, T2 y)
{
return x + y;
}
此时:
cpp
T1
T2
可以是不同类型。
四 typename 和 class 有什么区别?
我们在之前学过了类与对象,初步了解到了class的相关知识,其实在这个地方,我们也可以用class来定义模板类型参数的
所以:
cpp
template<typename T>
也可以写:
cpp
template<class T>
所以:
cpp
template<typename T>
和:
cpp
template<class T>
在这个阶段可以看成一样
五 模板本身并不是一个真正的函数
函数模板是一个蓝图,本身并不是函数,而是编译器根据使用方式生成具体函数的模具
例如你写:
cpp
template<typename T>
void Swap(T& a, T& b)
{
T tmp = a;
a = b;
b = tmp;
}
这时不要理解成:已经生成了一个万能 Swap 函数
更准确地说:现在只有一张"生成函数的图纸"
然后:
cpp
int a = 1, b = 2;
Swap(a, b);
编译器看到:两个参数都是 int
于是推导:
cpp
T = int
然后生成一个类似:
cpp
void Swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
如果又写:
cpp
double x = 1.1, y = 2.2;
Swap(x, y);
编译器又推导:
cpp
T = double
生成类似:
cpp
void Swap(double& a, double& b)
{
double tmp = a;
a = b;
b = tmp;
}
所以真正过程是:
cpp
函数模板
↓
根据实参推导 T
↓
生成具体函数
↓
调用具体函数
六 模板实例化
使用不同类型参数调用函数模板时,产生具体版本的过程称为函数模板实例化
例如:
cpp
Swap(a, b);
如果:
cpp
a、b 是 int
就产生:
cpp
Swap<int>
这就是一次实例化。
如果:
cpp
double x, y;
Swap(x, y);
又产生:
cpp
Swap<double>
函数模板实例化又分两种:
隐式实例化
显式实例化
1. 隐式实例化:让编译器自己猜 T
例如:
cpp
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
然后:
cpp
int a1 = 10, a2 = 20;
double d1 = 10.0, d2 = 20.0;
Add(a1, a2);
Add(d1, d2);
第一句:
cpp
Add(a1, a2);
编译器看到:
cpp
a1 → int
a2 → int
于是推导:
cpp
T = int
第二句:
cpp
Add(d1, d2);
推导:
cpp
T = double
这就是:隐式实例化
你没有写:
cpp
Add<int>
也没有写:
cpp
Add<double>
而是编译器自己根据实参类型推导
七 函数模板最容易出错的地方:同一个 T 推导出了两个结果
考虑:
cpp
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
然后:
cpp
int a = 10;
double d = 20.0;
Add(a, d);
这时候编译器开始推导。
根据:a
得到:
cpp
T = int
但是根据:d
又得到:
cpp
T = double
问题来了:T到底是谁?
模板只有:T一个模板参数。
它不可能同时:
cpp
T = int
T = double
所以编译器无法确定具体类型,会产生错误
八 解决混合类型问题
1.方法一:自己转换
比如:
cpp
int a = 10;
double d = 20.0;
可以:
cpp
Add(a, (int)d);
现在参数变成:
cpp
int
int
于是:
cpp
T = int
没有冲突
2.方法二:显式指定模板参数
另一种方法:
cpp
Add<int>(a, d);
这里:
cpp
<int>
相当于直接告诉编译器:
不需要你推导了,我明确指定
T = int。
这就是:显式实例化
在函数名后面的
<>中直接指定模板参数的实际类型
所以:
cpp
Add<int>(a, d);
相当于指定:
cpp
T = int
于是模板生成:
cpp
int Add(const int& left, const int& right);
此时第二个参数原本是:
cpp
double d
因为目标参数类型已经确定为 int,编译器会尝试进行类型转换。
显式实例化之后,如果实际参数与指定类型不完全相同,编译器会尝试隐式转换,无法转换才会报错
3.方法三:使用两个模板参数
cpp
template<class T1, class T2>
T1 Add(T1 left, T2 right)
{
return left + right;
}
这时候:
cpp
Add(1, 2.0);
就不再要求两个参数必须对应同一个 T。
而是:
cpp
T1 = int
T2 = double
所以能够成功实例化
九 普通函数和函数模板
1.能不能同名?
答案是可以
比如:
cpp
int Add(int left, int right)
{
return left + right;
}
template<class T>
T Add(T left, T right)
{
return left + right;
}
这里同时存在:
cpp
普通函数 Add
函数模板 Add
是允许的
2.谁先被调用?
上方代码中同时存在:
cpp
普通函数 Add
函数模板 Add
那么到底调用谁?
答案是:普通函数和模板一样匹配时,优先普通函数
如果非模板函数和函数模板都能匹配,并且匹配程度相同,那么优先调用非模板函数。 06.模板初阶
所以:
cpp
int Add(int left, int right);
template<class T>
T Add(T left, T right);
执行:
cpp
Add(1, 2);
两个都能处理。
普通函数:
Add(int, int)
直接完美匹配。
模板也可以实例化:
Add<int>(int, int)
也完全匹配。
既然一样好:优先选择普通函数,所以不会没必要地再从模板生成一个版本
3.那如果我们想强制调用模板,该怎么做呢?
1.显示实例化
我们可以写:
cpp
Add<int>(1, 2);
只要你显式写了:
cpp
<int>
就明确表示:我要用模板版本了
2.模板匹配更佳
例如:
cpp
int Add(int left, int right)
{
return left + right;
}
同时:
cpp
template<class T1, class T2>
T1 Add(T1 left, T2 right)
{
return left + right;
}
调用:
cpp
Add(1, 2.0);
普通函数需要把:
cpp
2.0 double
转换成:
cpp
int
才能调用
而模板可以直接生成:
cpp
T1 = int
T2 = double
于是:模板版本匹配得更准确
所以:
如果模板能够生成一个匹配程度更好的函数,就选择模板版本
十 类模板
1.类模板的定义
基本格式:
cpp
template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类成员
};
例如:
cpp
template<typename T>
class Stack
{
public:
Stack(size_t capacity = 4)
{
_array = new T[capacity];
_capacity = capacity;
_size = 0;
}
private:
T* _array;
size_t _capacity;
size_t _size;
};
这里最关键:
cpp
T* _array;
以前:
cpp
int* _array;
只能保存 int。
现在:
cpp
T* _array;
具体存什么,等实例化时决定
2.Stack<T> 到底发生了什么?
例如:
cpp
Stack<int> st1;
编译器看到:
cpp
T = int
那么可以帮助理解成生成了一个具体版本:
cpp
class Stack<int>
{
private:
int* _array;
size_t _capacity;
size_t _size;
};
而:
cpp
Stack<double> st2;
相当于:
cpp
T = double
于是内部:
cpp
double* _array;
所以同一份类模板:
cpp
template<typename T>
class Stack
既能产生:
cpp
Stack<int>
Stack<double>
这就是类模板的意义
3.类模板中 Stack 本身不是一个真正的类型
Stack是类模板名,Stack<int>才是类型
所以:
cpp
Stack st;
按照规则是不完整的
因为编译器不知道:T 是什么?
应该写:
cpp
Stack<int> st1;
或者:
cpp
Stack<double> st2;
4.函数模板和类模板在实例化上的区别
函数模板:
cpp
template<class T>
void Func(T x);
调用:
cpp
Func(10);
编译器通常可以根据:
cpp
10 是 int
自己推导:
cpp
T = int
所以可以不写:
cpp
Func<int>(10);
但是类模板实例化时需要:
cpp
Stack<int>
显式把类型写到:<>里面
5.类模板成员函数放到类外怎么写?
类:
cpp
template<typename T>
class Stack
{
public:
void Push(const T& data);
};
如果想在类外定义push函数可以这么写:
cpp
template<class T>
void Stack<T>::Push(const T& data)
{
...
}
一定注意:
不是:
cpp
void Stack::Push(...)
而是:
cpp
void Stack<T>::Push(...)
为什么?
因为:
cpp
Stack
本身只是模板名字。
我们现在定义的是:
cpp
某个 T 对应的 Stack<T> 的成员函数
所以必须写:
cpp
Stack<T>::
而且前面还要再次声明:
cpp
template<class T>