📑 目录
- [1. 前言](#1. 前言)
- [2. 什么是泛型编程?](#2. 什么是泛型编程?)
- [3. 函数模板](#3. 函数模板)
- [3.1 函数模板的基本格式](#3.1 函数模板的基本格式)
- [3.2 函数模板的原理](#3.2 函数模板的原理)
- [3.3 什么是函数模板的实例化?](#3.3 什么是函数模板的实例化?)
- [3.3.1 隐式实例化](#3.3.1 隐式实例化)
- [3.3.2 显示实例化(无法自动推导的情况)](#3.3.2 显示实例化(无法自动推导的情况))
- [3.4 模板参数的匹配原则](#3.4 模板参数的匹配原则)
- [4. 类模板](#4. 类模板)
- [4.1 基本格式](#4.1 基本格式)
- [4.2 类模板的实例化](#4.2 类模板的实例化)
- [4.3 函数模板和类模板有什么区别?](#4.3 函数模板和类模板有什么区别?)
- [4.4 类模板的成员函数放到类外定义](#4.4 类模板的成员函数放到类外定义)
- 写在最后
1. 前言
前面我们已经学习了:
C++类和对象,构造函数,析构函数,拷贝构造,运算符重载,内存管理,学到这里,其实会发现一个问题。我们之前写代码的时候,经常会遇到一种情况:
代码逻辑完全一样,只是数据类型不一样。
比如我们想写一个交换函数。
对于 int:
cpp
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
现在又需要交换 double:
cpp
void Swap(double& left, double& right)
{
double temp = left;
left = right;
right = temp;
}
要是还需要交换 char:
cpp
void Swap(char& left, char& right)
{
char temp = left;
left = right;
right = temp;
}
可以直观地感受到:这几个函数的代码几乎一模一样。
只是数据类型不同,就很难受,难道其他类型的我还要多次 repeat 吗?那有没有一种办法:
能否告诉编译器一个模子,让编译器根据不同的类型利用该模子来生成代码呢?
这就是今天要学习的:模板。 前辈真是太伟大了!
2. 什么是泛型编程?
编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。

3. 函数模板
函数模板的概念可以简单理解成:
函数模板代表一个函数家族,它与具体类型无关,在使用的时候根据实参类型产生对应的具体版本。
PS:
函数模板本身并不是一个真正意义上直接执行的具体函数。 它更像是一份:蓝图
3.1 函数模板的基本格式
函数模板的基本格式:
cpp
template<typename T>
返回值类型 函数名(参数列表)
{
// 函数体
}
例如:
cpp
template<typename T>
void Swap(T& x1, T& x2)
{
T tmp = x1;
x1 = x2;
x2 = tmp;
}
这里最重要的就是:
cpp
typename T
T是什么
T是一个类型参数。
cpp
int a = 0, b = 1;
double c = 1.1, d = 2.2;
```编译器是可以识别 a,b 是什么的,也会识别 c,d 是什么,然后模板里边的 T 会自动进行推导。然后使用 `Swap` 进行交换。。
而
```cpp
typename
是用来定义模板参数的关键字。
也可以写成:
cpp
template<class T>
例如:
cpp
template<class T>
void Swap(T& left, T& right)
{
T temp = left;
left = right;
right = temp;
}
3.2 函数模板的原理
这里提出一个问题::
函数模板是不是一个函数?
严格来说:
函数模板是一个蓝图,它本身并不是函数,是编译器产生特定具体类型函数的模具。
比如:
cpp
template<typename T>
void Swap(T& left, T& right)
{
T temp = left;
left = right;
right = temp;
}
在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如当用 double 类型使用函数模板时时,编译器通过对实参类型的推演将 T 确定为 double 类型型然后产生一份专门处理 double 类型的代码码,对于字符类型也是如此。
---而且我们在调试的时候调用反汇编也能知道它调用的不是同一个函数。



3.3 什么是函数模板的实例化?在类和对象一开始就已经提到过实例化。在函数模板中,当我们使用不同类型的参数去调用函数模板时,编译器会根据具体类型产生对应的具体版本。。这个过程就叫:
函数模板实例化。
PDF 中明确把函数模板实例化分成:
- 隐式实例化
- 显式实例化
3.3.1 隐式实例化
什么叫隐式?就是:
我们不告诉编译器具体类型,让编译器自己推导。
例如:
cpp
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
int a1 = 10;
int a2 = 20;
Add(a1, a2);
}
编译器看到:
text
a1 → int
a2 → int
所以推导:
text
T → int
这就是:
隐式实例化。
3.3.2 显式实例化(无法自动推导的情况))
cpp
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
int a = 10;
double b = 20.0;
Add(a, b);
}
这时候能不能正常推导?这里就会出现问题

text
第一个参数:
T → int
第二个参数:
T → double
但是我们这个模板只有一个:
cpp
T
```编译器不知道 T 到底应该是 int 还是 double?编译器没有办法同时满足。。因此不能简单地通过这种调用完成模板参数推导。就是两难的地步,我该听谁的?
1. 解决方法一:自己强制转换
如果我们确定希望两个参数都按照 `int` 来处理:
```cpp
int a = 10;
double b = 20.0;Add(a, (int)b);
// 这两者都可以
Add((double)a, b))
text
T->double
T → int
推导就没有问题了。

- 显式实例化
显式指定模板参数。
比如:
cpp
Add<int>(a, b);
这里:
cpp
<int>
就是明确告诉编译器:这一次你把 T 当成 int。
所以:
cpp
Add<int>(a, b);
里面:
text
T = int
已经明确了。
这就是:
显式实例化。
---现在可以放在一起比较:隐式实例化化
cpp
Add(a, b);
让编译器:
text
根据实参
↓
自己推导T
显式实例化
cpp
Add<int>(a, b);
我们直接:
text告诉编译器
所以:
text
隐式:
编译器自己推
显式:
我们直接指定
并不是所有情况下,编译器都能够自动推导出模板参数。
cpp
template<class T>
T* Alloc(int n)
{
return new T[n];
}
调用:
cpp
double* p1 = Alloc<double>(10);
这里就直接指定了:
cpp
double
为什么?因为:
cpp
Alloc(10);
参数里面只有:
cpp
10
它的类型是:
cpp
int
但是我们真正想推导的是:
cpp
T
而 T 并没有出现在能够帮助我们推导它的参数位置中。
所以这种情况下就需要:
cpp
Alloc<double>(10);
3.4 模板参数的匹配原则
这里主要需要理解一个问题:
如果普通函数和函数模板可以同时存在吗,如果可以,会调用谁?
比如:
cpp
int Add(int left, int right)
{
return left + right;
}
同时又有:
cpp
template<class T>
T Add(T left, T right)
{
return left + right;
}
这时候两个函数都叫:
cpp
Add
那么:
cpp
Add(1, 2);
到底可以共存吗?答案是:可以。
普通函数:
cpp
int Add(int left, int right)
{
return left + right;
}
函数模板:
cpp
template<class T>
T Add(T left, T right)
{
return left + right;
}
```Add(1, 2) 调用谁?因为::
```text
1 → int
2 → int
普通函数:
cpp
int Add(int, int)
已经能够非常直接地匹配。所以在这种情况下会优先匹配非模板函数。但如果一定要用模板的,那就
cpp
Add<int>(1, 2);
这时候我们明确指定:
text
T = int
那么调用的就是模板实例化出来的版本。
所以:
cpp
Add(1, 2);
和:
cpp
Add<int>(1, 2);
```但是一般来说不会那么无聊去指定,直接设置两个不同的 T1、T2 就好了,为什么非要只设置一个 T,然后去显式调用呢?你说是不不
```cpp
template<class T1, class T2>
T1 Add(T1 left, T2 right)
{
return left + right;
}
void Test()
{
Add(1, 2); // 与非函数模板类型完全匹配,不需要函数模板实例化
Add(1, 2.0); // 模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的 Addd
}
我们现在把函数模板的思维整理一下:
text
泛型编程
↓
函数模板
↓
template<typename T>
↓
T代表类型参数
↓
根据实参推导T
↓
函数模板实例化
↓
隐式实例化 / 显式实例化
↓
模板参数匹配
到这里,函数模板基本就学完了。
函数可以使用模板,那类可不可以也使用模板?
答案当然是:可以。
这就是:
类模板。
4. 类模板
先回忆一下我们以前写过的栈。假设我们写一个:
cpp
StackInt
```专门存:int,那么里面的数据可能是:int* _array; 如果现在又需要 StackDouble
存 double,那么可能又得重新写:double* _array; 它们的代码结构基本一样,只是 `DataType` 不一样。。这和前面的:
```text
Swap(int)
Swap(double)
Swap(char)
是不是非常像类模板就是解决这个问题的 。
既然::
text
int版本
double版本
char版本
代码结构基本一样。那么我们能不能也写一个模板?当然可以。## 4.1 基本格式式
cpp
template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员
};
代码中的Stack模板
cpp
StackInt
StackDouble
进行了模板化。代码:
cpp
template<class T>
class Stack
{
public:
Stack(size_t capacity = 3);
void Push(const T& data);
~Stack()
{
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private:
T* _array;
int _capacity;
int _size;
};
template<class T>
Stack<T>::Stack(size_t capacity)
{
_array = new T[capacity];
_capacity = capacity;
_size = 0;
}
template<class T>
void Stack<T>::Push(const T& data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
这里最关键的是:
cpp
template<class T>
以及:
cpp
T* _array;
4.2 类模板的实例化
这里要特别注意:
类模板和函数模板的实例化方式不完全一样,类模板实例化需要在类模板名字后跟 <>,然后将实例化的类型放在 <> 中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。*
函数模板:
cpp
Add(a, b);
可以根据参数自动推导。但是类模板使用的时候,一般需要明确指定类型。比如:
cpp
Stack<int> s1;
Stack<double> s2;
需要注意 Stack 到底是什么?普通类::
cpp
class A
{
};
通常:
text
A
就是类名,同时也是类型名。但是对于类模板:
cpp
template<class T>
class Stack
{
};
这里:
text
Stack
是:类名。
真正实例化出来的具体类型是:
cpp
Stack<int>
或者:
cpp
Stack<double>
4.3 函数模板和类模板有什么区别?
现在可以简单比较一下。
| 对比 | 函数模板 | 类模板 |
|---|---|---|
| 作用 | 生成不同类型的函数 | 生成不同类型的类 |
| 基本形式 | template<class T> |
template<class T> |
| 类型来源 | 通常根据函数实参推导 | 使用时指定 |
| 实例化 | 可以隐式/显式 | 通常需要显式指定 |
| 示例 | Add<int>() |
Stack<int> |
4.4 类模板的成员函数放到类外定义
如果我们把类模板的成员函数直接写在类里面:这都能明白。白
cpp
template<class T>
class Stack
{
public:
void Push(const T& data)
{
// ...
}
};
把成员函数放到类外:
cpp
template<class T>
void Stack<T>::Push(const T& data)
{
// ...
}
这里必须要知道,前面还需要::
cpp
template<class T>
再加上
cpp
Stack<T>::Push(const T& data)
就是这种写法。为什么类外定义前面还要写 template?因为我们是在告诉编译器:下面这个函数属于一个类模板。不要写成::
cpp
void Stack::Push(...)
因为:
cpp
Stack
只是类名。真正的类型应该是:
cpp
Stack<T>
或者实例化之后:
cpp
Stack<int>
写在最后
模板这一块刚开始学习的时候,我觉得最容易产生的一种感觉就是:
"这到底是什么东西?"
尤其是第一次看到:
cpp
template<class T>
可能会觉得语法比较奇怪。
但是如果把它和前面的 Swap 联系起来,其实就很好理解。
我们之前:
cpp
int Swap
double Swap
char Swap
写了很多重复代码。模板出现以后:
cpp
template<class T>
Swap
我们只需要写一份。剩下的事情交给编译器。这就是模板最核心的思想:
把重复的工作交给编译器。
而类模板也是一样。所以这一篇真正需要理解的不是:
cpp
template<class T>
这几个字符怎么背。而是:
text
代码逻辑相同
↓
只是类型不同
↓
不想重复写代码
↓
使用泛型编程
↓
使用模板
↓
让编译器生成具体代码
这才是模板真正解决的问题。而且从这里开始,我们也会慢慢接触到 C++ 中非常重要的一部分:STL。 以后看到:
cpp
vector<int>
vector<double>
或者其他类似的模板代码,就不会再觉得特别陌生了。
感谢大家的观看阅读,如果这篇文章能够给你们带来哪怕一点点收获和解决困惑,那对我而言都是一种动力和成就感。如果文章对你有帮助,欢迎点赞 👍、收藏 ⭐、评论 💬 支持一下