【C++】模板进阶:非类型模板参数与模板的特化 📅 2026/8/13 23:29:43 博主名称鱼子星_✅数据结构专栏【数据结构】✅算法竞赛专栏【算法竞赛】✅C系列专栏【C从零开始系列】目录一. 非类型的模板参数1. 非类型模板参数定义方式2. 非类型模板参数的使用场景二. 模板的特化1. 函数模板的特化2. 函数模板特化的定义3. 函数模板特化与函数重载2. 类模板的特化2.1 类模板的全特化2.2 类模板的偏特化2.3 类模板特化的应用-仿函数的改造三. 模板声明和定义不可分离原因问题场景定义和声明分离编译和链接过程发生了什么本文知识点思维导图一. 非类型的模板参数C中的模板是泛型编程的核心有了模板这个东西之后减少了很多逻辑相似的代码且STL也是基于模板才产生。由前面的学习我们知道模板的参数大多是类型而不是对象这也符合编译器推断模板的类型这个规则。但是模板参数并不是只能传递类型还可以传递对象指针和引用。当某个对象作为模板参数时实参只能传递常量而不能传递变量且在模板实例化之后这个对象也为常量当指针或引用作为模板参数时实参只能传递生命周期为全局的指针或引用1. 非类型模板参数定义方式非类型的模板参数的定义和函数的形参的定义差不多。非类型的模板参数定义如下templateclassT,size_t Nclassstack{private:T _a[N];size_t _size;};对于模板类stack其中的N为整型常量而如果想要实例化这个模板类第二个模板参数只能传递常量而不能传递变量。✅正确示范非类型的模板参数传递常量stackint, 10 st;❌错误示范非类型的模板参数传递变量int a 10; stackint, a st;2. 非类型模板参数的使用场景非类型模板参数的使用场景有2个如下所示需要同时存在不同大小的静态栈当程序中需要的是不同大小的静态栈时此时对于模板类stack来说就需要一个整型常量作为模板参数来控制静态栈的大小。stackint,100st1;stackint,1000st2;std::array的定义std::array文档标准库中对于静态数组的封装(std::array)也用到了非类型的模板参数如图1-1所示其中非类型的模板参数的主要作用其实和场景1相似。图 1-1 std::array的非类型模板参数二. 模板的特化模板可以通过编译器的推断使得所有相同样式但不同类型的调用都执行相同逻辑的操作但是这种所有的相同样式的调用都执行一套逻辑也有缺陷。对于某些特殊的类型其调用时并不希望和其它类型一样执行模板内部的操作而是有其特殊的操作需求对于这种情况就需要使用模板的特化。模板的特化也叫特例化即对一个模板进行特殊化的处理使得某些特殊的类型调用到这个模板时和其它类型调用模板时执行不一样的操作/代码。1. 函数模板的特化下面来看一个普通的模板函数Compare此函数用于比较两个相同类型的对象的大小。该函数可以用于大部分类型的对象进行比较如intdoublestring……但是如果此时需要比较两个指向int类型对象的指针所指向对象的大小这个比较函数就无法正确的对所指向对象进行比较而是只会对指针的进行比较。templateclassTboolCompare(constTa,constTb){returnab;}2. 函数模板特化的定义对于这种使用模板不能满足一些特殊类型的需求的情况就需要对指针类型进行特化。函数模板的特化需要在函数整体的之前加上关键字template和一对空的尖括号()来表示这是一个函数模板的特化函数名和原先的函数名相同特化的函数模板需要在函数名之后加上需要特化的类型需要特化的类型需要使用尖括号()括起来函数参数需要和原模板函数的参数一 一对应。下面以整型作为需要特化的类型为例假设我们的需求是当使用Compare比较整型时需要当a b时才返回true否则返回false。templateboolCompareint(constinta,constintb){returnab;}我们真正所需要的还是指针类型的特化但是需要注意的是指针引用函数模板特化这几个东西都参与之后会产生一些奇妙的效果。templateboolCompareint*(int*consta,int*constb){return*a*b;}指针的特化的参数的解释由于特化需要和原模版的参数一 一对应原先模板参数为const T a这个const修饰的是对象本身即 a所以对于指针类型的特化也应该让const修饰对象本身所以const需要放在*之后而因为原模版参数是对象的引用所以指针的特化也需要加上引用最终就形成了int* const a。3. 函数模板特化与函数重载函数模板的特化本质上是原函数模板的实例并不是创建了一个新的函数。举个例子小明是个跑腿专家你给他几块钱他就会给你带回几瓶水来你给他1块钱他会给你带1瓶矿泉水给他3块钱就会带3瓶矿泉水现在你给小明提出了新的要求“如果我给你3块钱你不要给我带3瓶矿泉水了而是给我带一瓶可乐回来”之后只要你给小明3块钱他就会带可乐而其它价钱依旧是矿泉水。这其中”给小明几块带几瓶矿泉水“就是函数原模版而“给3块钱带可乐回来”只是在原先模板上限定了特例就是模板的特化也就是模板的实例。而如果是叫小红帮忙跑腿那就是创建一个新的函数了。所以函数模板的特化不能影响函数重载的调用特性例如对于Compare函数如果此时写一个参数为int*类型的函数重载那么当调用Compare函数实参为两个int*类型的指针时调用的是函数重载的那个函数而不是函数模板特化的那个函数。// Compare函数的重载当实参传递为Compare(int*, int*)时优先调用这个函数boolCompare(int*consta,int*constb){return*a*b;}2. 类模板的特化除了函数模板外类模板也可以进行特化类模板的特化本质上也是模板的实例特化的类模板的内部实现可以和原类模板的实现不相同。类模板分为全特化和偏特化半特化下面我们来看类模板的这两种特化的特点。2.1 类模板的全特化类模板的全特化即对全部的模板参数都进行了特化处理类模板的全特化和函数模板的特化方式相似都需要在定义前面加上template以表示为类模板的全特化而在类名之后需要加上用尖括号括起来的特化的模板参数特化的模板参数需要和原模版参数一一对应这其中最难理解的就是参数需要一一对应下面用例子进行讲解。如下所示为类的原模版和特化的类模板其中特化的类模板的参数和原类模板的参数一 一对应即第一个参数int对应着类型T1第二个参数int对应着类型T2。templateclassT1,classT2// 原类模版classA{private:T1 _a1;T2 _a2;};template//特化的类模板classAint,int{private:T1 _a1;T2 _a2;};2.2 类模板的偏特化与函数模板不同类模板并不需要为全部的模板参数提供实参。类模板的特化可以指定部分而非全部的参数或者指定部分而非全部的参数的特性而这种特化方式就称为偏特化。类模板的偏特化需要为还没有确定类型的参数保留形参。注意只有类模板可以使用偏特化函数模板不能使用偏特化类模板的偏特化常见的形式有两种如下所示指定部分参数的类型当偏特化为指定部分参数的类型时模板参数中还需要为没有确定类型的参数保留形参对于原模版来说参数T1已经确定了类型为int而参数T2还没有确定类型所以需要在模板参数中保留参数T2。templateclassT2//特化的类模板classAint,T2{private:T1 _a1;T2 _a2;};指定参数的部分特性指定参数的部分特性本质上提前特化出参数为指针或者引用类型的参数当对类A实例化且参数为两个指针时以Aint*, int* a1;为例类模板的就会实例化这个版本的特化。这种偏特化的方式需要注意的是此时T1和T2的类型为int而不是int*。templateclassT1,classT2//特化的类模板classAT1*,T2*{private:T1 _a1;T2 _a2;};2.3 类模板特化的应用-仿函数的改造在priority_queue的实现中需要使用默认的仿函数templateclass T class Less;来控制堆为大堆对于当前版本如果堆中的数据是指针类型而此时的需求是要指向的对象最大的指针在堆顶那使用这样的版本是无法得到正确的堆的此时就需要创建类模板Less的偏特化版本。templateclassTclassLess// 原类模板/仿函数{public:booloperator()(constTa,constTb){returnab;}}对于目标的偏特化版本我们需要到达的效果是只要实例化时实参是指针类型就实例化这个偏特化版本所以我们就可以通过指定模板参数的特性来实现这个偏特化。类模板的偏特化的代码如下templateclassTclassLessT*// 类模板的偏特化{public:booloperator()(constT*a,constT*b){return*a*b;}};三. 模板声明和定义不可分离原因模板声明和定义不可分离原因在前面STL的学习中我们提到了模板的声明和定义不可以存在于不同的文件究其原因主要还是因为模板没有实例化导致的问题。首先我们要知道模板的本质其实就是”一个装着代码模板的容器“。模板是按需求实例化的且模板只有被实例化之后才是真正意义上类或函数。假设此时一个函数模板的声明和定义分离声明放在my_template.h的文件中定义放在my_template.cpp的文件中如果main.cpp文件中使用了该模板实例化对象当程序运行时程序在编译器的控制下会发生如下变化程序运行时首先是预处理阶段预处理阶段会将main.cpp中包含的头文件my_template.h展开此时main.cpp文件中就会有模板的声明。预处理完之后编译器会分别对my_template.cpp和main.cpp文件进行编译在编译main.cpp文件时由于文件中只有模板函数的声明而没有定义此时编译器会留下占位符等链接时寻找模板函数的地址在编译模板的实现文件my_template.cpp时文件中没有对模板实例化的指令所以编译阶段没有产生模板的实例化的目标代码链接时main.cpp没有找到模板函数的地址此时链接就会报错问题场景定义和声明分离my_template.h头文件只放声明// my_template.htemplatetypenameTvoidmyFunction(T x);// 声明没有实现my_template.cpp源文件放定义// my_template.cpp#includemy_template.htemplatetypenameTvoidmyFunction(T x){/* 实现 */}// 定义main.cpp使用文件调用模板// main.cpp#includemy_template.hintmain(){myFunction(42);// 尝试实例化 myFunctionintreturn0;}编译和链接过程发生了什么步骤发生了什么为什么出错1. 编译main.cpp- 编译器看到myFunction(42)知道需要实例化myFunctionint。 -但它只能看到声明my_template.h看不到定义my_template.cpp中的实现。 - 因此编译器无法生成myFunctionint的目标代码因为不知道如何实现。 -结果main.o中只留下一个“占位符”符号引用等待链接器填充地址。编译器需要模板的完整定义才能实例化。声明只告诉编译器“这个函数存在”但无法生成具体代码。2. 编译my_template.cpp- 编译器处理此文件时没有遇到任何实例化请求文件内没有调用myFunction。 - 因此它不会为任何类型生成目标代码模板定义只是“闲置的蓝图”。 -结果my_template.o中完全没有myFunction的代码。模板实例化是“按需触发”的。如果定义文件中没有显式调用或显式实例化编译器会忽略它避免生成无用代码。3. 链接阶段- 链接器尝试合并main.o和my_template.o。 -main.o要求提供myFunctionint的地址但my_template.o中不存在这个符号。 -结果链接器报错undefined reference to void myFunctionint(int)。根本原因实例化从未发生。定义文件中没有触发实例化的代码导致目标文件缺失所需符号。结论编译器在定义文件.cpp中无法主动推断需要哪些实例化因此不会生成任何目标代码。解决方案在模板的定义文件(my_template.cpp)中显式实例化不推荐缺点对于其它文件中所有不同的实例化模板的方式都需要显式实例化。例如如果调用了myFunction(int)就需要在定义文件中写函数模板的int类型的显式实例化而如果使用myFunction(double)则需要实例化double类型的函数模板……// 显式实例化告诉编译器“必须为int生成代码”templatevoidmyFunctionint(int);// 显式实例化告诉编译器“必须为int生成代码”在声明和定义都放在头文件(my_template.h)中推荐这种解决方案是最简便的因为只要模板的定义在头文件中调用处在编译时就可以直接对模板进行实例化这样就不会导致模板实例化失败。