☰
【C++】 进阶篇 模板进阶详解
2026/10/21 2:17:04 网站建设 项目流程

一、非类型模板参数

1.1 非类型模板参数的介绍

模板参数分为类型形参与非类型形参,类型形参即出现在模板参数列表中,跟在class或者typename关键字之后的参数类型名称,我们前面使用的所有模板参数都是类型形参;而非类型形参则是用一个常量作为类模板/函数模板的一个参数,在类模板/函数模板中可将该参数当成常量来使用。

1.2 类型模板参数的问题

我们以静态数组为例;在没有非类型模板参数时,我们采用如下方式来定义一个静态数组:

#defineN10template<classT>classarr{public://...private:T _a[N];};voidtest1(){arr<int>arr;}

但是这样有一个缺陷,因为 N 的大小是固定的,所以我们只能定义出 N 个元素大小的arr对象,那么此时如果我们想要同时定义两个大小为 10 和 100 的数组显然是做不到的。

1.3 非类型模板参数解决

C++ 中设计了非类型模板参数来解决了这个问题,如下,我们可以通过传递不同的非类型形参来定义不同的类,非类型模板参数在函数模板中的使用也是如此:

template<classT,size_t N>classarr{public://...private:T _a[N];};voidtest1(){arr<int,10>arr1;arr<double,100>arr2;}

注意事项:

  • 非类型模板参数也可以给定缺省值;
  • 非类型模板参数只适用于整型(这里的整型是一种广义的整型,不仅指int、long等整数类型。还有char,char是字符没错,但是它的本质还是整型,此外bool类型也算整型家族的。),不适用于浮点数、类对象以及字符串;(错误示例:template<class T, double PI> class Circle {};(浮点数 PI 非法))
  • 非类型的模板参数必须在编译期间就能确认结果,即非类型模板参数的实参只能是常量。(错误示例:int n = 5; Array<int, n> arr;(n 是运行时变量,非法),这个我后面会讲)

核心优势

  • 性能优化:数组大小、缓冲区容量等参数在编译期确定,避免动态内存分配(如vector的扩容开销);
  • 类型安全:编译期校验常量合法性(如索引越界、参数类型错误),提前暴露问题;
  • 灵活性:同一模板可通过不同常量参数生成不同配置的版本(如Array<int,5>和Array<int,10>是两个独立类型)。

1.4 非类型模板参数 vs 宏

template<size_t N=10>classarray{private:int_array[N];};#defineN10classarray{private:T _array[N];};

对比以上这两种代码,VS 不支持变长数组,但是宏定义的 N 在预处理的时候会宏替换掉_array的 N,非类型模板参数的 N 也是在编译期就明确成了常量。

但是相比宏,非类型模板参数的优势在于它的可控性。代码如下:

intmain(){array<10>arr1;array<5>arr2;return0;}

宏一旦定义就不能修改,如果我要一个 5 个单位大小的数组,一个 10 个单位大小的数组,就必须开辟两个 10 个单位大小的数组。这样就产生了浪费,但是现在我们可以通过传递模板参数来控制我想要多大的数组。

1.5 C++11 中的 array 类

一个静态数组,这个在 C++11 后才支持。迭代器是随机迭代器(原生指针),C++11 的目的是代替静态数组。需要头文件<array>。

使用方式和对比如下:

array<int,10>a1;intarray[10];

array在越界检查的时候还是不错的:编译器有时候查不出数组的越界。越界刚好在标志位进行写的操作的时候会检查,

标志位:数组会在数组的末尾加入一到三个标志位,这个标志位默认为 -1,如果这个值被改变了就会被检查出来。

voidtest1(){array<int,10>a1;intarray[10];//array[11] = 1;//array[12] = 2;//array[13] = 3;cout<<array[11]<<" "<<array[12]<<" "<<array[13]<<endl;cout<<a1[11]<<endl;}

总结,越界读取不检查,越界写要检查。标志位不能整太多。因为占用空间。但是array在越界读取和写的时候都可以检查,因为它在内部的operator[]处做了强制检查。

但是实用性上来讲,vector更好。不过话又说回来了:有一点array好,就是array的数据在栈上的,vector在堆上的,栈上开辟空间比在堆上开辟空间效率要高。

二、模板的特化

2.1 模板特化的介绍

通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理,比如:实现了一个专门用来进行小于比较的函数模板。

// 自定义日期类classDate{public:Date(intyear,intmonth,intday):_year(year),_month(month),_day(day){}booloperator<(constDate&other)const{return_year<other._year||(_year==other._year&&_month<other._month)||(_year==other._year&&_month==other._month&&_day<other._day);}private:int_year,_month,_day;};// 函数模板 -- 参数匹配template<classT>boolLess(T left,T right){returnleft<right;}voidtest2(){cout<<Less(1,2)<<endl;// 可以比较,结果正确Dated1(2022,7,7);Dated2(2022,7,8);cout<<Less(d1,d2)<<endl;// 可以比较,结果正确Date*p1=&d1;Date*p2=&d2;cout<<Less(p1,p2)<<endl;// 可以比较,结果错误}

可以看到,Less绝对多数情况下都可以正常比较,但是在特殊场景下就得到错误的结果;上述示例中,p1 指向的 d1 显然小于 p2 指向的 d2 对象,但是Less内部并没有比较 p1 和 p2 指向的对象内容,而比较的是 p1 和 p2 指针的地址,这就无法达到预期而发生错误。

此时,就需要对模板进行特化 – 即在原模板类的基础上,针对特殊类型进行特殊化的处理;模板特化中分为函数模板特化与类模板特化。

2.2 函数模板的特化

  1. 必须要先有一个基础的函数模板
  2. 关键字template后面接一对空的尖括号<>
  3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型
  4. 函数形参表:必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误
template<>boolLessFunc<Date*>(Date*left,Date*right){return*left<*right;}

注意:一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出。此时如果现成的函数和函数模板特化同时存在,编译器会调用现成的函数。

2.3 函数模板特化中的潜在问题

函数模板在一些情况下问题很多,不建议初学者过多使用。以下讲解两种常见的坑。如下是一个函数模板,参数是const引用类型。如果我们要把T换成指针类型该怎么做?

template<classT>boolLess(constT&left,constT&right){returnleft<right;}//1template<>boolLess<Date*>(constDate*&left,constDate*&right){return*left<*right;}//2template<>boolLess<Date*>(Date*const&left,Date*const&right){return*left<*right;}

以上给出了两种写法,哪个对?一般的人们一定会选择第一种,直接把Date*插入到T的位置,但是由于const的原因,这样做不对。原先模板的const修饰的是left这个引用,但是加上Date*,按照方案 1,const就转而去修饰*left。所以方案 2 才是正确的。

一定还是要记住一个问题:在*之前修饰指向内容,在*之后修饰指针本身。

intmain(){constDate*p3=&d1;constDate*p4=&d2;cout<<LessFunc(p3,p4)<<endl;return0;}

如果现在只有一个对应的函数模板和一个Date*的模板特化,因为直接传递给非const的函数特化会导致权限放大,这种就不会去匹配特化,只会去匹配原模板。

但是,模板匹配比较严谨,即使是权限缩小,比如将Date*传递给const Date*的模板特化的参数,也不行。必须完全匹配。

三、类模板的特化

3.1 全特化与偏特化

全特化即是将模板参数列表中所有的参数都确定化。偏特化就是任何针对模板参数进一步进行条件限制设计的特化版本。(不是所有的参数)

举例如下:

template<classT1,classT2>classData{public:Data(){cout<<"Data<T1, T2>"<<endl;}private:T1 _d1;T2 _d2;};// 全特化template<>classData<int,char>{public:Data(){cout<<"Data<int, char>"<<endl;}};// 偏特化/半特化template<classT1>classData<T1,double>{public:Data(){cout<<"Data<T1, double>"<<endl;}};

偏特化有以下两种表现方式:

  • 部分特化:将模板参数列表中的一部分参数特化
  • 参数更进一步的限制:偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本(指针特别限定,引用特别限定,混合限定)
// 部分偏特化template<classT1>classData<T1,double>{public:Data(){cout<<"Data<T1, double>"<<endl;}};//两个参数偏特化为指针类型template<typenameT1,typenameT2>classData<T1*,T2*>{public:Data(){cout<<"Data<T1*, T2*>"<<endl;}private:T1 _d1;T2 _d2;};//两个参数偏特化为引用类型template<typenameT1,typenameT2>classData<T1&,T2&>{public:Data(constT1&d1,constT2&d2):_d1(d1),_d2(d2){cout<<"Data<T1&, T2&>"<<endl;}private:constT1&_d1;constT2&_d2;};

3.2 调用和传递的问题

同时有一个全特化和偏特化的,哪个更匹配调用哪个,哪个具体参数对齐更多的就调用哪个。

template<classT1>classData<T1,double>{public:Data(){cout<<"Data<T1, double>"<<endl;}};template<>classData<double,double>{public:Data(){cout<<"Data<double, double>"<<endl;}};

3.3 函数模板不能偏特化

函数模板不允许部分特化,只允许全特化。

为什么类模板可以部分特化,而函数模板不行?因为函数有重载机制。对于函数模板,语言设计者认为:函数的部分特化可以用函数重载来等价表达,而类模板没有重载机制,所以才允许部分特化。

四、模板的分离编译

4.1 案例分析为什么不能分离

阅读我博客的同学会发现,自从学习了模板以后,凡是要用到模板的类我们成员函数的声明和定义都是放在一起的,或者是直接在类中给出函数的定义,而不提供函数的声明,比如我们模拟实现的vector、list、stack、queue、priority_queue等容器;

那为什么我们不像 C 语言或者非模板类那样将类成员函数的声明和定义进行分离呢?函数声明和定义分离不是既能够方便别人阅读我们的代码,还能够保护源码不被泄露吗?-- 这是因为模板不支持分离编译。

我们以下面这个为例:

// a.h 内容template<classT>TAdd(constT&,constT&);// a.cpp 内容template<classT>TAdd(constT&left,constT&right){returnleft+right;}// main.cpp 原有内容intmain(){Add(1,2);Add(1.0,2.0);}

如上,我们将模板类进行声明和定义分离,注意:

1、类模板的外部成员定义不得具有默认参数,即类模板声明与定义分离时不能成员函数不能使用缺省参数;
2、类模板的成员函数在分离定义时必须指明该函数是属于那个类的,而stack是类名,stack<T>才是类型,所以我们需要在每个成员函数前面指明类类型stack<T>。

但是当我们编译运行的时候我们发现分离定义的所有成员函数都出现了链接性错误。造成这种错误的原因如下:

一个 .c/.cpp 程序要变成 .exe 可执行程序一共要经历四个步骤,分别是预处理、编译、汇编和链接,这期间它们的工作分别为:

  • 预处理:注释的删除,#define 定义的符号、宏的替换以及删除,各种条件编译的处理,头文件的展开;
  • 编译:进行语法分析、词法分析、语义分析和符号汇总;
  • 汇编:生成符号表;
  • 链接:合并段表、符号表的合并和重定位。

4.2 细谈

1. 编译 main.cpp

预处理后它只看到:

template<classT>TAdd(constT&,constT&);

当编译器看到:

Add(1,2);// 推导 T = intAdd(1.0,2.0);// 推导 T = double

它必须做两件事:

  • 模板参数推导
  • 模板实例化(生成Add<int>和Add<double>的函数代码)

但是它此时只有声明,没有定义。根据标准规则:

模板在被实例化时,必须在当前翻译单元内可见完整定义。

由于看不到函数体:

  • 编译器无法实例化
  • 因而不会生成Add<int>或Add<double>的代码
  • main.o 中只会留下 “需要这个符号” 的引用

2. 编译 a.cpp

这个翻译单元里:

template<classT>TAdd(constT&,constT&){returnleft+right;}

但这里没有任何地方使用它,因为是模板也没有函数名修饰规则

  • 没有用到
  • 就不会实例化
  • 就不会生成任何具体函数
  • .o 文件中不会产生Add<int>或Add<double>符号

链接阶段

链接器现在面对:

  • main.o:需要Add<int>和Add<double>
  • a.o:根本没有这些符号
  • 于是报:unresolved external symbol

4.3 本质问题

模板和普通函数的核心区别:

普通函数:编译时就生成代码,.o 一定有符号
模板函数:只有在实例化时才生成代码,实例化发生在编译阶段,而不是链接阶段

链接器不会实例化模板,它只解析符号地址

4.4 解决办法

1. 放头文件就行

因为:main.cpp 在编译时能看到模板定义,能直接实例化,生成Add<int>/Add<double>,代码直接进 main.o,链接自然成功。

以后我们将模板函数的声明和定义放到同一个文件 “xxx.hpp” 中(hpp:.h + .cpp)。(这种方式适用于类较大时,方便别人快速了解我们的类)

模板实例化发生在编译阶段,而模板定义不在当前翻译单元,导致无法生成具体函数代码,最终链接阶段找不到符号。

  1. 模板定义的位置显式实例化。
    如下。这种方法不实用,以后要是还需要别的还要继续写所以,不推荐使用。
// a.cpptemplate<classT>TAdd(constT&left,constT&right){returnleft+right;}// 显式实例化templateintAdd<int>(constint&,constint&);templatedoubleAdd<double>(constdouble&,constdouble&);

五、模板总结

【优点】

  1. 模板复用了代码,节省资源,更快的迭代开发,C++ 的标准模板库(STL)因此而产生。增强了代码的灵活性。

【缺陷】

  1. 模板会导致代码膨胀问题,也会导致编译时间变长。
  2. 出现模板编译错误时,错误信息非常凌乱,不易定位错误。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询