模板参数分为:

类型模板形参即:出现在模板参数列表中,跟在class或者typename之类的参数类型名称。
非类型模板形参,就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。
在前面的学习中,我们已经学习类型模板参数,接下来我们一起来看一下非类型模板参数。
在我们现阶段的学习中,非类型模板参数就是指传的是常量(现阶段只能传整型int)
假设现在我想搞一个静态的栈,按照以前的逻辑,我们是不是可以搞个宏——
#define N=10
template<class T>
class Stack
{
private:
T _a[N];
int _top;
};void test1()
{
stack<int> st;//存储10个数据
stack<int> st1;//存储1000个数据
}那如果我想存储1000个数据,那是不是就要N=1000,那st中就会有空间浪费
这时候,非类型模板参数就闪亮登场~~~
namespace carrot
{
template<class T ,size_t N=10>非类型模板参数可以给缺省值
class Stack
{
public:
Stack()
:_a(new T[n])
{}
private:
T _a[N];
int _top;
};
void test1()
{
Stack<int, 10> st1;//存储10个数据的静态栈
Stack<int, 1000> st1;//存储1000个数据的静态栈
}
}
注意:C++20才开始支持double,int* ,现在只支持整型!!!
注意:
ok,接下来我们就来介绍一下array——

array是一个静态数组,它的底层使用了非类型模板参数,然后用这个非类型模板参数定义的数组。

我们看到array的接口和前面我们所学容器的接口没有啥区别,唯一的区别就是:array不支持头插、尾插以及中间插入(这是因为空间已经开好了,无法进行扩容操作)
我们看到array是一个静态数组,那array和 int a[10] 有啥区别呢?array是封装的,而a不是封装的。
这里还有个一个问题:array支持的,数组a也支持啊, 那为什么要有array?
//假如现在我想在一个链表中中的每个节点中存一个数组,用array就很方便
list<array<int, 10>> lt;
如果数组想在函数中使用范围for,就要将数组的大小传进去。
除此之外,普通数组也可以使用sort,指向数组的指针是天然的迭代器!!!
总结:再去做其他容器类型,或者进行传参时,array都有普通数组达不到的优势!!!
在前面的学习中,我们了解到:数组越界是会检查的,但是这种检查是抽查,靠近临近位置可以查出来——

但是,这些问题对于array来说,简直就是小意思~
因为array是运算符重载调用,内存严格检查!!!
所谓“特化”,其实就是特殊化处理
通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理,比如:实现了一个专门用来进行小于比较的函数模板 ok,举个例子:
#include<iostream>
#include<ostream>
using namespace std;
template<class T>
bool Less(T left, T right)
{
return left < right;
}
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
bool operator>(const Date& d) const
{
return (_year > d._year) ||
(_year == d._year && _month > d._month) ||
(_year == d._year && _month == d._month && _day > d._day);
}
bool operator<(const Date& d) const
{
return (_year < d._year) ||
(_year == d._year && _month < d._month) ||
(_year == d._year && _month == d._month && _day < d._day);
}
friend std::ostream& operator<<(std::ostream& out, const Date& d);
private:
int _year;
int _month;
int _day;
};
std::ostream& operator<<(std::ostream& out, const Date& d)
{
out << d._year << "/" << d._month << "/" << d._day;
return out;
}
struct PDateLess
{
bool operator() (const Date* p1, const Date* p2)
{
return *p1 < *p2;
}
};
struct PDategreater
{
bool operator() (const Date* p1, const Date* p2)
{
return *p1 > *p2;
}
};
int main()
{
Date* p1 = new Date(2025, 1, 1);
Date* p2 = new Date(2025, 1, 2);
cout << Less(p1, p2)<<endl;
return 0;
}但我们多次运行上面的代码,发现结果有时是1,有时又是0,上面的代码是按照指针进行大小的比较。
但是我们期望的不是按指针来比较,而是按照指针指向的内容来比较,那此时我们就可以实现一个函数模板的特化(对原版本的特殊化处理)

特化:针对某些类型进行特殊化处理!!!
ok,接下来,我们正式开始学习特化相关的知识~~~
函数模板的特化步骤:
// 函数模板 -- 参数匹配
template<class T>
bool Less(T left, T right)
{
return left < right;
}
// 对Less函数模板进行特化
template<>
bool Less<Date*>(Date * left, Date * right)
{
return *left < *right;
}
int main()
{
cout << Less(1, 2) << endl;
Date d1(2022, 7, 7);
Date d2(2022, 7, 8);
cout << Less(d1, d2) << endl;
Date* p1 = &d1;
Date* p2 = &d2;
cout << Less(p1, p2) << endl; // 调用特化之后的版本,而不走模板生成了
return 0;
}特化的特点:如果没有特化版本,就用类模板实例化出的;有特化版本就用特化版本!!!
特化并不像上面所写的这么简单,有些地方很复杂——
通过前面的学习,我们知道,我们应该加上const 引用,这样可以减少传值传参进行拷贝的操作
也就是说,我们应该这么写——

如果函数模板是这样的,那这个函数模板特化会很好写吗?
也许会有UU想说,这不是很简单嘛!直接将 T 换成 Date* 不就是实现针对 Date* 类型的特化了嘛——

好像有点不太对~


所以,我们应该让下面的const修饰引用,而不是修饰指针本身!!!

补充知识:在C++类型声明中,
const总是修饰它左边的内容
函数模板特化在有些地方还是有点麻烦的,通过前面的学习,我们知道模板和函数是可以一起存在的,那我们就可以不用写特化版本,可以直接写函数。

类模板特化分为两种:
类模板特化对内部成员没有要求,也就是原模板中定义的,特化版中可以不定义,也可以新增定义,可以认为类模板特化出的类是一个全新的类
所谓全特化,就是将模板参数列表中所有的参数都确定化

偏特化也叫做半特化,也就是特化部分模板参数
//原模板
template<class T1,class T2>
class Date
{
public:
Date()
{
cout << "Date<T1,T2>" << endl;
}
private:
T1 _d1;
T2 _d2;
};现在我想给T2确定为char类型,我就可以这样做——

当第二个模板参数是char时回去调用这个特化版本的模板
那这时候就有一个问题:当全特化和偏特化同时出现时,该怎么调用呢?

比如专门用来按照小于比较的类模板Less:
#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模板中方式比较,所以只会比较指针,而不是比较指针指向空间中内容
此时可以使用类版本特化来处理上述问题:
template<>
struct Less<Date*>
{
bool operator()(Date* x,Date* y) const
{
return *x < *y;
}
};
偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一 个特化版本。
偏特化可以对参数进行进一步限制!!!


除了上面两种操作之外,还可以混着一起用——

在这块内容里,我们就要来谈谈为什么说模板不能声明和定义分离到两个文件中(比如:一个在.cpp中,一个在.h中)
一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。
假如有以下场景,模板的声明与定义分离开,在头文件中进行声明,源文件中完成定义:
#include<iostream>
using namespace std;
template<class T>
T Add(const T& left, const T& right);
int add(int x, int y);#define _CRT_SECURE_NO_WARNINGS
#include"a.h"
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
int add(int x, int y)
{
return x + y;
}// main.cpp
#include"a.h"
int main()
{
add(1, 2);
Add(1.0, 2.0);
return 0;
}当我们运行上面的代码时,会发现编译器会报出链接错误:

通常情况下,链接错误都是找不到定义
嗯?为什么不能找到这个模板中函数的定义呢?
这就需要我们对编译器的执行过程有一定的掌握:

所谓的链接错误,就是找不到这个函数的地址,那为什么链接时,普通add可以找到,模板函数Add就找不到了呢?


解决方法:
#include"a.h"
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
//显示实例化
template<>
double Add(const double& left, const double& right)
{
return left + right;
}但是,如果类型是int呢?或者是其他类型呢?这样写是不是有点麻烦~

直接在.h文件中定义,不要分离到两个文件中,就不会链接,本质是用的地方包含.h,直接就有函数模板的定义,直接实例化,编译直接拿到函数地址,不需要链接去找地址了
重点记住:模板不支持在两个文件中声明和定义分离!!!