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