本文已收录至《C++语言》专栏!
作者:ARMCSKGT
目录
前言
正文
泛型编程
问题引入
泛型
函数模板
概念
格式
使用方式
模板原理
模板的实例化
隐式实例化
显示实例化
模板匹配规则
类模板
类模板定义格式
类模板的实例化
非类型模板参数
模板的特化
概念
函数模板的特化
类模板的特化
关于类模板的特化
仿函数的定义和使用
模板分离编译
什么是分离编译
模板的分离编译
解决方案
模板总结
优点
缺点
最后
前言
C++为了提高开发效率,提高代码的复用性增加了模板的概念;模板就像打印卷子一样,100份卷子只需要一个模板卷子就能打印出来,但模板的神奇不止于此,通过这篇文章,我将为您打开泛型编程的大门!
正文
C++的模板语法是根据泛型编程而来的,为了提高代码的复用性!
泛型编程
问题引入
在C语言中,我们要实现一个swap交换函数,只能应付一种数据类型,且C语言没有函数重载,对于这种场景及其乏力。
而在C++中有函数重载,但是除了内置类型还有无数的自定义类型,如果每一个类型都写一个swap函数,那么代码的重复性就太高了。
//int类型的swap void Swap(int& left, int& right) { int temp = left; left = right; right = temp; } //double类型的swap void Swap(double& left, double& right) { double temp = left; left = right; right = temp; } //char类型的swap void Swap(char& left, char& right) { char temp = left; left = right; right = temp; } ......
使用函数重载虽然可以实现,但是有一下几个不好的地方:
- 重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增加对应的函数
- 代码的可维护性比较低,一个出错可能所有的重载均出错
泛型
那能否告诉编译器一个模子,让编译器根据不同的类型利用该模子来生成代码呢?
如果在C++中,也能够存在这样一个模具,通过给这个模具中填充不同材料(类型),来获得不同材料的铸件(即生成具体类型的代码),那将会节省许多头发。巧的是前人早已将树栽好,我们只需在此乘凉。
泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。
函数模板
概念
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
格式
- 关键字:template
- 模板参数定义:template<typename T>或template<class T>
- 注意:typename是用来定义模板参数关键字,也可以使用class(切记:不能使用struct代替class)
- T是模板参数名,可以自定义模板参数名,typename和class可以在一个模板中定义参数(注意:只能使用这两个关键字定义,struct等其他关键字无法定义模板参数),多个模板参数之间通过逗号,分隔!
- 一个模板的所有的模板参数只能被一个类或函数使用
//示例 template<typename 模板参数1, typename 模板参数2,......,typename 模板参数n> template<class 模板参数1, class 模板参数2,......,class 模板参数n> template<typename 模板参数1, class 模板参数2,......>
使用方式
//在使用前定义好模板参数和模板函数 template<class T> //定义模板参数T void Print(T data) { //typeid(T).name() 以字符串的形式输出T的数据类型 cout << typeid(T).name() << " : " << data << endl; //cout << typeid(data).name() << " : " << data << endl;的显示结果也是一样的 }
模板原理
函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器!
在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型也是如此!
所以对于上面的示例,当我们调用模板函数时,编译器在编译时通过识别不同的数据类型实例化出对应类型的该函数,从而实现了不同类型调用同一个函数,底层上这些函数构成函数重载,但实际上我们以为只有一个函数,其实是编译器帮我们写了这些类型的函数!
Linux环境下,发现其汇编指令中调用了四个不同的函数(call是调用函数)!
模板的实例化
用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化。
隐式实例化
隐式实例化是让编译器通过参数自动确定参数类型从而形成对应的函数,但是隐式实例化也可能存在一些问题!
#include <iostream> using namespace std; template<class T> T Add(T left,T right) { return left+right; } int main() { cout<<Add(1,2)<<endl; //该处正常编译 cout<<Add(1.0,2)<<endl; //此处发生异常 return 0; }
main函数中第二句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型通过实参left将T推演为double,通过实参right将T推演为int类型,但模板参数列表中只有一个T, 编译器无法确定此处到底该将T确定为 int 或者 double 类型而报错注意:在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,编译器就需要背黑锅 Add(left, right);
这里的细节:根据编译器函数名生成规则,当编译器识别到1.0时,将T确定为double,此时生成的函数名是_3Adddd,而实际调用时是_3Adddi,此时发生链接错误,编译器直接报错!
如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错。
这种情况下有两种解决办法:
- 1.强制类型转换
- 2.定义多个模板参数
- 3.显示实例化
1.强制类型转换
#include <iostream> using namespace std; template<class T> T Add(T left,T right) { return left+right; } int main() { cout<<Add((int)1.0,2)<<endl; //对1.0进行强制类型转换 cout<<Add(1.0,(double)2)<<endl; //对2进行强制类型转换 return 0; }
注意:在这里因为强制类型转换会产生临时变量,所以在传参时选择进行值传递,如果要使用引用传递,则需要加上const!
2.定义多个模板参数
#include <iostream> using namespcae std; template<class T1,class T2> //定义两个模板参数T1,T2 T2 Add(T1 left, T2 right) //返回值类型只能是模板参数中的其中一个 { return left + right; } int main() { cout << Add(1.0, 2) << endl; cout << Add(1.0, 2.2) << endl; return 0; }
对于定义多个模板参数,如果函数有返回值,则只能由一个模板参数做返回值!
3.显示实例化
对于显示实例化,需要展开详细介绍!
显示实例化
使用方法
#include <iostream> using namespace std; template<class T> T Add(T left, T right) { cout << typeid(T).name() << endl; return left + right; } int main() { cout<<Add<int>(1.0,2)<<endl; //显示实例化为int类型的函数 cout<<Add<double>(1.0,2)<<endl; //显示实例化为double类型的函数 return 0; }
格式
模板函数名<类型>(参数); //将模板函数实例化为指定参数类型 模板类<类型> 实例对象; //实例化模板类为指定参数类型(后面会介绍类模板)
这里我们认识了新的符号 < > 用于实例化模板!
我们显示实例化的模板函数支持隐式类型转换,但是编译器推演的模板函数不支持隐式类型转换!
模板匹配规则
1.一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数。因为无论是模板函数和显示定义的函数都构成重载,所以编译器会选择最匹配的那个函数进行调用,如果我们直接实现的函数是最佳选择则编译器不会实例化对应的模板函数!
#include <iostream> using namespcae std; template<class T> T Add(T left, T right) { cout << typeid(T).name() << endl; return left + right; } double Add(double left, int right) //再次显示定义Add函数 { cout << "_3Adddi" << endl; return left + right; } int main() { cout << Add(1.0, 2) << endl; //显示实例化为int类型的函数 cout << Add<int>(1.1, 2.2) << endl; //如果我们显示实例化则编译器会调用模板函数 cout << Add(1, 2.2) << endl; //否则会调用其他较为匹配的函数 return 0; }
2. 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模 板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板!
#include <iostream> using namespace std; template<class T> T Add(T left, T right) { cout << typeid(T).name() << endl; return left + right; } double Add(double left, double right) { cout << "_3Adddd" << endl; return left + right; } int main() { cout << Add(1.2, 2.3) << endl; cout << Add(1, 2) << endl; return 0; }
3. 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换
类模板
类模板定义格式
#include <iostream> using namespace std; //template<class T1,class T2,...,class Tn> //模板参数列表 template<class T> class Test { public: Test() {} Test(T n) :_num(n) {} void print() { cout << typeid(T).name() << endl; cout << _num << endl; } private: T _num; };
类模板的实例化
前面我们简单介绍了模板显示实例化,对于类模板实例化与函数模板实例化不同,函数模板可以通过参数推到,但类模板只能显示实例化,类模板实例化需要在类模板名字后跟 < > ,然后将实例化的类型放在 < > 中即可,类模板名字不是真正的类,而实例化的对象才是真正的类。
//对于上面的类模板 int main() { Test<int> i(1); //其中Test是类名,Test<int>才是类型 Test<char> c('a'); Test<double> d(3.14); i.print(); c.print(); d.print(); return 0; }
关于类模板的一些注意事项
- 模板类中的函数在定义时,如果没有在类域中,就需要通过 类模板+ 类域访问 的方式定义
- 类模板不支持声明与定义分离(头文件声明,其他文件定义),会导致链接错误!
非类型模板参数
模板参数分类类型形参与非类型形参。
类型形参:出现在模板参数列表中,跟在class或者typename之类的参数类型名称。
非类型形参:就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用,这个常量类似于函数参数也可以给缺省值,但是这个参数一旦实例化就不可修改。
// 定义一个模板类型的静态数组 template<class T, size_t N = 10>//这里N相当于给了缺省值10,也可以传递其他整数作为参数 class _array { public: _array() :_size(N) {} T& operator[](size_t index) { return _arr[index]; } const T& operator[](size_t index)const { return _arr[index]; } size_t size()const { return _size; } bool empty()const { return 0 == _size; } private: T _arr[N]; size_t _size; }; //这个是我们自己实现的,在库中确实存在array这个容器,有兴趣的小伙伴可以了解了解
注意
- 浮点数、类对象以及字符串是不允许作为非类型模板参数的
- 非类型的模板参数必须在编译期就能确认结果
模板的特化
特化必须建立在有原模板的基础上!
概念
通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理,比如:实现了一个专门用来进行相加的函数模板。
class Date //日期类 { public: Date() = default; Date(int y, int m, int d) :_y(y) , _m(m) , _d(d) {} bool operator<(const Date& d) { return _y < d._y || _y == d._y && _m < d._m || _y == d._y && _m == d._m && _d < d._d; } bool operator>(const Date& d) { return !(*this < d); } int _y; int _m; int _d; }; 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; }
显然编译器内置的<运算符无法对指针所指的内容进行比较!
此时,就需要对模板进行特化。即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。模板特化中分为函数模板特化与类模板特化。
函数模板的特化
函数模板的特化步骤:
- 必须要先有一个基础的函数模板
- 关键字template后面接一对空的尖括号<>
- 函数名后跟一对尖括号,尖括号中指定需要特化的类型
- 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误
对以上代码中的指针特殊情况进行特化
// 对Less函数模板进行特化 template<> bool Less<Date*>(Date* left, Date* right) { return *left < *right; //解引用转而调用Date自己的运算符重载 } int main() { Date d1(2022, 7, 7); Date d2(2022, 7, 8); Date* p1 = &d1; Date* p2 = &d2; cout << Less(p1, p2) << endl; // 可以比较,结果错误 return 0; }
注意:一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接接出。
bool Less(Date* left, Date* right) { return *left < *right; }
该种实现简单明了,代码的可读性高,容易书写,因为对于一些参数类型复杂的函数模板,特化时特别给 出,因此函数模板不建议特化。
类模板的特化
1.全特化
全特化即是将模板参数列表中所有的参数都确定化。
#include <iostream> using namespace std; template<class T1, class T2> class Test { public: Test() { cout << "Data<T1, T2>" << endl; } private: T1 _d1; T2 _d2; }; template<> //全特化 class Test<int, char> { public: Test() { cout << "Data<int, char>" << endl; } private: int _d1; char _d2; }; int main() { Test<int, int> d1; Test<int, char> d2; return 0; }
2.偏特化
偏特化:任何针对模版参数进一步进行条件限制设计的特化版本。比如对于以下模板类:
template<class T1, class T2> class Test { public: Test() { cout << "Data<T1, T2>" << endl; } private: T1 _d1; T2 _d2; };
偏特化有以下两种表现方式:
- 部分特化
将模板参数类表中的一部分参数特化
//对于上面的Test类 //将第二个参数特化为int template <class T1> class Test<T1, int> { public: Test() { cout << "Data<T1, int>" << endl; } private: T1 _d1; int _d2; };
- 参数更进一步的限制
偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本。
//两个参数偏特化为指针类型 template <typename T1, typename T2> class Test <T1*, T2*> { public: Test() { cout << "Data<T1*, T2*>" << endl; } private: T1 _d1; T2 _d2; }; //两个参数偏特化为引用类型 template <typename T1, typename T2> class Test <T1&, T2&> { public: Test(const T1& d1, const T2& d2) : _d1(d1) , _d2(d2) { cout << "Data<T1&, T2&>" << endl; } private: const T1& _d1; const T2& _d2; }; int main() { Test<double, int> t1; // 调用特化的int版本 Test<int, double> t2; // 调用基础的模板 Test<int*, int*> t3; // 调用特化的指针版本 Test<int&, int&> t4(1, 2); // 调用特化的指针版本 return 0; }
关于类模板的特化
#include <iostream> using namespace std; #include<vector> //vector容器 - 这里大家可以当成顺序表理解即可 #include <algorithm> class Date //日期类 { public: Date() = default; Date(int y, int m, int d) :_y(y) , _m(m) , _d(d) {} bool operator<(const Date& d) { return _y < d._y || _y == d._y && _m < d._m || _y == d._y && _m == d._m && _d < d._d; } bool operator>(const Date& d) { return !(*this < d); } int _y; int _m; int _d; }; template<class T> //仿函数实现比较 struct Less { bool operator()(const T& x, const T& y) const { return x < y; } }; int main() { Date d1(2022, 7, 7); Date d2(2022, 7, 6); Date d3(2022, 7, 8); vector<Date> v1; v1.push_back(d1); v1.push_back(d2); v1.push_back(d3); // 可以直接排序,结果是日期升序 sort(v1.begin(), v1.end(), Less<Date>()); vector<Date*> v2; v2.push_back(&d1); v2.push_back(&d2); v2.push_back(&d3); // 可以直接排序,结果错误日期还不是升序,而v2中放的地址是升序 // 此处需要在排序过程中,让sort比较v2中存放地址指向的日期对象 // 但是走Less模板,sort在排序时实际比较的是v2中指针的地址,因此无法达到预期 sort(v2.begin(), v2.end(), Less<Date*>()); return 0; }
通过观察上述程序的结果发现,对于日期对象可以直接排序,并且结果是正确的。但是如果待排序元素是指 针,结果就不一定正确。因为:sort最终按照Less模板中方式比较,所以只会比较指针,而不是比较指针指 向空间中内容,此时可以使用类版本特化来处理上述问题:
// 对Less类模板按照指针方式特化 template<> struct Less<Date*> { bool operator()(Date* x, Date* y) const { return *x < *y; } };
特化之后,在运行上述代码,就可以得到正确的结果!
只有类模板有全特化和偏特化的概念,函数模板只有特化的概念!
仿函数的定义和使用
//定义一个相加的仿函数 template<class T> //可以使用模板参数也可以指定仿函数的参数类型 struct Add //类对象 { T operator()(const T& left, const T& right) //重载 () 自定义函数功能 { return left + right; } }; int main() { cout << Add<int>()(1, 2) << endl; //通过匿名对象调用仿函数 cout << Add<double>()(1.1, 2.2) << endl; return 0; }
仿函数是通过重载运算符 () 实现的,在一些涉及比较大小的函数例如sort排序和部分STL容器中会使用,通常less是小,greater是大,在使用时进行实例化就行了,之所以会有仿函数这样的语法,是因为C语言对于将函数作为参数传递十分不友好,C++使用仿函数传递参数极大的方便了函数作为参数进行传递的不足!
对于仿函数的一些细节,在后续会进行介绍!
模板分离编译
什么是分离编译
一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。
模板的分离编译
假如有以下场景,模板的声明与定义分离开,在头文件中进行声明,源文件中完成定义:
// test.h template<class T> T Add(const T & left, const T & right); // test.cpp template<class T> T Add(const T & left, const T & right) { return left + right; } // main.cpp #include"test.h" int main() { Add(1, 2); Add(1.0, 2.0); return 0; }
分析:
解决方案
1. 将声明和定义(实现)放到一个文件 "xxx.hpp" 里面或者"xxx.h"其实也是可以的(推荐使用这种)。
2. 模板定义的位置显式实例化;这种方法不实用,不推荐使用。
声明和定义放在一起,直接就可以实例化,编译时就有地址,不需要链接(去其他文件中找),而且如果是在对象中,对于短小的函数还可能会被编译器修饰为内联函数直接展开!
模板总结
优点
- 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生
- 增强了代码的灵活性
缺点
- 模板会导致代码膨胀问题(编译器生成过多的模板函数),也会导致编译时间变长
- 出现模板编译错误时,错误信息非常凌乱,不易定位错误
最后
<C++ 模板>的介绍到这里就差不多结束了,相信模板的到来一定可以解决大家困惑已久因为类型而导致代码重复的问题,合理的使用模板可以极大的提高我们的生成效率和代码复用,而且模板会贯穿我们C++学习的始终,包括后面STL容器就是通过模板实现了所有类型的兼容,所以对于模板的掌握是必不可少的!
本次 <++ 模板> 就先介绍到这里啦,希望能够尽可能帮助到大家。
如果文章中有瑕疵,还请各位大佬细心点评和留言,我将立即修补错误,谢谢!
🌟其他文章阅读推荐🌟
C++ <内存管理> -CSDN博客
C++ <类和对象 - 下> -CSDN博客
C++ <类和对象 - 中> -CSDN博客
C++ <类和对象 - 上> -CSDN博客
🌹欢迎读者多多浏览多多支持!🌹