C++ 模板初阶完全指南
一篇帮你彻底搞懂 C++ 模板基础的复习笔记
一、泛型编程
1.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;
}
// ...... 每种类型都要写一遍
1.2 函数重载的缺点
| 缺点 | 说明 |
|---|---|
| 代码复用率低 | 仅仅是类型不同,却要重复编写 |
| 可维护性差 | 一处出错,所有重载都要修改 |
| 扩展困难 | 新类型出现需要手动添加函数 |
1.3 泛型编程概念
泛型编程 :编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。
┌─────────────────────────────────────┐
│ 泛型编程思想 │
├─────────────────────────────────────┤
│ │
│ 模板 (模具) │
│ │ │
│ ├──→ 填充 int → int 版本 │
│ ├──→ 填充 double → double 版本 │
│ └──→ 填充 char → char 版本 │
│ │
│ 编译器自动生成,无需手写重复代码 │
│ │
└─────────────────────────────────────┘
二、函数模板
2.1 概念
函数模板代表了一个函数家族,与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
2.2 定义格式
cpp
template<typename T1, typename T2, ..., typename Tn>
返回值类型 函数名(参数列表) {
// 函数体
}
2.3 示例
cpp
template<typename T>
void Swap(T& left, T& right) {
T temp = left;
left = right;
right = temp;
}
int main() {
int a = 1, b = 2;
double c = 1.0, d = 2.0;
char e = 'a', f = 'b';
Swap(a, b); // 编译器生成 int 版本
Swap(c, d); // 编译器生成 double 版本
Swap(e, f); // 编译器生成 char 版本
return 0;
}
⚠️ 注意 :
typename用来定义模板参数关键字,也可以使用class,但不能使用struct!
cpp
// 以下两种写法等价
template<typename T>
template<class T>
// 错误写法
template<struct T> // ❌ 不允许
2.4 函数模板的原理
┌───────────────────────────────────────────────────┐
│ 编译器工作流程 │
├───────────────────────────────────────────────────┤
│ │
│ 源代码阶段: │
│ template<typename T> │
│ void Swap(T& left, T& right) { ... } │
│ ↓ │
│ 这只是蓝图/模具,不是真正的函数! │
│ │
│ 编译阶段: │
│ Swap(int, int) → 推演 T = int │
│ Swap(double, double) → 推演 T = double │
│ ↓ │
│ 编译器自动生成对应类型的具体函数 │
│ │
└───────────────────────────────────────────────────┘
核心思想:将本来应该我们做的重复的事情交给了编译器!
三、函数模板的实例化
用不同类型的参数使用函数模板时,称为函数模板的实例化。
3.1 隐式实例化
让编译器根据实参推演模板参数的实际类型。
cpp
template<class T>
T Add(const T& left, const T& right) {
return left + right;
}
int main() {
int a1 = 10, a2 = 20;
double d1 = 10.0, d2 = 20.0;
Add(a1, a2); // T 推演为 int
Add(d1, d2); // T 推演为 double
// ❌ 编译错误!
// Add(a1, d1);
// a1 推演 T = int,d1 推演 T = double
// 只有一个 T,编译器无法确定类型
return 0;
}
3.2 解决类型冲突的两种方式
cpp
int main() {
int a = 10;
double b = 20.0;
// 方式一:用户强制转换
Add(a, (int)b); // b 强转为 int
// 方式二:显式实例化
Add<int>(a, b); // 指定 T 为 int
return 0;
}
3.3 显式实例化
在函数名后的 <> 中指定模板参数的实际类型。
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<int>(a, b); // 指定 T = int,b 会隐式转换为 int
return 0;
}
⚠️ 如果类型不匹配,编译器会尝试进行隐式类型转换,无法转换则报错。
四、模板参数的匹配原则
4.1 原则一:非模板函数与模板可共存
cpp
// 专门处理 int 的加法函数
int Add(int left, int right) {
return left + right;
}
// 通用加法函数模板
template<class T>
T Add(T left, T right) {
return left + right;
}
void Test() {
Add(1, 2); // 调用非模板函数,完全匹配
Add<int>(1, 2); // 调用模板实例化版本
}
4.2 原则二:优先匹配非模板函数
cpp
int Add(int left, int right) {
return left + right;
}
template<class T>
T Add(T left, T right) {
return left + right;
}
void Test() {
Add(1, 2); // 优先调用非模板函数(不需要实例化)
}
4.3 原则三:模板可产生更好匹配
cpp
int Add(int left, int right) {
return left + right;
}
template<class T1, class T2>
T1 Add(T1 left, T2 right) {
return left + right;
}
void Test() {
Add(1, 2); // 调用非模板函数(int, int 完全匹配)
Add(1, 2.0); // 调用模板(生成 Add(int, double) 更匹配)
}
4.4 原则四:模板不自动类型转换
cpp
template<class T>
T Add(T left, T right) {
return left + right;
}
int main() {
int a = 10;
double b = 20.0;
// Add(a, b); // ❌ 编译错误!模板不会自动转换类型
Add(a, (int)b); // ✅ 手动转换
Add<int>(a, b); // ✅ 显式实例化
return 0;
}
⚠️ 注意:编译器一般不会对模板参数进行类型转换,因为一旦转换出问题,编译器需要"背黑锅"!
4.5 匹配原则速查表
| 场景 | 调用结果 |
|---|---|
| 非模板函数完全匹配 | ✅ 调用非模板函数 |
| 模板函数更匹配 | ✅ 调用模板实例化 |
| 都不匹配但非模板可转换 | ✅ 调用非模板函数 |
| 都不匹配且模板不可转换 | ❌ 编译错误 |
五、类模板
5.1 定义格式
cpp
template<class T1, class T2, ..., class Tn>
class 类模板名 {
// 类内成员定义
};
5.2 示例:栈类模板
cpp
#include<iostream>
using namespace std;
template<typename T>
class Stack {
public:
Stack(size_t capacity = 4) {
_array = new T[capacity];
_capacity = capacity;
_size = 0;
}
void Push(const T& data);
private:
T* _array;
size_t _capacity;
size_t _size;
};
// 类外定义成员函数
template<class T>
void Stack<T>::Push(const T& data) {
// 扩容逻辑
_array[_size] = data;
++_size;
}
5.3 类模板的实例化
cpp
int main() {
Stack<int> st1; // int 栈
Stack<double> st2; // double 栈
return 0;
}
⚠️ 重要 :
Stack是类名,Stack<int>才是类型!
5.4 类模板 vs 函数模板实例化
| 对比项 | 函数模板 | 类模板 |
|---|---|---|
| 实例化方式 | 可隐式推演 | 必须显式指定 |
| 语法 | func(args) 或 func<T>(args) |
ClassName<T> |
| 类型确定 | 编译器推演 | 用户指定 |
六、模板注意事项
6.1 声明和定义分离
cpp
// ❌ 不建议:声明和定义分离到 .h 和 .cpp
// 会出现链接错误!
// ✅ 建议:声明和定义放在同一个头文件中
原因后续章节会讲解(模板实例化需要完整定义可见)
6.2 模板参数命名
cpp
// 单个参数
template<typename T>
// 多个参数
template<typename T1, typename T2>
// 混合使用
template<typename T, int N> // N 是非类型模板参数
七、一图总结
┌───────────────────────────────────────────────────────┐
│ C++ 模板初阶总结 │
├───────────────────────────────────────────────────────┤
│ │
│ 【泛型编程】 │
│ 目的:编写与类型无关的通用代码 │
│ 核心:模板是泛型编程的基础 │
│ │
│ 【函数模板】 │
│ 格式:template<typename T> │
│ 实例化:隐式 / 显式 │
│ 原理:编译器根据类型生成具体函数 │
│ │
│ 【实例化规则】 │
│ 隐式:编译器推演类型 │
│ 显式:用户指定 <类型> │
│ │
│ 【匹配原则】 │
│ 1. 非模板函数与模板可共存 │
│ 2. 优先匹配非模板函数 │
│ 3. 模板可产生更好匹配时选模板 │
│ 4. 模板不自动类型转换 │
│ │
│ 【类模板】 │
│ 实例化必须显式指定类型 │
│ Stack<int> 才是类型,Stack 只是类名 │
│ │
└───────────────────────────────────────────────────────┘
八、记忆口诀
模板模具造函数,类型填充自动生;
隐式推演显式定,优先匹配非模板;
类模板要显式写,Stack<int> 才是型;
声明定义放一起,分离编译会出错!
希望这篇笔记能帮助你快速复习 C++ 模板初阶知识!如有疑问,欢迎讨论交流。