C++ 模版初阶 请移步
文章目录
- [1. 非类型模板参数](#1. 非类型模板参数)
- [2. array(加餐,作为示例)](#2. array(加餐,作为示例))
-
- [2.1 核心特性](#2.1 核心特性)
- [2.2 核心优势](#2.2 核心优势)
- [2.3 注意事项](#2.3 注意事项)
- [2.4 与原生数组/vector 对比表](#2.4 与原生数组/vector 对比表)
- [3. 模板的特化](#3. 模板的特化)
- [4. 模板分离编译](#4. 模板分离编译)
-
- [4.1 什么是分离编译](#4.1 什么是分离编译)
- [4.2 模板的分离编译](#4.2 模板的分离编译)
- 4.3解决方法
- 5.模板总结
- 求三!
1. 非类型模板参数
模板参数分类类型形参与非类型形参。
类型形参即:出现在模板参数列表中,跟在class或者typename之类的参数类型名称。(参考模版初阶内容)
非类型形参,就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。
以下是一个简单的类模版:

由于T的存在我们可以给Stack里面的参数定义为int、char、自定义类型等,但是如果我想创建两个Stack类 一个_a容量为10 一个为1000呢,我们的#define只能定义一个N,这样仅靠类型参数就做不到了。
那么该怎么解决呢?
我们可以利用非类型模版参数 ,用一个常量作为类(函数)模板的一个参数。
此时的
Stack<int,10>和Stack<int,1000>此时双方已经是两个不同的类。>我们调试的时候可以看到他们大小不同:
当然,我们也可以给个缺省值:
注意:
- 浮点数、类对象以及字符串是不允许作为非类型模板参数的。(double、string、自定义类型至少要C++20才支持,C++20 目前还不够成熟)

- 非类型的模板参数必须在编译期就能确认结果。
- 当前版本仅支持
char 、int、size_t这样的整形作为非类型模版参数。
2. array(加餐,作为示例)
array是 C++11 引入的固定大小数组容器,定义在<array>头文件中,本质是基于非类型模板参数实现的静态数组封装,兼具原生数组的高效性和容器的易用性、安全性。(注:默认是无初始化的)
std::array是固定大小的数组容器,其实现核心依赖非类型模板参数指定数组大小,示例定义逻辑:
cpp
// 模板参数列表中,N为非类型模板参数(整型常量)
template <typename T, size_t N>
class array {
// 内部可将N作为常量使用,定义固定大小的数组
T _arr[N];
public:
// 成员函数可基于N实现操作,如获取大小
size_t size() const { return N; }
};
2.1 核心特性
-
固定大小(编译期间开好栈帧)
-
大小由非类型模板参数指定,且必须是编译期常量
-示例:cpp#include <array> // 合法:10 是编译期常量(非类型模板参数) array<int, 10> arr1; // 非法:运行期变量不能作为非类型模板参数 int n = 5; array<int, n> arr2; -
对比原生数组:
array大小是类型的一部分(array<int,10>和array<int,20>是不同类型),原生数组仅语法层面限制大小。
-
-
连续内存 + 无额外开销
- 内存布局与原生数组完全一致(栈上连续存储,无堆内存分配),无额外空间开销(对比
vector的动态扩容、内存冗余); - 支持直接通过
[]访问元素,也可通过at()做越界检查(更安全)。
- 内存布局与原生数组完全一致(栈上连续存储,无堆内存分配),无额外空间开销(对比
-
容器化接口,易用性提升
提供 STL 容器统一的成员函数,避免原生数组的缺陷:核心成员函数 功能 对比原生数组优势 size()返回数组元素个数 原生数组需手动计算(如 sizeof(arr)/sizeof(arr[0]))empty()判断是否为空(仅 size=0 时为 true) 原生数组无此接口 at(idx)访问下标 idx 元素,越界抛 out_of_range异常原生数组 []越界无检查,行为未定义front()/back()获取第一个/最后一个元素 原生数组需手动写 arr[0]/arr[size-1]data()返回指向数组首元素的指针 原生数组可直接退化为指针,但语义不明确 fill(val)将所有元素填充为 val 原生数组需手动循环赋值
2.2 核心优势
-
安全性远超原生数组
-
at()和[]都提供越界检查,避免原生数组[]越界导致的内存非法访问(原生数组虽然也会检查越界,但是是抽查而且只查越界写(因为是依靠标记位查的,因为数组的[]是转化为指针 解引用的形式访问的,有的时候越界写依然会跑);但是array只要你越界就一定会查出来,而且不仅能查越界写,也能查到越界读(arrar里的[]是operator[]函数调用,所以能查出来)。 -
不会像原生数组那样在传参时退化为指针(丢失大小信息),示例:
cpp// 原生数组传参:退化为 int*,size 丢失 void func(int arr[]) { /* 无法获取原数组大小 */ } // array 传参:保留大小信息(模板参数固定) void func(array<int,10> arr) { cout << arr.size(); // 正确输出 10 }
-
-
兼容性好,支持 STL 算法
-
可无缝对接 STL 算法(如
sort、find、for_each),示例:cpparray<int,5> arr = {3,1,4,2,5}; sort(arr.begin(), arr.end()); // 排序 auto it = find(arr.begin(), arr.end(), 3); // 查找元素 -
原生数组需手动传递首尾指针(
sort(arr, arr+5)),语义不如array清晰。
-
-
无动态内存管理成本
- 对比
vector:array是静态数组(栈内存,无构造/析构的内存开销),适合存储固定大小、生命周期短的数据。
- 对比
2.3 注意事项
- 大小不可动态调整 :若需运行期改变数组大小,应使用
vector; - 初始化方式 :
- 列表初始化:
array<int,3> arr = {1,2,3};(C++11 及以上); - 未初始化时元素值为未定义(同原生数组),可通过
fill()初始化:arr.fill(0);;
- 列表初始化:
- 非类型模板参数约束:大小必须是整型常量表达式,浮点数、字符串、类对象不能作为大小参数。
2.4 与原生数组/vector 对比表
| 特性 | array | 原生数组 (int arr[N]) | vector |
|---|---|---|---|
| 大小是否固定 | 是(编译期确定) | 是(语法层面) | 否(运行期扩容) |
| 内存位置 | 栈/静态存储 | 栈/静态存储 | 堆 |
| 越界检查 | at() 、[]支持 | 无 | at() 、[]支持 |
| 传参是否丢失大小 | 否 | 是(退化为指针) | 否(size() 获取) |
| 动态扩容 | 不支持 | 不支持 | 支持 |
| 额外内存开销 | 无 | 无 | 有(扩容预留空间) |
3. 模板的特化
3.1 概念
通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理。
cpp
//函数模版 -- 参数匹配
template<class T>
bool Less(T left,T right)
{
return left<right;
}
int main()
{
cout<<Less(1, 2)<<endl; //1
double* p1=new double(2.2);
double* p2=new double(1.1);
cout<<Less(p1, p2)<<endl;//1 或 0
string* p3=new string("111");
string* p4=new string("222");
cout<<Less(p3, p4)<<endl;//1 或 0
return 0;
}
每次程序结果运行不同,那是p1 p2和p3 p4他们比较的是地址,而地址并不是说先new的就一定地址位置大,有可能前面会有地址释放,然后后new的地址就会比先new的大。
此时,就需要对模板进行特化 。即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。模板特化中分为函数模板特化 与类模板特化。
3.2 函数模板特化
函数模板的特化步骤:
- 必须要先有一个基础的函数模板
- 关键字
template后面接一对空的尖括号<> - 函数名后跟一对尖括号,尖括号中指定需要特化的类型
- 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
cpp
//函数模版 -- 参数匹配
template<class T>
bool Less(T left,T right)
{
return left<right;
}
//特化
template<>
bool Less<double*>(double* left,double* right)
{
return *left<*right;
}
//特化
template<>
bool Less<string*>(string* left,string* right)
{
return *left<*right;
}
int main()
{
cout<<Less(1, 2)<<endl;
double* p1=new double(2.2);
double* p2=new double(1.1);
cout<<Less(p1, p2)<<endl;//此时Less 就会优先走特化 而不是原模版
string* p3=new string("111");
string* p4=new string("222");
cout<<Less(p3, p4)<<endl;////此时Less 就会优先走特化 而不是原模版
return 0;
}

特化易错点
然而在实际中 我们的模版参数大多为const T&形式,此时我们按照上面特化的方式写,我们发现我们直接特化不上

那怎么办?有人会说了 主播主播 我们把特化也按上面格式加上const 和 &不就可以了?但结果还是特化失败:

这个就和const的修饰有关了
const T& a:const在&的左侧,修饰的是T,表示指向的对象不能被修改。T& const a:const在&的右侧,但引用本身是 "别名",没有独立的内存地址,不能被const修饰(引用一旦绑定对象就不能再改变指向),所以这种写法是语法错误。const T* p1 && T const * p2:const在*的左边都是修饰指针指向对象不能修改。T* const p3:const在*的右边都是修饰指针本身。
这里的const double*& left中 left → 是一个引用(&) → 指向一个指针(*) → 该指针指向一个const double 类型的对象(const double) 说白了就是const修饰的是const double对象本身 而不是原模版中的引用。
如果还不是很理解 可以看面的分析:
left是一个指向 "const double 类型指针" 的引用,拆解成通俗语言:
- left 是引用(别名),它绑定的目标是一个「指向
const double的指针」;- 通过这个指针,不能修改指向的
double值(因为const修饰double);- 但
left作为指针的引用,可以修改指针本身的指向(比如让指针指向另一个const double对象)。
所以正确的写法是:
cpp
//函数模版 -- 参数匹配
template<class T>
bool Less(const T& left,const T& right)
{
return left<right;
}
//特化
template<>
bool Less<double*>( double* const & left,double* const& right)
{
return *left<*right;
}
注意:一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出。(普通函数和函数模版是可以同时存在的)
cpp
bool Less(double* left,double* right)
{
return *left<*right;
}
该种实现简单明了,代码的可读性高,容易书写,因为对于一些参数类型复杂的函数模板,特化时特别容易出错,因此函数模板不建议特化。
3.3 类模板特化
3.3.1 全特化
全特化即是将模板参数列表中所有的参数都确定化。
cpp
template<class T1,class T2>
class Data
{
public:
Data() {cout<< "Date<T1,T2>" <<endl;}
private:
T1 _d1;
T2 _d2;
};
//全特化
template<>
class Data<int,char>
{
public:
Data(){cout<< "Date<int,char>"<<endl;}
};
int main()
{
Data<int, int> d1;// Date<T1,T2>
Data<int, char> d2;// Date<int,char>
return 0;
}
3.3.2 偏特化
偏特化:任何针对模版参数进一步进行条件限制设计的特化版本。
偏特化有以下两种表现方式:
部分特化
将模板参数类表中的一部分参数特化。
cpp
//偏特化
template<class T1>
class Data<T1,char>
{
public:
Data(){cout<<"Date<T1,char>"<<endl;}
};
int main()
{
//尝试找最匹配的
Data<char,char> d3;// Date<T1,char>
return 0;
}
⚠️:特化本质就是写一个全新的类,跟你原类 里面有什么函数,什么变量是没关系的。
参数更进一步的限制
偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本。
cpp
//对参数的进一步限制
template<class T1,class T2>
class Data<T1*,T2*>
{
public:
Data(){cout<<"Date<T1*,T2*>"<<endl;}
};
int main()
{
Data<char*, int*> d4;// Date<T1*,T2*>
return 0;
}
当然里面的T1 T2依然不影响使用
cpp
template<class T1,class T2>
class Data<T1*,T2*>
{
public:
Data(){cout<<"Date<T1*,T2*>"<<endl;}
void f1()
{
T1 x;
cout<< typeid(x).name()<<endl;//头文件<typeinfo>
}
};
int main()
{
Data<char*, int*> d4;// Date<T1*,T2*>
d4.f1();// char
return 0;
}
当然不光可以限制指针,也可以限制引用,甚至是混合在一起!
3.3.3 类模板特化应用示例
cpp
#include<vector>
#include<algorithm>
template<class T>
struct Less
{
bool operator()(const T& x, const T& y) const
{
return x < y;
}
};
int main()
{
Date d1(2022, 7, 7);
Date d2(2022, 7, 6);
Date d3(2022, 7, 8);
vector<Date> v1;
v1.push_back(d1);
v1.push_back(d2);
v1.push_back(d3);
// 可以直接排序,结果是日期升序
sort(v1.begin(), v1.end(), Less<Date>());
vector<Date*> v2;
v2.push_back(&d1);
v2.push_back(&d2);
v2.push_back(&d3);
// 可以直接排序,结果错误日期还不是升序,而v2中放的地址是升序
// 此处需要在排序过程中,让sort比较v2中存放地址指向的日期对象
// 但是走Less模板,sort在排序时实际比较的是v2中指针的地址,因此无法达到预期
sort(v2.begin(), v2.end(), Less<Date*>()));
return 0;
}
通过观察上述程序的结果发现,对于日期对象可以直接排序,并且结果是正确的。但是如果待排序元素是指针,结果就不一定正确。因为:sort最终按照Less模板中方式比较,所以只会比较指针,而不是比较指针指向空间中内容,我们之前是使用仿函数 来解决的(参考优先级队列章节),此时可以使用类版本特化来处理上述问题:
cpp
// 对Less类模板按照指针方式特化
template<>
struct Less<Date*>
{
bool operator()(Date* x, Date* y) const
{
return *x < *y;
}
};
4. 模板分离编译
4.1 什么是分离编译
一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。
比如说一个函数 他只有.h文件的声明 没有.cpp的实现 那么他就会有链接错误 下图是文件编译的全过程:

在汇编语言中 其实调用函数的本质 就是call + 函数地址,而函数其实跟数组很像,我们在test.cpp中对于函数的申明其实我们是没有它的地址的。而函数的地址是在定义中不是声明中。 在有些编译器中 test.cpp文件中汇编语言会形成call (?)的声明。我们在test.cpp文件通过声明去符号表中找函数地址,找到后就会生成可执行程序。
而模版的问题是有定义也会有错误!
4.2 模板的分离编译
假如有以下场景,模板的声明与定义分离开,在头文件中进行声明,源文件中完成定义:
cpp
// a.h
template<class T>
T Add(const T& left, const T& right);
// a.cpp
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
// main.cpp
#include"a.h"
int main()
{
Add(1, 2);
Add(1.0, 2.0);
return 0;
}

函数模版的实例化在main.cpp文件中,但是函数模版定义确在a.cpp中 他们在链接前是不会交互的 所以根本不会生成对于加法函数 所以编译器链接时候根本找不到函数地址!
4.3解决方法
法一:显示实例化
cpp
// a.h
template<class T>
T Add(const T& left, const T& right);
// a.cpp
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
//显示实例化
template
int Add(const int& x);
template
double Add(const double& x);
// main.cpp
#include"a.h"
int main()
{
Add(1, 2);
Add(1.0, 2.0);
return 0;
}
但是现实中太麻烦 很少人这么用!
法二:直接定义到.h文件中
cpp
// a.h
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
// main.cpp
#include"a.h"
int main()
{
Add(1, 2);
Add(1.0, 2.0);
return 0;
}
当预编译期间 头文件展开main.cpp直接就存在模版的定义,就不需要链接了,编译期间就实例化上了地址!更加推荐这个方法
5.模板总结
【优点】
- 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生
- 增强了代码的灵活性
【缺陷】
- 模板会导致代码膨胀问题,也会导致编译时间变长
- 出现模板编译错误时,错误信息非常凌乱,不易定位错误
| 分类 | 具体内容 |
|---|---|
| 优点 | 1. 复用代码,节省资源,加速迭代开发(STL的核心基础) 2. 提升代码灵活性 |
| 缺陷 | 1. 引发代码膨胀,增加编译时长 2. 编译错误信息杂乱,定位问题难度高 |
C++ 模版初阶 请移步
>我们调试的时候可以看到他们大小不同:
