1. 泛型编程 ------ 为什么需要模板?
如何实现一个通用的交换函数呢?
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;
}
因为c++支持函数重载,所以如果我们想用不同类型的参数,是不是可以这么写啊,但是这样写是不是有点麻烦和冗余啊,因为它们的逻辑完全相同,仅仅是类型不同。
那能否告诉编译器一个模子 ,让编译器根据不同的类型利用该模子来生成代码呢?
就像这样:
cpp
┌─────────────────────────────────────────────────────────────┐
│ 模具 (模板) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ void Swap( T& left, T& right ) │ │
│ │ { │ │
│ │ T temp = left; │ │
│ │ left = right; │ │
│ │ right = temp; │ │
│ │ } │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│ │ │
▼ 倒入绿色液体 ▼ 倒入蓝色液体 ▼ 倒入红色液体
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Swap │ │ Swap │ │ Swap │
│ <int> │ │ <double>│ │ <char> │
└─────────┘ └─────────┘ └─────────┘
如果在C++中,也能够存在这样一个模具,通过给这个模具中填充不同材料(类型),来获得不同材料的铸件(即生成具体类型的代码),那将会节省许多头发。巧的是前人早已将树栽好,我们只需在此乘凉。
核心思想:泛型编程------编写与类型无关的通用代码,由编译器根据实际使用时的类型,自动生成针对该类型的代码。
2. 函数模板
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
2.1 函数模板的语法
template<typename T1, typename T2,...,typename Tn>
返回值类型 函数名(参数列表){}
cpp
template<typename T> // T 是模板参数,可以是 typename 或 class
void Swap(T& left, T& right) {
T temp = left;
left = right;
right = temp;
}
2.2 函数模板的原理
函数模板本身不是函数,它只是一个蓝图。编译器遇到函数模板的调用时,才会根据实参类型,生成一个具体的函数。这个过程叫做模板实例化。
流程图解:编译器在编译期的推演过程
cpp
源代码:
─────────────────────────────────────────────────────────────
int main()
{
double d1 = 2.0, d2 = 5.0;
Swap(d1, d2); // 调用点1
int i1 = 10, i2 = 20;
Swap(i1, i2); // 调用点2
char a = '0', b = '9';
Swap(a, b); // 调用点3
}
─────────────────────────────────────────────────────────────
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 编译器推演: │ │ 编译器推演: │ │ 编译器推演: │
│ 实参类型 double │ │ 实参类型 int │ │ 实参类型 char │
│ 推导 T = double │ │ 推导 T = int │ │ 推导 T = char │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 生成的函数: │ │ 生成的函数: │ │ 生成的函数: │
│ void Swap( │ │ void Swap( │ │ void Swap( │
│ double& left, │ │ int& left, │ │ char& left, │
│ double& right │ │ int& right) │ │ char& right) │
│ ) { ... } │ │ { ... } │ │ { ... } │
└─────────────────┘ └─────────────────┘ └─────────────────┘
编译器为每种不同的类型组合,生成一份独立的函数代码。最终的可执行文件中,包含了 Swap<double>、Swap<int>、Swap<char> 三个具体的函数,就像你手动写了三个重载一样。
2.3 函数模板的实例化
用具体类型使用函数模板,称为实例化。分为两种:
- 隐式实例化
让编译器自动根据实参类型推导模板参数。
cpp
template<class T>
T Add(const T& left, const T& right) {
return left + right;
}
int main() {
Add(1, 2); // 两个实参都是 int,推导 T = int
Add(1.0, 2.0); // 两个实参都是 double,推导 T = double
// Add(1, 2.0); // 编译错误!一个 int,一个 double,编译器推导冲突
}
错误图解:
cpp
调用 Add(1, 2.0):
│
├─ 实参1 (1) 类型为 int → 推导 T = int
├─ 实参2 (2.0) 类型为 double → 推导 T = double
│
└─ 冲突!模板参数列表中只有一个 T,编译器无法确定 T 到底是 int 还是 double。
编译器不会进行隐式类型转换,因为转换可能丢失数据,编译器不背这个锅。
解决方案:
cpp
Add(a, (int)d); // 方案1:用户手动强转
Add<int>(a, d); // 方案2:显式实例化(推荐)
- 显式实例化
在函数名后用 <类型> 强制指定模板参数。
cpp
Add<int>(10, 20.0); // 强制 T = int,20.0 会被隐式转换为 int
流程图:
cpp
调用 Add<int>(a, b):
│
▼
┌─────────────────────────────────────────┐
│ 编译器:用户已指定 T = int,不用推导了 │
│ 实参 a (int) → 匹配 │
│ 实参 b (double) → 尝试隐式转换为 int │
│ (如果能转就编译通过,否则报错) │
└─────────────────────────────────────────┘
│
▼
生成函数:int Add(const int& left, const int& right)
2.4 模板参数的匹配原则
原则一:非模板函数可以和同名模板函数共存
cpp
// 非模板函数(专门处理 int)
int Add(int left, int right)
{
return left + right;
}
// 模板函数(通用版本)
template<class T>
T Add(T left, T right)
{
return left + right;
}
原则二:优先调用非模板函数,除非模板能生成更好的匹配
cpp
Add(1, 2); // 调用非模板函数(完全匹配,且非模板优先)
Add<int>(1, 2); // 强制调用模板实例化的版本
Add(1, 2.0); // 非模板不匹配(参数类型不同),模板可以生成更好的匹配(如果模板有两个参数)
决策流程图:
cpp
遇到函数调用 Add(1, 2):
│
├─ 查找同名非模板函数 → 找到 int Add(int, int) → 完全匹配 → 调用
│
└─ 即使模板能生成完全相同的函数,也不考虑,非模板优先
遇到函数调用 Add(1, 2.0):
│
├─ 查找同名非模板函数 → int Add(int, int) 不匹配(第二个参数类型不对)
│
└─ 查找模板 → 若有 template<class T1, class T2> 版本,可生成匹配函数 → 调用模板实例化版本
原则三:模板函数不允许自动类型转换,普通函数可以
cpp
void func(int x, int y) { } // 普通函数
template<class T>
void func(T x, T y) { } // 模板函数
func(1, 'a'); // 调用普通函数:'a' 自动转换为 int(ASCII 97)
// 模板函数不会考虑,因为 char 和 int 推导冲突
3. 类模板
3.1 为什么需要类模板?
以 Stack(栈)为例,我们需要存储 int 的栈,也需要存储 double、string 的栈。如果不用模板,要么为每种类型写一个类,要么用 void* 或继承(不类型安全)。
类模板就是类的模具。
3.2 类模板的定义格式
cpp
template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};
我们来举一个我们很熟练的栈的例子:
cpp
template <class Type>
class Stack
{
public:
Stack(int capacity = 4)
:_arr(new Type[capacity])
,_size(0)
,_capacity(capacity)
{
}
~Stack()
{
delete[] _arr;
_arr = nullptr;
_size = _capacity = 0;
}
void Push(const Type& x)
{
if (_capacity == _size)
{
Type* tmp = new Type[_capacity*2];
memcpy(tmp, _arr, sizeof(Type) * _size);
delete[] _arr;
_arr = tmp;
_capacity = _capacity * 2;
}
_arr[_size++] = x;
}
void Print() const {
for (int i = 0; i < _size; ++i)
{
cout << _arr[i] << " ";
}
}
private:
Type* _arr;
int _size;
int _capacity;
};
int main()
{
Stack<int> st1;
st1.Push(1);
st1.Push(2);
st1.Push(3);
st1.Push(4);
st1.Print();
return 0;
}
关键点:
- 类模板名字是 Stack,但真正的类型是
Stack<int>、Stack<double>等。 - 成员函数在类外定义时,必须写成
template<class T> void Stack<T>::Push(...)。 - 模板的声明和定义通常不分离到 .h 和 .cpp 两个文件,否则会导致链接错误(原因涉及模板实例化的编译模型,后面会深入)。
3.3 类模板的实例化
类模板必须显式实例化(不能像函数模板那样隐式推导)。
类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。
cpp
Stack<int> st1; // 实例化出一个存储 int 的栈类
Stack<double> st2; // 实例化出一个存储 double 的栈类
cpp
程序代码段(编译后):
┌───────────────────────────────────────────────────────────┐
│ Stack<int> 类(编译器生成) │
│ - _array: int* │
│ - Push(const int&) │
├───────────────────────────────────────────────────────────┤
│ Stack<double> 类(编译器生成) │
│ - _array: double* │
│ - Push(const double&) │
└───────────────────────────────────────────────────────────┘
↑ ↑
│ │
使用 Stack<int> st1 使用 Stack<double> st2