【C++模板进阶】C++ 模板进阶的拦路虎:模板特化和分离编译,该如何逐个突破?
前言:在之前的文章中,我们介绍了模板的基础知识,包括函数模板和类模板的使用方法。本文将深入探讨模板的进阶内容,涵盖非类型模板参数、模板特化以及模板的分离编译等高级特性。
🌟 专注用图文结合拆解难点+代码落地知识,让技术学习从「难懂」变“一看就会”!
🏠 个人主页 :MSTcheng · ZEEKLOG
💻 代码仓库 :MSTcheng · Gitee📚 精选专栏 :📖 :《C语言》🧩 :《数据结构》💡 :《C++由浅入深》💬 座右铭 :“路虽远行则将至,事虽难做则必成!”
文章目录
一、非类型模板参数
1.1模板参数的分类
首先要知道模板参数分为类型模板参数和非类型模板参数,在前面的文章中我们介绍了类型模板参数 例如:
template<classT>classDate{public:voidprint(){ cout << _year <<"-"<< _month <<"-"<< _day;}private: T _year=2025; T _month=11; T _day=20;};intmain(){ Date<int> d; d.print();return0;}类型形参即:出现在模板参数列表中,跟在class或者typename之类的参数类型名称。
🔺T类型的私有成员在该日期类实例化对象的时候就实例化出了具体类型,比如上面示例中的int。所以类型模板参数是在实例化的时候才确定类型的。
1.2非类型模板参数的认识
那什么是非类型模板参数呢?🤔
非类型模板参数(Non-Type Template Parameters)是C++模板中允许使用的非类型值作为参数,即在编译时确定的常量表达式。与类型模板参数(如typename T)不同,非类型模板参数可以是整型、枚举、指针、引用或std::nullptr_t等具体值。
下面举个例子:
#defineN10//静态的栈template<classT>classstack{public:stack():_a(nullptr),_top(0),_capacity(0){}private: T* _a[N];int _top;int _capacity;};intmain(){ stack<int> s1;//可以控制N为10 创建一个空间为10的静态栈 stack<int> s2;//也可以控制N为100创建一个空间为100的静态栈return0;}我们可以通过宏定义来控制静态栈的空间大小,但这种方法存在明显局限——所有栈实例只能使用相同的预设大小,无法实现不同栈拥有不同容量(例如一个栈10个元素,另一个栈100个元素这时小的那个栈就会浪费90个空间)。为解决这个问题,可以通过引入非类型模板参数来灵活控制各个栈的独立容量。
//使用整型做非模板参数//非模板参数:使用一个正数说常量作为类模板的一个参数 //使用整型来做类模板的参数 这里的N为10是缺省值 传了值就用传的那个值 没传就用缺省值 template<classT=int, size_t N =10>classstack{private: T* _a[N];int _top;int _capacity;};intmain(){ stack<int> s1;//10个空间 stack<int,100> s2;//100个空间 stack<int,1000> s3;//1000个空间 cout <<sizeof(s1)<< endl; cout <<sizeof(s2)<< endl; cout <<sizeof(s3)<< endl;return0;}这段代码利用非类型模板参数N来动态调整栈空间大小。 相比宏定义,模板参数提供了更大的灵活性,使每个栈实例都能拥有独立的存储空间。这种设计既确保了空间分配的确定性,又有效避免了资源浪费问题。
🔺注意:
1. 浮点数、类对象以及字符串是不允许作为非类型模板参数的。
2. 非类型的模板参数必须在编译期就能确认结果。
1.3array容器
在我们之前学过的容器中,如string、vector、stack和queue,都很少使用非类型模板参数。而array容器则是个例外,它采用了非类型模板参数。既然提到了这个概念,我们不妨来了解一下它。


该容器提供的接口与vector类似,迭代器也采用原生指针实现,因此不再赘述接口的使用方法。我们将重点探讨array与传统C语言数组的主要区别:
//---------------------静态数组 与array的区别#include<array>intmain(){//array容器 array<int,10> a1;//静态数组int a2[10]={0};//静态数组的越界访问 a2[0]=2; a2[9]=100;//访问这两个越界的地方没有报错 说明静态数组越界访问不会报错 cout << a2[10]<< endl; cout << a2[15]<< endl;//将越界处的数据修改会报错 说明静态数组越界 写(修改)会报错检查不出来//a2[11] = 23;//a2[15] = 24;for(auto e : a2){ cout << e <<" ";} cout << endl;//越界写入数据 会报错 a1[10]=2; cout << a1[10]<< endl;//越界访问读取数据 也会报错 由此得出array比静态数组 检查的更严格 静态数组只是抽查 cout << a1[15]<< endl;return0;}🔺需要注意的是,array之所以能够检测数组越界读写,关键在于其重载的operator[]函数内部实现了对传入下标的边界检查机制。
二、模板的特化
2.1什么是特化?
模板特化是C++中针对泛型编程的一种机制,允许为特定类型或条件提供定制化的模板实现。当通用模板无法满足某些特殊类型的需求时,可以通过特化来优化或改变其行为。相当于是模板的特殊处理。
2.2什么场景下使用特化?
下面请看例子:
template<classT>boolLess(const T& left,const T& right){return left < right;}intmain(){int a =1, b =2; cout <<Less(a,b)<<" "<<endl; cout <<Less(2,3)<< endl;int* pa =&a;int* pb =&b; cout <<Less(pb, pa)<< endl;double* p1 =newdouble(2.2);double* p2 =newdouble(1.1); cout <<Less(p1, p2)<< endl;//这里明明p1指向的内容比p2大 //传值过去判断应该为假才对 但是输出结果是1//所以就要对函数模板进行特殊化 即生成一种专门应对这种情况的模板 string* p3 =newstring("111"); string* p4 =newstring("222"); cout <<Less(p3, p4)<< endl;//把p3和p4传递过去后 模板就实例化了一个string*的类型//而left和right拿到的是p3和p4的地址 所以比较的就是地址 而不是内容return0;}🤨从上述代码可见,Less函数模板虽然能处理大多数情况,但在比较指针类型时存在局限:它比较的是指针本身而非指针指向的内容。 因此,我们需要对模板进行特化处理来解决这个问题。
对于上面函数模板的特化版本如下:
//特化template<>//template<>表示这是一个显式特化bool Less<double*>(double*const& left,double*const& right){return*left <*right;}注意:
🔺特化的模板需要显示实例化例如:Less<double*>表明对模板类或模板函数Less进行特化,特化的类型是double*(即指向double的指针)。
🔺特化后的模板参数不能写成const double* & ! 因为主模板是const T&修饰的是形参本身不能被修改,如果T=double*就边成const double* &修饰的是指针指向的内部不能被修改而不是指针本身不能被修改!所以特化模板和主模板参数不匹配!会报错!
📢最初的模板的参数一定要与特化后函数的参数对应匹配 !!!

const T* p1 ——>const在*的左边修饰指向的内容 内容不能改 *p1(解引用)不能修改 T const* p2——>const在*的左边修饰指向的内容 内容不能改 *p2(解引用)不能修改 *const p3 ——>const在*的右边修饰指针本身 本身的指向不能改 p3(指针本身)不能修改 如果涉及较多的指针内容的比较我们也可写成一个通过模板——>前提:要有主模板
template<classT>boolLess( T*const& left, T*const& right){return*left <*right;}2.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;}private:int _d1;char _d2;};intmain(){ Data<int,char> d1;//调用特化的模板 Data<int,int>d2;//调用主模板 cout <<typeid(d1).name()<< endl; cout <<typeid(d2).name()<< endl;return0;}
2、偏特化
偏特化👉就是指定部分参数。
//------------------------------偏特化-------------------------------template<classT1,classT2>classData{public:Data(){ cout <<"Data<T1,T2>"<< endl;}voidf1(){};};// 偏特化// 特化部分参数template<classT1>classData<T1,char>{public:Data(){ cout <<"Data<T1, char>"<< endl;}voidf1(){};};// 对参数进一步限制template<classT1,classT2>classData<T1*, T2*>{public:Data(){ cout <<"Data<T1*, T2*>"<< endl;}voidf1(){ T1 x1; cout <<typeid(x1).name()<< endl; T1* x2; cout <<typeid(x2).name()<< endl;}};intmain(){ Data<int,int> d1;//调用主模板 d1.f1(); Data<int,char> d2;//调用偏特化模板 d2.f1(); Data<char,char> d3;//调用偏特化模板 Data<char*,char*> d4; Data<int*,char*> d5; Data<double*,double*> d6; d4.f1(); Data<double&,double&> d7; Data<double*,double&> d8;return0;}
这里可能会有人有疑问:d4中使用一个T定义了一个x1,使用T*定义了一个x2使用typeid打印出来的为什么一个是char一个是char*?
因为T1 x1声明了一个非指针变量,其类型为T1(即指针T1*所指向的底层类型)。T1* x2声明了一个指针变量,其类型为T1*(即原始的模板参数类型)。
举个例子:
若实例化Data<int*, double*>:
T1被推导为int,T2被推导为double。T1 x1;中的x1类型为int。T1* x2;中的x2类型为int*。
三、模板的分离编译
3.1什么是模板的分离编译?
模板分离编译👉指将模板的声明和实现分别放在不同的文件中(通常是头文件.h和源文件.cpp),类似于普通函数的声明与实现分离。这种设计初衷是为了提高代码的可维护性和编译效率。
3.2模板的分离编译
假设有下面两个函数,他们的声明和定义均分别放在.h文件(声明)和.cpp文件(定义)中:
<在Func.h文件中>#include<iostream>usingnamespace std;//模板函数的声明template<classT>voidFuncT(const T& x);//普通函数的声明voidFuncF();<在Func.cpp文件中>#include"Func.h"//模板函数的定义template<classT>voidFuncT(const T& x){ cout <<"模板函数:FuncT(const T& x)"<< endl;}//普通函数的定义voidFuncF(){ cout <<"普通函数:FuncF()"<< endl;}<在test.cpp文件中调用这两个函数>#include"Func.h"intmain(){//函数模板调用FuncT(1);//call FuncT(?)找不到FuncT的地址//普通函数调用FuncF();//call FuncF(普通函数的地址)return0;}

为什么会报链接错误呢?这也是我们在前面STL各种容器的模拟实现中强调类模板声明和定义最好不要分离(分文件)的原因!这其实是没有实例化造成的,下面画个图让大家更加直观的理解:

3.3链接错误的解决方案
上面我们已经分析了,编译器报链接错误是由于函数模板没有实例化造成的,那么要解决该问题我们就要从实例化入手!
方法一:在func.cpp文件中显示实例化
//模板函数的定义template<classT>voidFuncT(const T& x){ cout <<"模板函数:FuncT(const T& x)"<< endl;}//显示实例化template<>voidFuncT(constint& x){ cout <<"模板函数:FuncT(const T& x)"<< endl;}这种实例化方式有较为明显的局限性——>每实例化一种类型就要人为的去显示实例化,既增加了工作量也增加了代码的冗余度所以这种方法并不推荐。
方法二:将声明和定义放在同一个文件例如“xxx.h”文件或"xxx.hpp"文件
<在.h文件中>template<classT>voidFuncT(const T& x){ cout <<"void FuncT(const T& x)"<< endl;}//类模板也类似template<classT>classStack{public://类里面定义voidPush(const T& x);};//类外面定义template<classT>voidStack<T>::Push(const T& x){ cout <<"void Push(const T& x)"<< endl;}这种方法明显优于第一种既减少了工作量,也减少了代码冗余度。推荐使用这种方法!
四、模板总结
💡【优点】
- 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生。
- 增强了代码的灵活性。
💡【缺陷】
- 模板会导致代码膨胀问题,也会导致编译时间变长。
- 出现模板编译错误时,错误信息非常凌乱,不易定位错误。