目录
[非类型模板参数的应用 : array](#非类型模板参数的应用 : array)
非类型模板参数
模板参数分为 类型形参 与 非类型形参。
类型形参 :出现在模板参数列表中,跟在class或者typename之类的参数类型名称。
非类型形参:就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。
举个例子,比如我们写个静态栈结构:
cpp
#define N 10
template<class T>//类型模板参数
class Stack
{
private:
T _a[N];
size_t _top;
};
int main()
{
Stack<int> st1;//大小为10
Stack<int> st2;
}
我们想要改栈的大小就改宏就可以了,但是我们有两个栈呢?一个栈的大小想要10,另一个栈想要1000的大小,这样就不能满足多个栈的需求了,除非再定义一个类模板,但是并不推荐这样做,在C++模板当中有一个非类型的模板参数概念:
cpp
template<class T,size_t N>
//T是类型模板参数,N是非类型模板参数,N是一个常量
class Stack
{
private:
T _a[N];
size_t _top;
};
int main()
{
Stack<int,100> st1;//100
Stack<int,20000> st2;//20000
}
我们这样就可以通过传参完成每个栈想要的大小需求了,在template<class T,size_t N>当中,T是类型模板参数,这里的N是非类型模板参数,这里的N是一个常量
cpp
int main()
{
int n;
cin>>n;
Stack<int,n> st;//error,非类型模板参数不能是变量
return 0;
}
需要注意的是 :
- 非类型模板参数不能是变量 , 比如上面的n , 这种写法是错误的
- 浮点数 , 类对象以及字符串是不允许作为非类型模板参数的
- 非类型的模板参数必须在编译器就能确认结果
非类型模板参数的应用 : array

-
观察在array类模板的中同样也使用了非类型模板参数去定义固定大小的数据,这也是非类型模板参数的一个应用
-
array容器所管理的数组是固定大小,所以也就不存在什么扩容插入之类的说法

- array支持了迭代器,operator[]等
下面我们对array进行简单的使用:

可以看出array对其中存储的数据并不会进行初始化
array容器与c语言数组的对比
array容器相对于c语言的数组并没有什么太大的区别,因为c语言数组同样支持它的大部分接口

而这最大的区别就是array容器对于越界的检查更严格了,array可以检查出越界读和越界写,而C语言对数组越界是一种抽查的行为

从上图就可以看出array容器对于越界检查非常严格
模板的特化
概念引入
- 使用模板可以实现一些与类型无关的代码, 但是对于一些特殊类型无法针对性的得出我们想要的结果,这时候就需要进行特殊处理。这里以下面比较小于的函数为例进行讲解

- 当传入 int 类型的 a=1 和 b=0 时, Compare(a, b) 判断 1 < 0 ,结果为 false (输出 0 )。
- 当传入 int* 类型的指针 pa (指向 a )和 pb (指向 b )时, Compare(pa, pb) 实际比较的是指针的地址大小(而非指针指向的值)。由于 pa 的地址大于 pb 的地址,所以 pa < pb 不成立,结果为 true (输出 1 )。
分析 : 这里对于普通类型int的比较结果正确,但是对于int*的指针比较结果错误,这里本意是想要使用传指针特殊化比较指针指向的对象的大小,这里却比较成了指针,即对象的地址,从而导致比较错误,那么这种结果无法达到小编的预期结果,应该如何处理呢?
这时候应该使用模板的特化,即针对特殊类型,对函数(类)模板进行特殊化的实现
模板特化分为函数模板特化和类模板特化
函数模板特化
函数模板特化的注意事项:
- ++必须要先有一个基础的函数模板++,因为特化的函数模板不能脱离基础的函数模板而独立存在
- 必须要使用关键字template并且后面加空的尖括号<>表示这是函数模板的特化
- 函数名后面跟尖括号<>,在尖括号中指定需要特化的类型
- 特化的函数模板的参数列表中的形参的类型必须要跟模板函数的基础参数类型保持一致
- 函数模板的基础参数类型指的是内置类型和自定义类型和引用
那么再对上述代码进行模板特化:

当对 Compare 模板进行 int* 类型的特化后,代码的行为会发生变化,我们结合运行结果来分析:
- 对于 Compare(a, b) :这里 a 和 b 是 int 类型,会调用通用模板 template<typename T> bool Compare(T a, T b) ,比较 a = 1 和 b = 0 的值, 1 < 0 为 false ,所以输出 0 。
- 对于 Compare(pa, pb) :此时由于存在 int* 类型的模板特化 template<> bool Compare<int*>(int* a, int* b) ,会调用这个特化版本 。在特化版本中,是比较指针 pa 和 pb 所指向的值,即 *pa = 1 和 *pb = 0 , 1 < 0 为 false ,所以输出 0 。
与未特化时的区别 :
在未对 int* 类型进行模板特化时,调用 Compare(pa, pb) 会使用通用模板,比较的是指针 pa 和 pb 的地址大小;而特化后,比较的是指针所指向的整数的值,这就是模板特化的作用------为特定类型(这里是 int* )定制模板的行为。
但是 :
- 通常来讲,当遇到函数模板需要进行特化的时候,我们一般直接进行函数重载,函数重载后,编译器在进行调用的时候会优先调用与自身类型最匹配的或者现成已经实例化的函数
- 这种函数重载的方式易于编写,可读性强
cppbool Compare(int* a, int* b) { return *a < *b; }
类模板特化
类模板特化是 C++ 中对类模板的定制化处理,分为全特化和偏特化两种类型,用于为特定类型(或类型组合)提供专属的类定义,以满足特殊场景的需求。
类模板特化的注意事项
- 必须先存在基础类模板,特化的类模板不能脱离基础类模板独立存在。
- 全特化需使用 template<> 语法,偏特化需根据模板参数的约束情况编写模板声明(如 template<typename T> class A<T, int> )。
- 类名后需跟尖括号 <> ,在尖括号中指定特化的类型(全特化指定所有类型,偏特化指定部分类型或添加类型约束)。
- 特化的类模板中,成员变量的类型、成员函数的逻辑等可以根据特化的类型进行定制化修改,与基础类模板的实现可以完全不同。
- 类模板的基础参数类型涵盖内置类型、自定义类型、指针、引用、const 修饰的类型等。
全特化
全特化是++将模板参数列表中的全部参数都确定化++
这是一段C++类模板全特化的代码示例,用于展示类模板针对特定类型( double, double )的定制化实现:
- 通用类模板 A<T1, T2> :
- 定义了一个包含两个模板参数 T1 、 T2 的类,构造函数输出 "A<T1, T2>" ,私有成员为 T1 类型的 _a 和 T2 类型的 _b 。
2. 全特化类 A<double, double> :
- 针对模板参数为 double, double 的情况进行特化,构造函数输出 "A<double, double>" ,私有成员为 double 类型的 _a 和 _b 。
当 main 函数调用:
-
声明 A<int, int> a 时,会调用通用类模板,输出 "A<T1, T2>" ;
-
声明 A<double, double> a1 时,会调用全特化的类模板,输出 "A<double, double>" 。
类模板全特化:当模板参数列表中的所有参数都指定为具体类型时,即为全特化。全特化的类会完全替代通用类模板在该类型组合下的行为。
偏特化
偏特化:任何针对模板参数进行进一步的条件限制设计的特化版本。
偏特化的两种表现形式:
- 部分特化
- 参数更进一步的限制
部分特化
将模板参数列表中的++部分参数进行特化++

这是一段C++类模板偏特化的代码示例,用于展示类模板针对部分类型( T, double )的定制化实现:
- 通用类模板 A<T1, T2> :
- 定义了包含两个模板参数 T1 、 T2 的类,构造函数输出 "A<T1, T2>" ,私有成员为 T1 类型的 _a 和 T2 类型的 _b 。
- 偏特化类 A<T, double> :
- 针对模板参数中第二个参数为 double 的情况进行偏特化,构造函数输出 "A<T, double>" ,私有成员为 T 类型的 _a 和 double 类型的 _b 。
当调用 main 函数调用:
-
声明 A<int, int> a1 时,会调用通用类模板,输出 "A<T1, T2>" ;
-
声明 A<int, double> a2 时,会调用偏特化的类模板,输出 "A<T, double>" 。
类模板偏特化:只对模板参数列表中的部分参数指定具体类型(或添加类型约束),剩余参数仍保持模板形式。偏特化能让类模板对某一类特定的类型组合(如本例中第二个参数为 double 的所有组合)进行定制化处理。
参数更进一步限制(带类型约束)
可以针对模板参数更进一步进行条件限制设计出来的特化版本
- 同时我们进行设计的特化的类模板中的成员没有必要追求和原类模板的成员一样,应该具体根据实际的应用场景去进行设计成员

在部分特化的基础上对参数更进一步限制的特化:
3. 指针约束的偏特化 A<T1*, T2*> :
- 针对两个模板参数均为指针类型的情况进行偏特化,构造函数输出 "A<T1*, T2*>" 。
4. 引用约束的偏特化 A<T1&, T2&> :
- 针对两个模板参数均为引用类型的情况进行偏特化,构造函数输出 "A<T1&, T2&>" 。
当调用 main 函数调用:
-
声明 A<int*, int*> a2 时,会匹配指针约束的偏特化,输出 "A<T1*, T2*>" ;
-
声明 A<int&, int&> a3 时,会匹配引用约束的偏特化,输出 "A<T1&, T2&>" 。
类模板偏特化的类型约束:可以通过在模板参数中添加 * (指针)、 & (引用)、 const 等修饰符,对模板参数的类型范围进行更精确的约束,从而为某一类特定的类型组合(如指针类型组合、引用类型组合)定制类的行为。
类模板特化的应用举例
- 我们编写一个进行比较小于的仿函数,但是只能处理普通类型的比较

-
上述代码对于我们传的指针类型,我们本意是想要比较指针指向的对象的大小关系,这里进行比较的是对象的地址(指针),比较结果是错误的,不符合我们的预期
-
那么我们针对指针类型对类模板的参数进行更进一步的条件限制实现特化版本,进而达到我们的预期效果
-
这样当模板参数的类型为指针类型的时候,就会去调用我们限制的指针类型的特化版本,对指针指向的对象的大小进行比较

仿函数结合类模板特化:
- 通用仿函数类模板 Less<T> :
- 定义了一个函数对象类,重载 operator() 实现 T 类型的直接比较( a < b )。
2. 指针约束的特化类 Less<T*> :
- 针对指针类型 T* 进行特化,重载 operator() 时通过解引用( *a < *b )实现指针指向值的比较。
当 main 函数调用时:
-
Less<int> com 调用通用模板,比较 a=1 和 b=0 的直接值,输出 1 (因为 1 < 0 不成立,返回 false ,即 0 )
-
Less<int*> com1 调用特化模板,比较 pa (指向 1 )和 pb (指向 0 )的解引用值,即 1 < 0 ,返回 false ,输出 0
模板的分离编译
什么是分离编译
什么是分离编译:一个程序(项目)由多个源文件共同实现,而每个源文件单独编译形成目标文件,最后将所有的目标文件链接起来形成单一的可执行文件的过程称为分离编译模式
那么小编使用栈的模拟实现的代码进行讲解,并且为了便于讲解,小编将栈的模拟实现代码进行一定的删减,在Stack.h下面是声明和定义不分离的源代码,下面小编将针对Stack.h进行改编讲解
cpp
namespace ming
{
template<typename T, typename Container = std::deque<T>>
class stack
{
public:
void push(const T& val)
{
_con.push_back(val);
}
T& top()
{
return _con.back();
}
private:
Container _con;
};
}
通常来讲模板为什么不能声明和定义分离
cpp
//Stack.h
#include <deque>
namespace ming
{
template<typename T, typename Container = deque<T>>
class stack
{
public:
void push(const T& val);
T& top()
{
return _con.back();
}
private:
Container _con;
};
}
//Stack.cpp
using namespace std;
#include "Stack.h"
namespace ming
{
template<typename T, typename Container>
void stack<T, Container>::push(const T& val)
{
_con.push_back(val);
}
}
//test.cpp
#include <iostream>
using namespace std;
#include "Stack.h"
int main()
{
ming::stack<int> st;
st.push(1);
st.top();
return 0;
}
运行代码,会出现链接错误
- 这里的stack是类模板,在进行编译的时候,由于Stack.cpp和test.cpp是分离编译,所以各自会进行各自的预处理编译汇编,直到链接的时候才会交互
- 那么在test.cpp中预处理阶段会进行展开的头文件Stack.h,那么在Stack.h中有push的声明,没有push的定义,在Stack.h中有top的声明和定义,那么test.cpp在进行编译的时候会将push和top函数名符号进行汇总起来,同时在编译阶段在test.cpp中有stack<int>,T会被确定为int,那么也就可以进行对应的实例化了,会将stack类模板对应有定义的函数进行实例化,函数实例化之后定义才能的到地址,在汇编阶段根据这两个符号去找对应的地址,这时候push,虽然有类型T,不可以进行实例化函数,由于没有定义没有进行实例化,所以push找不到地址,top有定义并且进行了实例化找到地址了,那么在test.o中的符号表中,push没有对应的地址,top有对应的地址,但是由于push有声明并不会直接进行报错因为链接时在其它的目标文件中可能会存在对应符号的地址进行汇总
- 那么在Stack.cpp中预处理阶段会进行展开头文件Stack.h,那么在Stack.h中有push的声明,没有push的定义,在Stack.h中有top的声明和定义,在Stack.cpp中有push的定义(注意这里是类模板),那么Stack.cpp中也没有类似于test.cpp中的stack<int>进行实例化类模板,并且这里也没有进行显示实例化类模板,那么此时T的类型就不能进行确定,在编译阶段,会将push和pop符号汇总起来,并且由于T的类型无法确定,没有办法实例化类模板,那么stack类模板中对应的函数也就不会进行实例化,那么函数不进行实例化函数也就不会有对应的地址,在汇编阶段去根据对应的符号去寻找符号对应的地址的时候就没有办法找到符号对应的地址,那么在Stack.o的符号表中,push和top都没有对应的地址,但是由于push和top有声明并不会直接进行报错因为链接时在其它的目标文件中可能会存在对应符号的地址进行汇总
- 在链接过程,test.o中的符号表中,push没有对应的地址,top有对应的地址,在Stack.o的符号表中,push和top都没有对应的地址,此时进行符号表的合并和符号地址的重定位之后,push还是没有对应的地址,top有对应的地址了,那么此时push没有对应的地址,编译器就会进行报错,那么就会出现链接错误,无法找到符号(函数)对应的地址
怎么样模板才能进行分离编译(只能缓解分离编译,本质上模板不能进行分离编译)
模板分离编译做法:
模板声明和定义分离放在不同的源文件中,进行模板的显示实例化,这里是以类模板为例进行讲解的,那么这里就进行类模板的显示实例化进行讲解
类模板的显示实例化的注意事项:
- 使用关键字template注意这个关键字后面不加分号
- class 类名加尖括号<>; 尖括号内放需要进行对应实例化的模板参数的类型,注意这个语句后面必须要加分号
cpp
//Stack.cpp
#include "Stack.h"
using namespace std;
namespace ming
{
template<typename T, typename Container>
void stack<T, Container>::push(const T& val)
{
_con.push_back(val);
}
template
class stack<int>;
}
//Stack.h
//test.cpp
//这两个文件中维持原代码不变
测试结果如下,push和top可以正常调用运行
模板不分离编译做法
声明和定义分离编译之后将声明和定义放在同一个源文件中
cpp
//Stack.h
#pragma once
#include <deque>
namespace ming
{
template<typename T, typename Container = deque<T>>
class stack
{
public:
void push(const T& val);
T& top()
{
return _con.back();
}
private:
Container _con;
};
template<typename T, typename Container>
void stack<T, Container>::push(const T& val)
{
_con.push_back(val);
}
}
//test.cpp
#include <iostream>
using namespace std;
#include "Stack.h"
int main()
{
ming::stack<int> st;
st.push(1);
st.top();
return 0;
}
运行结果如下 , 能顺利运行
那么下面来分析一下,为什么这种方式可以正常调用push和top?
- 在预处理阶段,test.i中会展开Stack.h头文件,在Stack.h中有push的声明,没有push的定义,在Stack.h中有top的声明和定义,在stack类模板外的Stack.h中声明了类域编写有push的定义,那么在Stack.h中就包含有push和top的声明和定义,进行展开后,在test.cpp中就会有push和top的声明和定义
- 在编译阶段,test.s中会汇总符号,即收集了函数名push和top,并且在此阶段由于代码中编写有stack<int>,那么T就会被确定为int类型,那么就会进行stack类模板的实例化,那么在stack类模板中的函数push和top由于T的类型被确定为int,那么stack模板类的类型就被确定为stack<int>就可以进行类模板的实例化,并且push和top都有对应的定义,就可以进行函数实例化,函数实例化之后push和top函数就有了地址,注意在编译阶段并不会将地址进行收集起来,而是在汇编阶段才会根据符号去寻找对应的地址
- 在汇编阶段,test.o中根据编译阶段汇总的符号,即根据汇编收集的函数名push和top去寻找对应的地址,那么在编译阶段push和top都进行了实例化,都有了地址,那么就可以正常找到地址,进而根据符号和地址去生成符号表,那么此时test.o中的符号表中就有push和top的地址
- 在链接阶段,a.out中进行符号表的合并和重定位,此时test.o的符号表有push和top的地址,链接后生成的符号表中push和top也都有对应的地址,此时编译器进行检查就不会报错,也就不会出现链接错误了
模板的优缺点
优点:
- 模板复用了代码,节省了资源,可以进行更快的迭代开发,c++标准库(STL)因此产生
- 编写者不用再去重复写相同的工作,并且可以根据特定的需求进行模板特化,代码的灵活性增强
缺点:
- 模板会导致代码膨胀(假设编写者本该编写5份类型不同,但是实现大体相同的代码,由于模板的存在,编写者只需要编写一份和类型无关的代码,而是把实例化编写5份大体重复的代码的任务交给编译器去做,不可避免的会造成代码膨胀),同时由于模板需要进行实例化,也会导致编译的时间变长
- 出现模板编译错误的时候,编译器的提示信息比较凌乱,不易于编写者根据提示信息去定位错误
总结
C++模板中的非类型模板参数允许使用常量作为模板参数,解决固定大小容器需求问题。函数模板特化可针对特定类型定制行为,但通常优先使用函数重载。类模板特化分为全特化和偏特化,通过部分参数特化或类型约束实现灵活定制。模板分离编译存在挑战,需通过显式实例化或声明定义合并解决。模板优点在于代码复用和灵活性,但会导致代码膨胀、编译时间增长和错误信息复杂化。
感谢大家的观看!


