C++ 模板初阶:函数模板与类模板详解
1. 泛型编程
在实际编程中,经常会遇到这样的情况:算法完全一样,只是处理的数据类型不同
例如实现交换函数:
voidSwap(int&left,int&right){inttemp=left;left=right;right=temp;}voidSwap(double&left,double&right){doubletemp=left;left=right;right=temp;}voidSwap(char&left,char&right){chartemp=left;left=right;right=temp;}这几个函数的逻辑完全一样,唯一的区别就是数据类型不同
这种写法存在两个明显问题:
- 代码重复:出现一种新的类型,就需要重新写一个函数
- 维护成本高:大量重复代码容易出现修改不一致的问题
因此,C++提供了模板
泛型编程
泛型编程就是编写与具体类型无关的通用代码
它的核心思想是:
把类型作为参数,让同一套代码能够处理不同的数据类型
模板就是实现泛型编程的重要工具
可以把模板理解成一个“代码模具”:
模板 ↓ 填入具体类型 ↓ 生成对应类型的代码例如:
Swap模板 ↓ 填入 int ↓ Swap(int&, int&) Swap模板 ↓ 填入 double ↓ Swap(double&, double&) Swap模板 ↓ 填入 char ↓ Swap(char&, char&)这样,原本需要程序员重复编写的代码,可以交给编译器完成
2. 函数模板
2.1 函数模板的概念
函数模板可以理解为一个函数家族
它本身并不是一个真正用于执行的具体函数,而是一个生成具体函数的模板
例如:
template<classT>voidSwap(T&left,T&right){T temp=left;left=right;right=temp;}这里的T就是一个模板参数
当我们使用:
inta=10;intb=20;Swap(a,b);编译器可以根据a、b的类型推导:
T → int于是可以生成一个针对int的Swap
如果:
doublea=1.1;doubleb=2.2;Swap(a,b);那么:
T → double从而生成针对double的版本
2.2 函数模板的基本格式
template<typenameT1,typenameT2,...,typenameTn>返回值类型 函数名(参数列表){// 函数体}最常见的形式:
template<typenameT>voidSwap(T&left,T&right){T temp=left;left=right;right=temp;}其中:
template<typenameT>表示下面定义的是一个模板
T是模板参数
2.3typename和class
定义模板参数时:
template<typenameT>也可以写成:
template<classT>两者在这里含义相同
例如:
template<typenameT>voidSwap(T&a,T&b){T tmp=a;a=b;b=tmp;}等价于:
template<classT>voidSwap(T&a,T&b){T tmp=a;a=b;b=tmp;}但是不能写成:
template<structT>模板类型参数这里使用的是typename或class
3. 函数模板的原理
理解这一部分非常重要
函数模板:
template<classT>TAdd(constT&left,constT&right){returnleft+right;}它本身不是一个具体函数
可以把它看成一份“函数生成规则”
例如:
inta=10;intb=20;Add(a,b);编译器看到实参以后,会推导:
T = int相当于产生一个处理int的具体版本
如果:
doublea=10.0;doubleb=20.0;Add(a,b);则:
T = double产生处理double的具体版本
所以模板的核心思想就是:
程序员只负责写一份通用代码,编译器根据实际使用情况生成具体版本
这个过程发生在编译阶段
4. 函数模板的实例化
使用不同类型的参数调用函数模板时,就会发生函数模板实例化
函数模板实例化主要分为:
- 隐式实例化
- 显式实例化
4.1 隐式实例化
隐式实例化就是:
由编译器根据函数实参自动推导模板参数
例如:
template<classT>TAdd(constT&left,constT&right){returnleft+right;}调用:
inta1=10;inta2=20;doubled1=10.0;doubled2=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>4.2 一个容易出错的情况
例如:
inta=10;doubled=20.0;Add(a,d);如果模板只有一个参数:
template<classT>TAdd(constT&left,constT&right){returnleft+right;}那么编译器会遇到:
第一个参数: a → int 第二个参数: d → double此时同一个T到底应该是什么?
T = int? T = double?无法确定,因此无法正常推导
所以这种调用不能依靠这种形式的模板参数推导
4.3 解决方法一:强制类型转换
可以让两个参数变成相同类型:
Add(a,(int)d);此时:
a → int (int)d → int于是:
T → int模板就可以实例化
4.4 解决方法二:显式实例化
也可以直接告诉编译器:
T 就是int写法:
Add<int>(a,d);这里的:
<int>就是明确指定模板参数
编译器知道:
T = int然后再处理:
double→int如果能够进行转换,就可以完成调用;如果无法转换,则编译报错
5. 模板参数的匹配原则
函数模板和普通函数可以同时存在
例如:
intAdd(intleft,intright){returnleft+right;}template<classT>TAdd(T left,T right){returnleft+right;}此时:
Add(1,2);既可以匹配普通函数:
intAdd(int,int);也可以让模板实例化出:
Add<int>(int,int);但是在其他条件相同的情况下:
Add(1,2);会优先匹配普通函数
也就是说:
普通函数和模板都能完全匹配时,优先使用普通函数
如果模板能够产生一个匹配程度更高的版本,则可能选择模板
5.1 显式指定模板参数
如果明确写:
Add<int>(1,2);那么就是明确要求使用模板,并指定:
T = int因此:
Add(1,2);和:
Add<int>(1,2);含义并不完全相同
前者会参与普通函数与模板之间的匹配
后者明确指定使用模板
6. 模板中的类型转换
模板参数推导时,编译器一般不会为了让模板匹配而随意进行类型转换
例如:
template<classT>TAdd(T left,T right){returnleft+right;}然后:
inta=10;doubleb=20.0;Add(a,b);此时:
T = int和:
T = double产生冲突
不能简单地认为:
int 自动转换成 double然后就可以完成模板参数推导
如果确实希望使用某种类型,可以明确指定:
Add<double>(a,b);或者:
Add(a,(int)b);核心要区分:
普通函数调用 ↓ 可以发生常规的类型转换 模板参数推导 ↓ 不会依靠这种类型转换来解决模板参数推导冲突7. 多模板参数
模板不一定只能有一个类型参数
例如:
template<classT1,classT2>T1Add(T1 left,T2 right){returnleft+right;}这里有两个模板参数:
T1 T2调用:
Add(1,2.0);编译器可以推导:
T1 → int T2 → double因此这种情况下就不会出现前面“两个实参争夺同一个T”的问题
8. 类模板
函数模板解决的是函数通用化问题
而类模板解决的是类通用化问题
例如实现一个栈
栈的基本结构无论存储什么数据,整体逻辑都可以保持一致
可能存:
int double char如果每种类型都重新写一个Stack,会产生大量重复代码
因此可以使用类模板
8.1 类模板的定义格式
基本格式:
template<classT1,classT2,...,classTn>class类模板名{// 类内成员};例如:
template<typenameT>classStack{public:Stack(size_t capacity=4){_array=newT[capacity];_capacity=capacity;_size=0;}voidPush(constT&data);private:T*_array;size_t _capacity;size_t _size;};这里:
T*_array;表示栈底层数组中的数据类型由T决定
如果:
T = int那么相当于:
int*_array;如果:
T = double那么相当于:
double*_array;因此,同一个类模板可以生成不同类型的栈
9. 类模板成员函数的定义
如果类模板的成员函数在类外定义,需要注意模板写法
例如类内声明:
template<classT>classStack{public:voidPush(constT&data);private:T*_array;size_t _capacity;size_t _size;};类外定义:
template<classT>voidStack<T>::Push(constT&data){_array[_size]=data;++_size;}这里有两个非常容易混淆的地方:
template<classT>表示这是一个模板
而:
Stack<T>表示当前定义的是Stack<T>这个类模板的成员函数
所以完整结构:
template<classT>voidStack<T>::Push(constT&data){// ...}10. 类模板为什么通常不建议声明和定义分离
类模板的成员函数通常不建议像普通类那样:
Stack.h Stack.cpp把声明和定义完全分开
例如:
// Stack.htemplate<classT>classStack{public:voidPush(constT&data);};然后:
// Stack.cpptemplate<classT>voidStack<T>::Push(constT&data){// ...}这种写法容易产生链接错误
原因在于模板真正需要的是:
使用某个具体类型实例化模板时,编译器需要看到模板的完整定义
例如:
Stack<int>st;st.Push(10);编译器需要根据:
T = int生成:
Stack<int>::Push(...)如果当前编译单元只看到了模板声明,却看不到完整的模板定义,就无法正常完成对应的实例化
因此实际编程中通常直接把模板的声明和定义放在一起,或者放在.h/.hpp中
11. 类模板的实例化
类模板和函数模板的实例化方式有一个重要区别
函数模板可以通过函数参数进行类型推导:
Swap(a,b);而类模板实例化时必须明确指定类型
例如:
Stack<int>st1;Stack<double>st2;这里:
Stack只是类模板名,并不是一个真正的具体类型
真正的类型是:
Stack<int>和:
Stack<double>可以理解为:
Stack ↓ 类模板 Stack<int> ↓ 具体的 int 类型栈 Stack<double> ↓ 具体的 double 类型栈因此:
Stack<int>st1;表示:
使用 Stack 模板 T = int 生成 Stack<int> 创建对象 st1而:
Stack<double>st2;表示:
使用 Stack 模板 T = double 生成 Stack<double> 创建对象 st212. 函数模板与类模板的核心区别
| 项目 | 函数模板 | 类模板 |
|---|---|---|
| 作用 | 生成通用函数 | 生成通用类 |
| 类型来源 | 通常可以根据实参推导 | 通常需要显式指定 |
| 使用方式 | Add(a, b) | Stack<int> st |
| 模板本身 | 不是具体函数 | 不是具体类 |
| 实例化结果 | 具体函数 | 具体类 |
| 常见用途 | 通用算法、操作 | 通用数据结构 |
最需要记住的是:
template<classT>TAdd(T a,T b);这是函数模板
而:
template<classT>classStack{};这是类模板
模板本身都是“模具”,真正参与程序工作的,是根据具体类型实例化出来的具体版本