文章目录
- 一、泛型编程
- 二、函数模板
- 1. 概念
- 2. 语法
- 3. 函数模板的原理
- 4. 函数模板的实例化
- 5. 模板参数的匹配原则
- 三、类模板
- 1. 语法
- 2. 实例化
- 四、模板的特化
- 1. 概念
- 2. 函数模板特化
- 3. 类模板特化
- 3.1 全特化
- 3.2 偏特化 / 半特化
- 3.3 应用示例
- 4. 小结
- 五、模板的分离编译
- 1. 分离编译的概念
- 2. 模板不能分离编译
- 六、模板总结
- 总结
一、泛型编程
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. 语法
- template<typename T1, typename T2,…,typename Tn>
- 注意:typename是用来定义模板参数关键字,也可以使用class(切记:不能使用struct代替class)
template<typename T>
void Swap( T& left, T& right)
{T temp = left;left = right;right = temp;
}
3. 函数模板的原理
函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器。在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用
4. 函数模板的实例化
用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化。
隐式实例化:让编译器根据实参推演模板参数的实际类型
template<class T> T Add(const T& left, const T& right) {return left + right; }int main() {int a1 = 10, a2 = 20;Add(a1, a2);return 0; }
模板参数个数 与 传参类型个数 不一致
int main() {int a3 = 30;double d1 = 10.0;Add(a3, d1);return 0; } /*该语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型通过实参a1将T推演为int,通过实参d1将T推演为double类型,但模板参数列表中只有一个T,编译器无法确定此处到底该将T确定为int 或者 double类型而报错注意:在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,编译器就需要背黑锅 */
解决:
- 用户自己来强制转化
Add(a3, (int)d1);
如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错- 使用显式实例化
显式实例化:在函数名后的<>中指定模板参数的实际类型
int main() {int a = 10;int b = 20;// 显式实例化Add<int>(a, b); return 0; }int main() {int a3 = 30;double d1 = 10.0;// 显式实例化Add<int>(a3, d1); //隐式类型转换return 0; }
5. 模板参数的匹配原则
一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数
思考一下,会不会发生重定义问题?答案是不会,详细解释请跳转到 “本文五、2.2 解决方法处”
// 专门处理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); // 显式实例化,调用编译器特化的Add版本(根据现成的,用模板生成一份相同的) }
对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板。(人话:有现成用现成的,没有就再造一个)
如何界定到底是匹配模板还是匹配现成的?
这里涉及到参数匹配的优先级: 完全匹配 > 模板替换后匹配 > 隐式类型转换后匹配
// 专门处理int的加法函数 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); // 调用现成的Add(1, 2.0); // 模板函数可以生成更加匹配的版本,匹配优先级:// 函数模板 > 隐式类型转换(现成的) }
三、类模板
1. 语法
template<class T1, class T2, ..., class Tn>
class 类模板名
{// 类内成员定义
};
#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;
};// 模版不建议声明和定义分离到两个文件.h 和.cpp会出现链接错误,具体原因后面会讲
template<class T>
void Stack<T>::Push(const T& data)
{// 扩容_array[_size] = data;++_size;
}int main()
{Stack<int> st1; // intStack<double> st2; // doublereturn 0;
}
2. 实例化
类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可
// Stack是类名,Stack<int>才是类型
Stack<int> st1; // int
Stack<double> st2; // double
四、模板的特化
1. 概念
在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。模板特化中分为函数模板特化与类模板特化。
2. 函数模板特化
语法:
- 关键字template后面接一对空的尖括号<>
- 函数名后跟一对尖括号,尖括号中指定需要特化的类型
// 函数模板 -- 参数匹配 template<class T> bool Less(T left, T right) {return left < right; }// 对Less函数模板进行特化 template<> bool Less<Date*>(Date* left, Date* right) {return *left < *right; }int main1() {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; }
缺陷(“大坑”):
如果基础模板参数是const类型,而特化参数为指针,要特别小心注意将const放在指针的
*
号后面( const 在*
前修饰的指针指向的对象,*
后修饰的是指针本身。我们要的当然是对指针本身进行const修饰)//基础模板 template<class T> bool Less(const T& left, const T& right) {return left < right; }//特化 template<> bool Less<Date*>(Date* const& left, Date* const& right) //注意Date*与const的位置关系 {return *left < *right; }void less_test() {Date* p1;Date* p2;Less(p1, p2); }
因此,对于上述情况,不建议使用函数模板。建议直接重载一份现成的函数,手动控制逻辑,以免出错。(使用模板时,我们很容易直接将T替换为Date*,而进行重载也许可以避开使用const,就算避不开也可以强迫我们进行代码逻辑的梳理,比较容易发现“坑点”)
//函数重载 bool Less(Date* left, Date* right) {return *left < *right; }
3. 类模板特化
3.1 全特化
语法:
- 关键字template后面接一对空的尖括号<>
- 类名后跟一对尖括号,尖括号中指定需要特化的类型
概念:
将模板参数列表中所有的参数都确定化。
//基础模板 template<class T1, class T2> class Data { public:Data() { cout << "Data<T1, T2>" << endl; } private:T1 _d1;T2 _d2; };//全特化类模板 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; //调用全特化模板 }
3.2 偏特化 / 半特化
部分参数特化
// 将第二个参数特化为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; };//两个参数偏特化为引用类型 template <typename T1, typename T2> class Data <T1&, T2&> { public:Data(const T1& d1, const T2& d2): _d1(d1), _d2(d2){cout << "Data<T1&, T2&>" << endl;} private:const T1& _d1;const T2& _d2; };void test2() {Data<int*, int*> d3; // 调用特化的指针版本Data<int&, int&> d4(1, 2); // 调用特化的指针版本 }
注意:
class Data <T1*, T2*>
中的<T1*, T2*>
只是个标识符,并不是只指一级指针,而是指所有指针T1,T2的类型就是传入参数的类型,但是在类内使用时会被替换为T1去掉一个*号后的类型(一级指针变为非指针,二级指针变为一级指针)
- 运行如下代码可以发现,T1,T2大小为4或者8,说明是一个指针,而打印它们的类型却显示为去掉了一个*后的类型,说明在类中使用时经过了处理
//两个参数偏特化为指针类型 template <typename T1, typename T2> class Data <T1*, T2*> { public:Data(){cout << "Data<T1*, T2*>" << endl;cout << sizeof(T1) << " " << sizeof(T2) << endl;cout << typeid(T1).name() << " " << typeid(T2).name() << endl;cout << typeid(_d1).name() << " " << typeid(_d2).name() << endl;} private:T1 _d1;T2 _d2; };void test2() {Data<int*, int*> d3; // 调用特化的指针版本Data<int**, int**> d5; // 调用特化的指针版本 }
3.3 应用示例
场景引入:
STL中的Priority_queue默认的仿函数只能对最基础的类型进行大小比较,然后按堆规则排序,如果需要对特定类型进行大小比较,需要用户手动传入一个专用的仿函数进行特殊处理。
同样都是特殊处理,我们能不能使用特化代替专用的仿函数?
代码对比:
传入专门的仿函数:
priority_queue < Date*, vector<Date*>, PDateless > q1;
// 模拟priority_queue中的缺省仿函数 template<class T> class myless { public:bool operator()(const T& x, const T& y){return x < y;} };//针对指针类型作对象时 手动传入的仿函数 struct PDateLess {bool operator()(Date* p1, Date* p2){return *p1 < *p2;} };
特化缺省仿函数:
// 缺省提供的仿函数 template<class T> class myless { public:bool operator()(const T& x, const T& y){return x < y;} };//对其特化版本,使之符合指针对象作元素时的比较 template<> class myless<Date*> { public:bool operator()(Date* const & x, Date* const & y) //也存在函数模板特化时的问题{return *x < *y;} };
4. 小结
- 函数模板特化实例化后就相当于函数重载
- 类模板特化实例化就“相当于重载了一个类”,务必注意c++语法中没有类重载这个概念,只是效果看起来可以这么理解
五、模板的分离编译
1. 分离编译的概念
一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件 ,其中共用的代码(函数)一般采用声明和定义分离的方式,使用时源文件中只包含其头文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。
2. 模板不能分离编译
分离编译的实现原理
C/C++的编译过程分为预处理、编译、汇编、链接四个过程,其中编译是进行语法错误检查的,检查无误后才生成汇编代码。(头文件不参与编译)
当函数经过编译时,如果只有声明,编译器此时是找不到函数地址的,但是认为后续链接时可以找到,所以会生成一个
call xxxx
的汇编指令,将其函数名放入到符号表中,让编译通过,生成 .obj文件。正常情况下,链接时 编译器会扫描各个符号表,寻找那些只有声明的那些函数的实际地址并将其替换到相应位置,然后再生成可执行文件。
符号表:
它是编译器在编译过程中创建的一种数据结构,用于存储变量名和内存地址之间的映射关系。主要作用是在编译时期将变量名转换为内存地址。当声明一个变量时,编译器会为其分配内存,并将变量名和分配的内存地址记录在符号表中。
例如,声明 int a; 后,编译器可能会为 a 分配一个内存地址 0x0040,并将 a 和 0x0040 的映射关系保存在符号表中。这样,在程序中对 a 进行操作时,编译器就可以通过符号表找到 a 的地址
但对于模板来说,C++标准明确表示,当一个模板不被用到的时侯它就不该被实例化出来即 按需实例化,这会导致什么呢?假设我们在 main.cpp中调用了函数模板,但是只有该函数的声明,定义在text.cpp文件中实现。正常情况链接时编译器会从text.cpp的符号表中找到该函数的地址,然后给main.cpp调用,可是我们刚说了模板是按需实例化,即text.cpp文件中即使有定义,但是没有在其中调用就不会被实例化,没有实例化就没有地址(没有开辟物理空间),那编译器不就是找了个寂寞吗?自然就会报错说找不到地址。
实际上就是想用的地方(main.cpp)调用不到,不需要用的地方(text.cpp)又需要调用才能实例化给想用的地方传地址
按需实例化:
模板、类成员函数,没有被调用时编译器不会对其进行实例化,不会对其进行编译。如果代码写的有问题则编译时不会语法报错,但是链接时如果被调用就可能会出错
可以认为它们相较于普通函数,多了一步实例化的过程
//-------------text.h----------------//void func(); // 函数声明//---------------text.cpp-------------//#include"text.h"template<class T> void func(T left, T right) //函数定义 {cout << "func被实现" << endl; }int main() {return 0; //没有调用函数模板,不会进行实例化 }//---------------main.cpp---------------//#include"text.h"int main() {func(1, 2); // 调用函数模板 }
解决方法
将声明和定义放到一个文件 xxx.h 中
会重定义吗?不会,因为c++标准中明确规定了编译器可以丢弃部分语句,其中就包括了相同的模板实例,会从中随机选取一份保留,其余丢弃。简而言之,就是编译器给模板开后门进行了特殊处理。
那理论上普通函数也可以这么处理,不就不存在重定义问题了?是这样的,但是C++有C的历史包袱,祖宗之法不可变,不然会有兼容问题
显式实例化
- 根据调用时的实际参数将模板实现,就相当于一个普通函数,自然会被实例化
- 不建议这样使用,每使用一个新类型就要显示实例化一次(本来使用模板就是为了省事,这样不就是一个个进行函数重载吗?模板此时啥用没有,全是用户手动在操作)
//---------------text.cpp-------------//#include"text.h"template<class T> void func(T left, T right) //函数定义 {//... }//显式实例化,每调用一个新类型都要补上一次显式实例化 void func(int left, int right) {//... } //---------------main.cpp---------------//#include"text.h"int main() {func(1, 2); // 调用函数模板 }
六、模板总结
【优点】
- 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生
- 增强了代码的灵活性
【缺陷】
- 模板会导致代码膨胀问题,也会导致编译时间变长
- 出现模板编译错误时,错误信息非常凌乱,不易定位错误
总结
本文讲解了模板的相关使用和常见误区。
尽管文章修正了多次,但由于水平有限,难免有不足甚至错误之处,敬请各位读者来评论区批评指正。