1.什么是泛型编程?
泛型:是一种在编程语言中用于创建可重用代码的机制。它允许我们定义类、接口和方法,可以使用不同类型的参数进行操作,从而提高代码的灵活性和复用性。
观察以下代码:
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; }
如果我们想实现一个Swap函数,就需要考虑所有被交换变量的类型(double、int、char等)。我们可以很容易的观察到,以上三个函数只是参数的类型不一样,函数体中的代码是一样的。虽然可以通过函数重载来区分不同参数但函数名相同的函数,但对于这种函数体代码一致的函数,重载函数重复定义完全一样的函数体,无疑使代码过于冗长,且不方便维护。而模板可以完美解决以上问题。
泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。
2.函数模板
2.1 函数模板的概念
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
对于刚才的问题,我们可以定义一个通用的函数模板(funcation template),而不是为每一个类型都定义一个新函数。一个函数模板就是一个公式,可以用来生成针对特定类型的函数版本。列如
template<typename T> void Swap(T& a, T& b) { T c = a; a = b; b = c; }
2.2函数模板的格式
template<typename T1, typename T2,......,typename Tn> 返回值类型 函数名(参数列表){}
模板定义以关键字template
开始,后面跟一个模板的参数列表,这是一个以逗号分隔的一个或者多个的模板参数的列表,用<>包围起来。
模板参数表示在类中或者函数定义中用到的类型或者值。当使用模板的时候,我们隐式或者显式的指定模板实参,将其绑定到模板参数上。
template<typename T> void Swap(T& a, T& b) { T c = a; a = b; b = c; }
Swap函数声明了一个名为T的类型参数。在Swap中,我们用名字T表示一个类型。T表示的实际类型需要在编译的时候根据使用情况来确定。
注意:typename是用来定义模板参数关键字,也可以使用class,或者两者同时使用(切记:不能使用struct代替class)
这也是struct和class的区别之一。
2.3函数模板的原理
在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double型,然后产生一份专门处理double类型的代码,对于字符类型也是如此。
template<typename T> void Swap(T& a, T& b) { T c = a; a = b; b = c; } int main() { int a1 = 1, a2 = 2; Swap(a1, a2); double b1 = 1, b2 = 2; Swap(b1, b2); char c1 = 'a', c2 = 'b'; Swap(c1, c2); return 0; }
加粗样式
2.4模板函数的实例化
用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化。
1.隐式实例化:让编译器根据实参推演模板参数的实际类型
注意:在模板中,编译器一般不会进行类型转换操作,也就意味着,如果我们传参的类型与模板不相符合,编译器不会自动帮我们类型转换。
2.显式实例化:在函数名后的<>中指定模板参数的实际类型(类似vector<int>)
如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错。
2.5模板参数的匹配原则
- 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数
- 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板.
- 3.模板函数不允许自动类型转换,但普通函数可以进行自动类型转换
3.类模板
类模板是用来生成类的蓝图的。与函数模板不一样的是,编译器不能为类模板推断模板的参数类型。所以我们在使用类模板实例化类的时候需要给编译器足够的信息,用来判断参数类型,这些信息在尖括号<>中描述。用来代替模板参数的实参列表。
3.1类模板的定义格式
template<class T1, class T2, ..., class Tn> class 类模板名 { // 类内成员定义 };
注意:类模板中函数放在类外进行定义时,需要加模板参数列表
3.2类模板的实例化
类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。
比如我们常见的vector<int>其实就是一个类模板实例化的一个类
4.模板不能声明与定义分离
模板是在需要的时候才会实例化,而这个“需要的时候”已经发生在链接阶段了。根据我们所学,模板的实例化,需要编译器在编译阶段推断出实际参数类型。
假设我们将模板函数的声明放在.h文件,将定义放在.cpp文件中。由于包含main函数的源文件中声明了该.h文件,所以编译不会报错。由于.cpp文件相互独立,在编译阶段编译器并没有在模板定义处收到实例化的请求,也就不会根据定义实例化。而到了链接阶段,这个时候main函数中需要寻找实例化的模板函数,已经迟了。
举例:
test.h文件,声明一个模板函数
test.cpp文件,定义实现模板函数
main函数处调用该函数
链接错误!!!
解决办法
值得注意的是,我上面说的模板声明与定义不能分离是指不能将声明和定义放在不同的文件中。比如我们经常将声明封装成一个.h的头文件中,定义封装在源文件中。模板并不支持这种分离方式。
但是我们可以在同一个文件内部实现模板的声明和定义分离。
参考以下代码:
//.h //声明 template<class T> class A { public: T add(T a, T b); T sub(T a, T b); }; //定义 template<class T> T A<T>::add(T a, T b) { return a + b; } template<class T> T A<T>::sub(T a, T b) { return a - b; }
将声明和定义都放在头文件里,在编译的时候就能找到定义处实例化,也就不会出现上面说的链接错误了。
5.非类型模板参数
模板的参数分为类类型形参和非类型形参。
类类型就是声明在class 或者typename后面的类型名称,例如:
tempname<class T,typename H>
其中T、H就是类类型形参
而非类型形参表示的是一个常量,在类和函数模板中可以当作常数来使用,例如:
tempname<class T,size_t N>
其中N就是非类型形参。
array容器底层就是借用了非类型参数来表示容量大小,例如:
// 定义一个模板类型的静态数组 template<class T, size_t N = 10> class array { public: T& operator[](size_t index){return _array[index];} const T& operator[](size_t index)const{return _array[index];} size_t size()const{return _size;} bool empty()const{return 0 == _size;} private: T _array[N]; size_t _size; };
值得注意的是,目前的模板中的非类型形参不支持浮点数、对象以及字符串。
模板的特化
什么叫模板的特化
模板是用来构造类或者函数的的蓝图。
模板跟造枪的图纸相似。有了这个图纸,只要有材料和设备,工匠就能造出和图纸上描述一致的枪。但哪天我们想要造一种反向开枪的枪呢?通过这个图纸就造不出来了。所以,大部分的图纸是用来满足一般设计的,对于特殊的设计要求就无能为例了。
模板也同样如此,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理。在原模板的基础上,针对某些特殊类型做处理,我们称之为模板的特化。特化不等于重新构建一个模板。
观察以下代码:
#include<iostream> #include<algorithm> #include"test.h" using namespace std; // 函数模板 -- 参数匹配 template<class T> bool Less(T left, T right) { return left < right; } int main() { cout << Less(1, 2) << endl; // 可以比较,结果正确 Date d1(2022, 7, 7); Date d2(2022, 7, 8); cout << Less(d1, d2) << endl; // 可以比较,结果正确 Date* p1 = &d1; Date* p2 = &d2; cout << Less(p1, p2) << endl; // 可以比较,结果错误 return 0; }
Less是我们设计比较两个相同类型变量大小的一个函数模板。可以看到,Less绝对多数情况下都可以正常比较,但是在特殊场景下就得到错误的结果。上述示例中,p1指向的d1显然小于p2指向的d2对象,但是Less内部并没有比较p1和p2指向的对象内容,而比较的是p1和p2指针的地址,这就无法达到预期而错误。
此时,就需要对模板进行特化。即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。模板特化中分为函数模板特化与类模板特化。
函数模板特化
函数模板特化的步骤:
- 必须要先有一个基础的函数模板
- 关键字template后面接一对空的尖括号<>
- 函数名后跟一对尖括号,尖括号中指定需要特化的类型
- 函数形参表: 必须要和模板函数的基础参数类型完全相同
函数模板特化相当于单独实例化出来一个函数版本。
对于上面的Less的代码,给出函数模板特化:
//函数模板特化 template<> bool Less<Date*>(Date* left, Date* right) { return *left < *right; }
对于以上类似Date*
的特殊类型可以用模板特化的方式去解决,也可以直接重新构建一个函数。
bool Less(Date* left, Date* right) { return *left < *right; }
该种实现简单明了,代码的可读性高,容易书写,因为对于一些参数类型复杂的函数模板,特化时特别给出,因此函数模板不建议特化。
类模板特化
全特化
全特化指的是将模板完全根据特定需求或情境进行定制,使其与具体任务或要求完全匹配。这种定制化程度高,通常需要花费更多时间和资源。常用的全特化就是将模板参数列表中所有的参数都确定化。
给出一个Date类模板全特化例子:
template<class T1, class T2> class Data { public: Data() {cout<<"Data<T1, T2>" <<endl;} private: T1 _d1; T2 _d2; }; //Date类模板全特化 template<> class Data<int, char> { public: Data() {cout<<"Data<int, char>" <<endl;} private: int _d1; char _d2; }; void TestVector() { Data<int, int> d1; Data<int, char> d2; }
偏特化
偏特化是指对模板进行部分定制,只针对特定方面或部分内容进行修改,以满足特定需求,但保留了模板的基本结构和功能。
偏特化有两种表现形式:
- 部分特化
- 参数的进一步限制
部分特化将模板参数类表中的一部分参数特化
// 将第二个参数特化为int template <class T1> class Data<T1, int> { public: Data() {cout<<"Data<T1, int>" <<endl;} private: T1 _d1; int _d2; };
参数更进一步的限制:偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本。
//两个参数偏特化为指针类型 template <typename T1, typename T2> class Data <T1*, T2*> { public: Data() {cout<<"Data<T1*, T2*>" <<endl;} private: T1 _d1; T2 _d2; };