Bootstrap

C++模板详解(1):泛型编程的核心工具

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;
}

使用函数重载虽然可以实现,但这种方式有极大的缺陷:

  1. 代码冗余:每个类型都需要单独实现
  2. 维护困难:修改时需要同步修改所有重载版本
  3. 扩展性差:新增类型需要添加对应函数

那能否告诉编译器一个模子,让编译器根据不同的类型利用该模子来生成代码呢?

模板的诞生

模板机制允许我们创建类型无关的通用代码,编译器根据具体类型自动生成对应版本,实现真正的代码复用。

模板的意义是将类型参数化


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 实现原理

函数模板是一个蓝图,它本身并不是函数,是编译器产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器

编译器处理模板的步骤:

  1. 解析模板定义
  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. 模板的优势

  1. 提高代码复用率
  2. 增强类型安全性
  3. 提升开发效率
  4. 支持泛型算法设计

5. 使用建议

  1. 优先使用模板替代函数重载
  2. 复杂类型建议使用类模板
  3. 注意模板实例化的编译开销
  4. 合理使用显式实例化控制代码生成

以上就是本文的所有内容了,如果觉得文章写的不错,还请留下免费的赞和收藏,也欢迎各位大佬在评论区交流

分享到此结束啦
一键三连,好运连连!

;