
目录1. 泛型编程2. 函数模板2.1 概念与语法2.2 模板的实例化2.3 模板参数的匹配原则3. 类模板3.1 概念与语法3.2 补充typename 的特殊用法4. 非类型模板参数5. 模板的特化5.1 为什么需要特化5.2 函数模板特化 vs 普通函数重载5.3 类模板特化5.3.1 全特化5.3.2 偏特化半特化6. 模板的分离编译6.1 什么是分离编译6.2 模板导致分离编译链接失败的原因6.3 解决方法7. 总结1. 泛型编程在日常开发中如果需要实现一个通用的交换函数使用函数重载虽然可以实现但存在明显的缺陷重载的函数仅仅是类型不同代码复用率比较低只要有新类型出现时就需要用户自己增加对应的函数并且代码的可维护性比较低一个出错可能所有的重载均出错。泛型编程就是为了解决这一问题而产生的。泛型编程是指编写与类型无关的通用代码它是代码复用的一种手段。模板是泛型编程的基础。可以把模板想象成一个模具在 C 中通过给这个模具填充不同的材料数据类型就能获得不同材料的铸件即生成具体类型的代码从而将重复的工作交给了编译器去完成。2. 函数模板2.1 概念与语法函数模板代表了一个函数家族该函数模板与类型无关在使用时被参数化编译器会根据实参类型产生函数的特定类型版本。定义格式templatetypenameT1,typenameT2,...,typenameTn返回值类型 函数名(参数列表){}注意typename是用来定义模板参数的关键字也可以使用class但切记不能使用struct代替。代码示例templatetypenameTvoidSwap(Tleft,Tright){T templeft;leftright;righttemp;}原理函数模板本身并不是函数而是编译器产生特定具体类型函数的模具。在编译阶段编译器根据传入的实参类型推演生成对应类型的函数以供调用。2.2 模板的实例化用不同类型的参数使用函数模板时称为函数模板的实例化分为隐式实例化和显式实例化。隐式实例化让编译器根据实参推演模板参数的实际类型。显式实例化在函数名后的中指定模板参数的实际类型。如果类型不匹配编译器会尝试进行隐式类型转换如果无法转换成功编译器将会报错。templatetypenameTTAdd(constTleft,constTright){coutT Add(const T left, const T right)endl;returnleftright;}intmain(){inta110,a220;doubled110.1,d220.2;Add(a1,a2);// 隐式实例化推演 T 为 intAdd(d1,d2);// 隐式实例化推演 T 为 double// 当参数类型不一致时直接调用会报错因为编译器无法确定 T 是 int 还是 double。// 处理方式 1强制类型转换coutAdd(a1,(int)d2)endl;coutAdd((double)a1,d2)endl;// 处理方式 2显式实例化不再让编译器去推演参数coutAddint(a1,d2)endl;return0;}2.3 模板参数的匹配原则一个非模板函数可以和一个同名的函数模板同时存在而且该函数模板还可以被实例化为这个非模板函数。对于非模板函数和同名函数模板如果其他条件都相同在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数那么将选择模板。模板函数不允许自动类型转换但普通函数可以进行自动类型转换。// 专门处理 int 的普通加法函数intAdd(intleft,intright){coutint Add(int left, int right)endl;returnleftright;}// 通用加法函数模板templateclassT1,classT2autoAdd(T1 left,T2 right){coutauto Add(T1 left, T2 right)endl;returnleftright;}intmain(){Add(1,2);// 优先匹配普通函数不需要函数模板实例化Add(1.1,2.2);// 模板函数可以生成更加匹配的版本编译器生成 auto Add(double, double)Add(1,2.0);// 普通函数需要类型转换而模板能精确匹配调用模板return0;}3. 类模板3.1 概念与语法类模板的定义格式与函数模板类似但实例化方式有所不同。类模板实例化需要在类模板名字后跟然后将实例化的类型放在其中。类模板名字不是真正的类实例化的结果才是真正的类。代码示例动态顺序栈实现templatetypenameTclassStack{public:Stack(size_t capacity4){_array(T*)malloc(sizeof(T)*capacity);if(nullptr_array){perror(malloc申请空间失败);return;}_capacitycapacity;_size0;}voidPush(constTdata);private:T*_array;size_t _capacity;size_t _size;};// 模板不建议声明和定义分离到 .h 和 .cpp会出现链接错误。要分离也建议都在 .h 中。// 类模板中函数放在类外进行定义时需要加模板参数列表templateclassTvoidStackT::Push(constTdata){_array[_size]data;_size;}intmain(){Stackintst1;// Stack 是类模板Stackint 才是真正的类类型Stackdoublest2;return0;}3.2 补充typename 的特殊用法当在模板内部使用某些依赖于模板参数的嵌套类型时如迭代器编译器无法确认它是一个静态变量还是一个类型。此时需要使用typename明确告知编译器这是一个类型。templateclassTvoidPrintVector(constvectorTv){// 类模板没实例化时不去里面查细节东西无法确认时类型还是静态变量// 加 typename 明确告诉是类型typenamevectorT::const_iterator itv.begin();// 现代 C 也可以直接使用 auto it v.begin();while(it!v.end()){cout*it ;it;}coutendl;}4. 非类型模板参数模板参数分为类型形参与非类型形参。类型形参出现在模板参数列表中跟在class或者typename之类的参数类型名称。非类型形参用一个常量作为类或函数模板的一个参数在类或函数模板中可将该参数当成常量来使用。// 静态的栈T 为类型形参N 为非类型形参templateclassT,size_t N10classStack{public:voidfunc(){// N 是常量不能被修改N 会报错}private:T _a[N];int_top;};intmain(){Stackintst1;// 容量为 10Stackint,100st2;// 容量为 100return0;}注意浮点数、类对象以及字符串是不允许作为非类型模板参数的C20 之前。非类型的模板参数必须在编译期就能确认结果。5. 模板的特化通常情况下使用模板可以实现一些与类型无关的代码但对于一些特殊类型的可能会得到一些错误的结果需要特殊处理。模板特化即在原模板类的基础上针对特殊类型所进行特殊化的实现方式。5.1 为什么需要特化以下面的Date类和Less模板为例如果比较的是指针类型模板会比较指针的地址大小而非指针指向的内容大小这违背了我们的本意。classDate{public:Date(intyear1900,intmonth1,intday1):_year(year),_month(month),_day(day){}booloperator(constDated)const{/* 日期比较逻辑 */returntrue;}private:int_year,_month,_day;};templateclassTboolLess(constTleft,constTright){returnleftright;}intmain(){Dated1(2022,7,7);Dated2(2022,7,8);coutLess(d1,d2)endl;// 比较对象结果正确Date*p1d1;Date*p2d2;coutLess(p1,p2)endl;// 比较指针地址结果错误return0;}5.2 函数模板特化 vs 普通函数重载虽然可以对函数模板进行特化通过template且尖括号中指定特化类型但函数模板的特化要求形参列表必须和模板函数的基础参数类型完全相同否则编译器可能报奇怪的错误。因此一般遇到函数模板不能处理或处理有误的类型为了实现简单通常都是直接给出一个重载的普通函数这样代码可读性高容易书写不建议特化函数模板。// 直接重载普通函数比函数模板特化更好boolLess(Date*left,Date*right){return*left*right;}5.3 类模板特化类模板特化分为全特化和偏特化。5.3.1 全特化全特化即是将模板参数列表中所有的参数都确定化。templateclassT1,classT2classData{public:Data(){coutDataT1, T2-原模板endl;}};// 全特化针对具体的 int 和 chartemplateclassDataint,char{public:Data(){coutDataint, char- 全特化endl;}};5.3.2 偏特化半特化偏特化是任何针对模板参数进一步进行条件限制设计的特化版本。表现为两种形式部分特化将模板参数类表中的一部分参数特化。参数更进一步的限制针对模板参数更进一步的条件限制如限定为指针或引用。// 1. 部分特化将第二个参数特化为 inttemplateclassT1classDataT1,int{public:Data(){coutDataT1, int-偏特化endl;}};// 2. 参数进一步限制限定模板的类型为指针templatetypenameT1,typenameT2classDataT1*,T2*{public:Data(){coutDataT1*, T2*-偏特化endl;}};// 2. 参数进一步限制限定模板的类型为引用templatetypenameT1,typenameT2classDataT1,T2{public:Data(){coutDataT1, T2endl;}};intmain(){Dataint,intd1;// 调用基础模板Dataint,chard2;// 调用全特化Datadouble,intd3;// 调用部分特化 (T1, int)Dataint*,double*d4;// 调用指针偏特化Dataint,intd6;// 调用引用偏特化return0;}6. 模板的分离编译6.1 什么是分离编译一个程序项目由若干个源文件共同实现而每个源文件单独编译生成目标文件最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。6.2 模板导致分离编译链接失败的原因如果将模板的声明放在a.h定义放在a.cpp并在main.cpp中调用该模板。 C/C 程序编译流程为预处理 - 编译 - 汇编 - 链接。 编译器对工程中的多个源文件是分离开单独编译的。在编译a.cpp时编译器没有看到对模板函数的实例化因此不会生成具体的函数代码。而在main.obj中调用了该函数链接器在链接时去寻找其地址发现找不到从而引发无法解析的外部符号链接报错。//a.h#pragmaonce// 模板的声明templateclassTTAdd(constTleft,constTright);//a.cpp#includea.h// 模板的定义templateclassTTAdd(constTleft,constTright){returnleftright;}//main.cpp#includeiostream#includea.husingnamespacestd;intmain(){// 编译时这里会触发“无法解析的外部符号” (Unresolved external symbol) 的链接错误coutAdd(1,2)endl;return0;}6.3 解决方法推荐做法将声明和定义放到一个文件xxx.hpp里面或者直接写在.h中。不推荐做法在模板定义的位置进行显式实例化。这种方法缺乏灵活性极不实用。7. 总结【优点】模板复用了代码节省资源更快的迭代开发C 的标准模板库 (STL) 因此而产生。增强了代码的灵活性。【缺陷】模板会导致代码膨胀问题也会导致编译时间变长。出现模板编译错误时错误信息非常凌乱不易定位错误。