C++模板:泛型编程与函数模板详解(上)
C++模板:泛型编程与函数模板详解(上)
1. 泛型编程:解决通用交换函数的困境
1.1 传统方法的局限性
在C++编程中,我们经常需要实现通用的功能,比如交换两个变量的值。传统的方法是使用函数重载:
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 泛型编程的解决方案
泛型编程的核心思想:编写与类型无关的通用代码,实现代码复用。
想象一下,如果有一个"模具",我们只需要告诉编译器用这个模具,它就能自动为我们生成各种类型的具体代码,那该多方便!
// 伪代码:理想中的通用交换函数
void Swap(任何类型& left, 任何类型& right)
{任何类型 temp = left;left = right;right = temp;
}
幸运的是,C++通过模板机制实现了这个梦想!
在下面我们将介绍函数模板和类模板
C++函数模板详解
2.1 函数模板概念
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。它就像是一个函数蓝图。
简单来说,函数模板就像一个"函数生成器",我们可以通过它来创建处理不同类型数据的相似函数,而无需为每种类型都手动编写一遍。
2.2 函数模板格式
函数模板的基本语法格式如下:
template<typename T1, typename T2, ..., typename Tn>
返回值类型 函数名(参数列表)
{// 函数体
}
示例:通用交换函数模板
template<typename T>
void Swap(T& left, T& right)
{T temp = left;left = right;right = temp;
}
重要说明:
typename是用来定义模板参数的关键字- 也可以使用
class关键字(效果相同) - 注意: 不能使用
struct代替class
// 这两种写法是等价的:
template<typename T>
template<class T> // 同样有效
这里的T就是我们的模板参数,他将会根据需要变成我们需要的类型,例如(int,char…)而无需我们再单独为他们实现相应的函数代码。
2.3 函数模板的原理
模板的工作机制
函数模板本身并不是函数,而是编译器用来生成具体函数的模具。这就像是工业革命中的机器生产:
- 手工生产 → 函数重载:为每种类型手动编写函数
- 机器生产 → 函数模板:编译器自动为各种类型生成函数
编译器的工作流程:
- 模板推演:编译器根据传入的实参类型推导模板参数
- 代码生成:编译器用推导出的类型替换模板参数,生成具体的函数代码
- 编译执行:将生成的具体函数编译为机器代码
具体示例分析:
template<typename T>
T Add(const T& left, const T& right)
{return left + right;
}int main()
{Add(1, 2); // 编译器生成 Add<int> 版本Add(1.5, 2.5); // 编译器生成 Add<double> 版本return 0;
}
编译器会为我们生成:
int Add_int(const int& left, const int& right)
{return left + right;
}double Add_double(const double& left, const double& right)
{return left + right;
}
可以看到,模板大大提升了我们编写代码的效率,同时也为后面的STL类库提供了语法基础。
2.4 函数模板的实例化
用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为两种方式:
2.4.1 隐式实例化
让编译器根据实参自动推演模板参数的实际类型:
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 为 intAdd(d1, d2); // 编译器推演 T 为 double/** 上面两句代码没有问题,而下面那个语句编译错误!* 编译器通过 a1 将 T 推演为 int* 通过 d1 将 T 推演为 double* 但模板参数列表中只有一个 T* 编译器无法确定 T 应该是 int 还是 double*/// Add(a1, d1); // 错误!return 0;
}
模板类型转换规则:
在模板中,编译器一般不会进行自动类型转换,因为一旦转换出问题,责任难以界定。
2.4.2 显式实例化
在函数名后的 <> 中指定模板参数的实际类型:
int main()
{int a = 10;double b = 20.0;// 显式实例化Add<int>(a, b); // 指定 T 为 int,b 会被隐式转换为 intAdd<double>(a, b); // 指定 T 为 double,a 会被隐式转换为 doublereturn 0;
}
显式实例化的特点:
- 明确告诉编译器使用什么类型
- 如果类型不匹配,编译器会尝试进行隐式类型转换(前面提到,如果是利用模板隐式类型转换,它不会自动类型转换,而显示实例化可以)
- 如果无法转换成功,编译器将会报错(例如string类型可能无法很好的转化成size_t)
处理混合类型参数的解决方案
int main()
{int a = 10;double d = 20.5;// 方法1:用户自己强制转换Add(a, (int)d); // 将 double 转换为 intAdd((double)a, d); // 将 int 转换为 double// 方法2:使用显式实例化Add<int>(a, d); // 显式指定为 intAdd<double>(a, d); // 显式指定为 doublereturn 0;
}
2.5 模板参数的匹配原则
原则1:模板函数与普通函数共存
一个非模板函数可以和一个同名的函数模板同时存在:
// 专门处理 int 的加法函数(普通函数)
int Add(int left, int right)
{std::cout << "调用普通函数 Add" << std::endl;return left + right;
}// 通用加法函数(函数模板)
template<class T>
T Add(T left, T right)
{std::cout << "调用模板函数 Add" << std::endl;return left + right;
}void Test()
{Add(1, 2); // 与非模板函数完全匹配,调用普通函数Add<int>(1, 2); // 显式调用模板函数,调用编译器特化的 Add 版本
}
原则2:优先匹配规则
对于非模板函数和同名函数模板:
- 如果其他条件都相同,优先调用非模板函数
- 如果模板能产生更好匹配的函数,则选择模板
// 专门处理 int 的加法函数
int Add(int left, int right)
{std::cout << "调用普通函数 Add" << std::endl;return left + right;
}// 通用加法函数模板(支持不同类型参数)
template<class T1, class T2>
auto Add(T1 left, T2 right) -> decltype(left + right)
{std::cout << "调用模板函数 Add" << std::endl;return left + right;
}//这里的auto和decltype都是用于推导返回值类型的,是C++11里面的知识,暂时可以不必在意void Test()
{Add(1, 2); // 完全匹配普通函数,调用普通函数Add(1, 2.0); // 模板函数能生成更好匹配的版本,调用模板函数
}
原则3:类型转换规则
- 模板函数:不允许自动类型转换
- 普通函数:可以进行自动类型转换
// 普通函数
void Print(int value)
{std::cout << "普通函数: " << value << std::endl;
}// 模板函数
template<typename T>
void Print(T value)
{std::cout << "模板函数: " << value << std::endl;
}void Test()
{Print(10); // 调用普通函数Print(10.5); // 调用模板函数(T 推导为 double)Print('A'); // char 可以转换为 int,但模板匹配更好,调用模板函数
}
C++类模板详解
3.1 类模板的定义格式
类模板允许我们创建通用的类,这些类可以处理不同类型的数据(这也是STL类库的语法基础)。类模板的基本定义格式如下:
template<class T1, class T2, ..., class Tn>
class 类模板名
{// 类内成员定义
};
需要注意,这样定义还不算真的类,它和函数模板一样,暂时还是个蓝图,只有当它实例化时,才是真正的类
动态顺序表示例
让我们通过一个动态顺序表(Vector)来理解类模板:
// 动态顺序表模板
// 注意:Vector不是具体的类,而是编译器生成具体类的模具
template<class T>
class Vector
{
public:// 构造函数Vector(size_t capacity = 10): _pData(new T[capacity]), _size(0), _capacity(capacity){}// 析构函数 - 在类中声明,在类外定义~Vector();// 成员函数void PushBack(const T& data);void PopBack();// 其他操作...size_t Size() { return _size; }// 重载下标运算符T& operator[](size_t pos){assert(pos < _size);return _pData[pos];}// const版本的下标运算符const T& operator[](size_t pos) const{assert(pos < _size);return _pData[pos];}private:T* _pData; // 指向动态数组的指针size_t _size; // 当前元素个数size_t _capacity; // 容量
};
类模板成员函数的类外定义
重要规则: 类模板的成员函数在类外定义时,需要添加模板参数列表。
// 析构函数的类外定义
template <class T>
Vector<T>::~Vector()
{if(_pData)delete[] _pData;_size = _capacity = 0;
}
3.2 类模板的实例化
类模板实例化的特殊性
类模板的实例化与函数模板有一个重要区别:
- 函数模板:可以通过参数推导隐式实例化
- 类模板:必须显式指定模板参数,也就是说必须在类模版名字后面跟<>,然后<>里面写上实例化的类型
实例化语法
类模板名<实际类型> 对象名(构造函数参数);
实际使用示例
// Vector是类名,Vector<int>才是类型Vector<int> s1;Vector<double> s2;//还有例如:stack<int> s1;string s2;
3.3 多参数类模板
类模板可以接受多个类型参数:
如下示例,有K,V两个参数:其中一个是键,一个是value(值)
这是一个红黑树的类模版:
template<class K, class V>
struct RBTreeNode
{RBTreeNode<K, V>* _left;RBTreeNode<K, V>* _right;RBTreeNode<K, V>* _parent;pair<K, V> _kv;Colour _col;RBTreeNode(const pair<K, V>& kv):_left(nullptr), _right(nullptr), _parent(nullptr), _kv(kv), _col(RED){}
};
3.4 类模板的分离编译问题
问题描述
在C++中,类模板的声明和定义通常需要放在同一个文件中,这是因为:
根本原因:C++的编译机制
-
模板不是真正的代码
- 类模板只是"蓝图",不是具体的类
- 编译器看到模板时,不会立即生成代码
-
编译时的实例化
- 只有当使用具体类型实例化模板时,编译器才生成实际代码
- 例如:
Vector<int> v;让编译器生成Vector<int>的具体实现
-
分离编译的问题
- 如果声明在
.h文件,实现在.cpp文件 - 编译
main.cpp时,编译器只知道模板声明,看不到具体实现 - 链接时找不到实际生成的代码,导致链接错误
- 如果声明在
-
解决方案
// Vector.h
template<class T>
class Vector
{
public:void PushBack(const T& data); // 声明
};// Vector.cpp
template<class T>
void Vector<T>::PushBack(const T& data) // 定义
{// 实现...
}// main.cpp,注意这已经是另一个文件了
#include "Vector.h"
int main()
{Vector<int> v;v.PushBack(10); // 链接错误!找不到PushBack<int>的实现
}
解决方案
- 将实现放在头文件中(推荐)
// Vector.h
template<class T>
class Vector
{
public:void PushBack(const T& data);
};// 在头文件中直接实现
template<class T>
void Vector<T>::PushBack(const T& data)
{// 实现代码...
}
