C++模板详解:泛型编程的核心工具
github地址
C++模板详解:泛型编程的核心工具
泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。
1. 泛型编程概述
传统方式的局限性
在C++中实现通用交换函数时,我们通常会使用函数重载:
void Swap(int& left, int& right) {
int temp = left;
left = right;
right = left;
}
void Swap(double& left, double& right) {
double temp = left;
left = right;
right = left;
}
void Swap(char& left, char& right) {
char temp = left;
left = right;
right = left;
}
使用函数重载虽然可以实现,但这种方式有极大的缺陷:
代码冗余
:每个类型都需要单独实现维护困难
:修改时需要同步修改所有重载版本扩展性差
:新增类型需要添加对应函数
那能否告诉编译器一个模子,让编译器根据不同的类型利用该模子来生成代码呢?
模板的诞生
模板机制允许我们创建类型无关的通用代码,编译器根据具体类型自动生成对应版本,实现真正的代码复用。
模板的意义是将类型参数化
2. 函数模板
2.1 基本概念
蓝图
:不是具体函数
,而是生成函数的模具参数化
:通过类型参数实现通用逻辑
2.2 语法格式
// 有n个T,就代表参数列表中会有n个不同类型的参数
template<typename T1, typename T2,..., typename Tn>
返回值类型 函数名(参数列表) {
// 函数体
}
// 示例:通用交换函数
//这里<>内只有一个参数T,代表两个待交换的数类型是相同的
template<typename T>
void Swap(T& a, T& b) {
T temp = a;
a = b;
b = temp;
}
注意:
typename
可用class
替代,但不可用struct
2.3 实现原理
函数模板是一个蓝图
,它本身并不是函数
,是编译器产生特定具体类型函数的模具
。所以其实模板就是将本来应该我们做的重复的事情交给了编译器
编译器处理模板的步骤:
- 解析模板定义
- 根据调用时的具体类型
- 生成对应类型的函数代码
示例分析:
template<typename T>
T Add(const T& left, const T& right){
return left + right;
}
Add(3, 5); // 编译器生成int版本
Add(2.5, 3.7); // 编译器生成double版本
在编译器编译阶段,对于模板函数的使用,
编译器
需要根据传入的实参类型来推演
生成对应类型的函数
以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型也是如此。
从下图中我们可以看到,调用模板函数,在底层汇编中,
实际上是调用了两个具有不同类型参数的同名函数
。
2.4 实例化方式
隐式实例化(编译器自动推导参数的类型)
template<typename T>
T Add(const T& a, const T& b) {
return a + b;
}
int main() {
Add(10, 20); // T推导为int
Add(10.5, 20.5); // T推导为double
}
显式实例化(手动指定生成函数的参数类型)
template<typename T>
T Add(const T& a, const T& b) {
return a + b;
}
int main(){
//函数模板的实例化
int a1 = 10, a2 = 20;
double d1 = 10.1, d2 = 20.1;
//编译器自动推导 进行函数模板的实例化
//通过实参传递的类型,推演T的类型
Add(a1, a2);
Add(d1, d2);
//通过强制类型转换,确保推导的结果一致
//显式类型转换
Add(a1, (int)d1);
Add((double)a1, d1);
//显式指定生成的函数类型, 发生了隐式类型转换
Add<int>(a1, d1); //显式指定参数类型为 int
Add<double>(a1, d1); //显式指定参数类型为 double
}
Add<int>(a1, d1);
,会发生隐式类型转换。Add((int)a1, d1);
,使用显式类型转换。
只能显式实例化的场景
template<typename T>
T* Alloc(int n) {
return new T[n];
}
//显式实例化的应用场景
//有些函数不能自动类型推导,需要显式指定
int* p_int = Alloc<int>(10);
double* p_double = Alloc<double>(20);
- 对于以上函数模板,
编译器无法完成类型自动推导
,因此只能通过显式指定类型的方式
来完成模板的实例化。
2.5 匹配原则
一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数。
优先匹配普通函数
对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例
int Add(const int& a, const int& b) { return a + b; }
template<typename T>
T Add(const T& a, const T& b) {
return a + b;
}
Add(1, 2); // 与非模板函数匹配,编译器不需要特化
Add<int>(1, 2); // 调用编译器特化的Add版本
模板函数
和普通函数
同时存在时,优先调用普通函数。
模板匹配更优时选择模板
如果模板可以产生一个具有更好匹配的函数, 那么将选择模板实例化出的函数。
int Add(const int& a, const int& b) { return a + b; }
template<typename T>
T Add(const T& a, const T& b) {
return a + b;
}
Add(1, 2); // 与非函数模板类型完全匹配,不需要函数模板实例化
Add(3.5, 5.5); // 调用模板生成的double版本(更加匹配)加法函数
类型转换限制
- 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换
特性 | 普通函数 | 模板函数 |
---|---|---|
参数类型匹配 | 允许隐式类型转换(如 double→int ) | 严格匹配类型,除非显式指定模板参数 |
函数调用灵活性 | 高(自动适应类型) | 低(需手动处理类型差异) |
典型错误场景 | 可能意外丢失精度 | 模板参数推导失败导致编译错误 |
3. 类模板
3.1 定义格式
template<class T1, class T2,..., class Tn>
class ClassName {
// 类成员定义
};
// 示例:栈模板
template<typename T>
class Stack {
public:
Stack(size_t capacity = 4)
: _array(new T[capacity])
, _capacity(capacity)
, _size(0)
{}
void Push(const T& data){
_array[_size++] = data;
}
private:
T* _array;
size_t _capacity;
size_t _size;
};
3.2 实例化特点
- 必须显式实例化
- 类模板名不是类型,实例化结果才是真正类型
int main(){
// Stack是类名,Stack<int>才是类型
Stack<int> intStack; // 整型栈
Stack<double> dblStack; // 双精度栈
return 0;
}
4. 模板的优势
- 提高代码复用率
- 增强类型安全性
- 提升开发效率
- 支持泛型算法设计
5. 使用建议
优先使用模板替代函数重载
复杂类型建议使用类模板
注意模板实例化的编译开销
合理使用显式实例化控制代码生成
以上就是本文的所有内容了,如果觉得文章写的不错,还请留下免费的赞和收藏,也欢迎各位大佬在评论区交流
分享到此结束啦
一键三连,好运连连!