【C++】 进阶篇 模板进阶详解

发布时间:2026/10/21 2:17:05
【C++】 进阶篇 模板进阶详解 一、非类型模板参数1.1 非类型模板参数的介绍模板参数分为类型形参与非类型形参类型形参即出现在模板参数列表中跟在class或者typename关键字之后的参数类型名称我们前面使用的所有模板参数都是类型形参而非类型形参则是用一个常量作为类模板/函数模板的一个参数在类模板/函数模板中可将该参数当成常量来使用。1.2 类型模板参数的问题我们以静态数组为例在没有非类型模板参数时我们采用如下方式来定义一个静态数组#defineN10templateclassTclassarr{public://...private:T _a[N];};voidtest1(){arrintarr;}但是这样有一个缺陷因为 N 的大小是固定的所以我们只能定义出 N 个元素大小的arr对象那么此时如果我们想要同时定义两个大小为 10 和 100 的数组显然是做不到的。1.3 非类型模板参数解决C 中设计了非类型模板参数来解决了这个问题如下我们可以通过传递不同的非类型形参来定义不同的类非类型模板参数在函数模板中的使用也是如此templateclassT,size_t Nclassarr{public://...private:T _a[N];};voidtest1(){arrint,10arr1;arrdouble,100arr2;}注意事项非类型模板参数也可以给定缺省值非类型模板参数只适用于整型这里的整型是一种广义的整型不仅指int、long等整数类型。还有charchar是字符没错但是它的本质还是整型此外bool类型也算整型家族的。不适用于浮点数、类对象以及字符串错误示例templateclass T, double PI class Circle {};浮点数 PI 非法非类型的模板参数必须在编译期间就能确认结果即非类型模板参数的实参只能是常量。错误示例int n 5; Arrayint, n arr;n 是运行时变量非法这个我后面会讲核心优势性能优化数组大小、缓冲区容量等参数在编译期确定避免动态内存分配如vector的扩容开销类型安全编译期校验常量合法性如索引越界、参数类型错误提前暴露问题灵活性同一模板可通过不同常量参数生成不同配置的版本如Arrayint,5和Arrayint,10是两个独立类型。1.4 非类型模板参数 vs 宏templatesize_t N10classarray{private:int_array[N];};#defineN10classarray{private:T _array[N];};对比以上这两种代码VS 不支持变长数组但是宏定义的 N 在预处理的时候会宏替换掉_array的 N非类型模板参数的 N 也是在编译期就明确成了常量。但是相比宏非类型模板参数的优势在于它的可控性。代码如下intmain(){array10arr1;array5arr2;return0;}宏一旦定义就不能修改如果我要一个 5 个单位大小的数组一个 10 个单位大小的数组就必须开辟两个 10 个单位大小的数组。这样就产生了浪费但是现在我们可以通过传递模板参数来控制我想要多大的数组。1.5 C11 中的 array 类一个静态数组这个在 C11 后才支持。迭代器是随机迭代器原生指针C11 的目的是代替静态数组。需要头文件array。使用方式和对比如下arrayint,10a1;intarray[10];array在越界检查的时候还是不错的编译器有时候查不出数组的越界。越界刚好在标志位进行写的操作的时候会检查标志位数组会在数组的末尾加入一到三个标志位这个标志位默认为 -1如果这个值被改变了就会被检查出来。voidtest1(){arrayint,10a1;intarray[10];//array[11] 1;//array[12] 2;//array[13] 3;coutarray[11] array[12] array[13]endl;couta1[11]endl;}总结越界读取不检查越界写要检查。标志位不能整太多。因为占用空间。但是array在越界读取和写的时候都可以检查因为它在内部的operator[]处做了强制检查。但是实用性上来讲vector更好。不过话又说回来了有一点array好就是array的数据在栈上的vector在堆上的栈上开辟空间比在堆上开辟空间效率要高。二、模板的特化2.1 模板特化的介绍通常情况下使用模板可以实现一些与类型无关的代码但对于一些特殊类型的可能会得到一些错误的结果需要特殊处理比如实现了一个专门用来进行小于比较的函数模板。// 自定义日期类classDate{public:Date(intyear,intmonth,intday):_year(year),_month(month),_day(day){}booloperator(constDateother)const{return_yearother._year||(_yearother._year_monthother._month)||(_yearother._year_monthother._month_dayother._day);}private:int_year,_month,_day;};// 函数模板 -- 参数匹配templateclassTboolLess(T left,T right){returnleftright;}voidtest2(){coutLess(1,2)endl;// 可以比较结果正确Dated1(2022,7,7);Dated2(2022,7,8);coutLess(d1,d2)endl;// 可以比较结果正确Date*p1d1;Date*p2d2;coutLess(p1,p2)endl;// 可以比较结果错误}可以看到Less绝对多数情况下都可以正常比较但是在特殊场景下就得到错误的结果上述示例中p1 指向的 d1 显然小于 p2 指向的 d2 对象但是Less内部并没有比较 p1 和 p2 指向的对象内容而比较的是 p1 和 p2 指针的地址这就无法达到预期而发生错误。此时就需要对模板进行特化 – 即在原模板类的基础上针对特殊类型进行特殊化的处理模板特化中分为函数模板特化与类模板特化。2.2 函数模板的特化必须要先有一个基础的函数模板关键字template后面接一对空的尖括号函数名后跟一对尖括号尖括号中指定需要特化的类型函数形参表必须要和模板函数的基础参数类型完全相同如果不同编译器可能会报一些奇怪的错误templateboolLessFuncDate*(Date*left,Date*right){return*left*right;}注意一般情况下如果函数模板遇到不能处理或者处理有误的类型为了实现简单通常都是将该函数直接给出。此时如果现成的函数和函数模板特化同时存在编译器会调用现成的函数。2.3 函数模板特化中的潜在问题函数模板在一些情况下问题很多不建议初学者过多使用。以下讲解两种常见的坑。如下是一个函数模板参数是const引用类型。如果我们要把T换成指针类型该怎么做templateclassTboolLess(constTleft,constTright){returnleftright;}//1templateboolLessDate*(constDate*left,constDate*right){return*left*right;}//2templateboolLessDate*(Date*constleft,Date*constright){return*left*right;}以上给出了两种写法哪个对一般的人们一定会选择第一种直接把Date*插入到T的位置但是由于const的原因这样做不对。原先模板的const修饰的是left这个引用但是加上Date*按照方案 1const就转而去修饰*left。所以方案 2 才是正确的。一定还是要记住一个问题在*之前修饰指向内容在*之后修饰指针本身。intmain(){constDate*p3d1;constDate*p4d2;coutLessFunc(p3,p4)endl;return0;}如果现在只有一个对应的函数模板和一个Date*的模板特化因为直接传递给非const的函数特化会导致权限放大这种就不会去匹配特化只会去匹配原模板。但是模板匹配比较严谨即使是权限缩小比如将Date*传递给const Date*的模板特化的参数也不行。必须完全匹配。三、类模板的特化3.1 全特化与偏特化全特化即是将模板参数列表中所有的参数都确定化。偏特化就是任何针对模板参数进一步进行条件限制设计的特化版本。不是所有的参数举例如下templateclassT1,classT2classData{public:Data(){coutDataT1, T2endl;}private:T1 _d1;T2 _d2;};// 全特化templateclassDataint,char{public:Data(){coutDataint, charendl;}};// 偏特化/半特化templateclassT1classDataT1,double{public:Data(){coutDataT1, doubleendl;}};偏特化有以下两种表现方式部分特化将模板参数列表中的一部分参数特化参数更进一步的限制偏特化并不仅仅是指特化部分参数而是针对模板参数更进一步的条件限制所设计出来的一个特化版本指针特别限定引用特别限定混合限定// 部分偏特化templateclassT1classDataT1,double{public:Data(){coutDataT1, doubleendl;}};//两个参数偏特化为指针类型templatetypenameT1,typenameT2classDataT1*,T2*{public:Data(){coutDataT1*, T2*endl;}private:T1 _d1;T2 _d2;};//两个参数偏特化为引用类型templatetypenameT1,typenameT2classDataT1,T2{public:Data(constT1d1,constT2d2):_d1(d1),_d2(d2){coutDataT1, T2endl;}private:constT1_d1;constT2_d2;};3.2 调用和传递的问题同时有一个全特化和偏特化的哪个更匹配调用哪个哪个具体参数对齐更多的就调用哪个。templateclassT1classDataT1,double{public:Data(){coutDataT1, doubleendl;}};templateclassDatadouble,double{public:Data(){coutDatadouble, doubleendl;}};3.3 函数模板不能偏特化函数模板不允许部分特化只允许全特化。为什么类模板可以部分特化而函数模板不行因为函数有重载机制。对于函数模板语言设计者认为函数的部分特化可以用函数重载来等价表达而类模板没有重载机制所以才允许部分特化。四、模板的分离编译4.1 案例分析为什么不能分离阅读我博客的同学会发现自从学习了模板以后凡是要用到模板的类我们成员函数的声明和定义都是放在一起的或者是直接在类中给出函数的定义而不提供函数的声明比如我们模拟实现的vector、list、stack、queue、priority_queue等容器那为什么我们不像 C 语言或者非模板类那样将类成员函数的声明和定义进行分离呢函数声明和定义分离不是既能够方便别人阅读我们的代码还能够保护源码不被泄露吗-- 这是因为模板不支持分离编译。我们以下面这个为例// a.h 内容templateclassTTAdd(constT,constT);// a.cpp 内容templateclassTTAdd(constTleft,constTright){returnleftright;}// main.cpp 原有内容intmain(){Add(1,2);Add(1.0,2.0);}如上我们将模板类进行声明和定义分离注意1、类模板的外部成员定义不得具有默认参数即类模板声明与定义分离时不能成员函数不能使用缺省参数2、类模板的成员函数在分离定义时必须指明该函数是属于那个类的而stack是类名stackT才是类型所以我们需要在每个成员函数前面指明类类型stackT。但是当我们编译运行的时候我们发现分离定义的所有成员函数都出现了链接性错误。造成这种错误的原因如下一个 .c/.cpp 程序要变成 .exe 可执行程序一共要经历四个步骤分别是预处理、编译、汇编和链接这期间它们的工作分别为预处理注释的删除#define 定义的符号、宏的替换以及删除各种条件编译的处理头文件的展开编译进行语法分析、词法分析、语义分析和符号汇总汇编生成符号表链接合并段表、符号表的合并和重定位。4.2 细谈1. 编译 main.cpp预处理后它只看到templateclassTTAdd(constT,constT);当编译器看到Add(1,2);// 推导 T intAdd(1.0,2.0);// 推导 T double它必须做两件事模板参数推导模板实例化生成Addint和Adddouble的函数代码但是它此时只有声明没有定义。根据标准规则模板在被实例化时必须在当前翻译单元内可见完整定义。由于看不到函数体编译器无法实例化因而不会生成Addint或Adddouble的代码main.o 中只会留下 “需要这个符号” 的引用2. 编译 a.cpp这个翻译单元里templateclassTTAdd(constT,constT){returnleftright;}但这里没有任何地方使用它因为是模板也没有函数名修饰规则没有用到就不会实例化就不会生成任何具体函数.o 文件中不会产生Addint或Adddouble符号链接阶段链接器现在面对main.o需要Addint和Adddoublea.o根本没有这些符号于是报unresolved external symbol4.3 本质问题模板和普通函数的核心区别普通函数编译时就生成代码.o 一定有符号模板函数只有在实例化时才生成代码实例化发生在编译阶段而不是链接阶段链接器不会实例化模板它只解析符号地址4.4 解决办法1. 放头文件就行因为main.cpp 在编译时能看到模板定义能直接实例化生成Addint/Adddouble代码直接进 main.o链接自然成功。以后我们将模板函数的声明和定义放到同一个文件 “xxx.hpp” 中hpp.h .cpp。这种方式适用于类较大时方便别人快速了解我们的类模板实例化发生在编译阶段而模板定义不在当前翻译单元导致无法生成具体函数代码最终链接阶段找不到符号。模板定义的位置显式实例化。如下。这种方法不实用以后要是还需要别的还要继续写所以不推荐使用。// a.cpptemplateclassTTAdd(constTleft,constTright){returnleftright;}// 显式实例化templateintAddint(constint,constint);templatedoubleAdddouble(constdouble,constdouble);五、模板总结【优点】模板复用了代码节省资源更快的迭代开发C 的标准模板库STL因此而产生。增强了代码的灵活性。【缺陷】模板会导致代码膨胀问题也会导致编译时间变长。出现模板编译错误时错误信息非常凌乱不易定位错误。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询