1. 非类型模板参数
模板参数分类类型形参和非类型形参。
类型形参即:出现在模板参数列表中,跟在class或者typename之类的参数类型名称。
非类型形参,就是用一个常量作为类(函数)模板的一个参数,在类(模板)中可将该参数当成常量来使用。
cpp
template<class T, size_t N = 10>
class mystack
{
//...
private:
T _a[N];
int _top;
};
int main()
{
//mystack<int,10> s1; //10
mystack<int> s1; //10
mystack<double,100> s2; //100
return 0;
}
注意:
1.非类型模板参数只能是整型,其他的都不行:浮点数、类对象以及字符串是不允许作为非类型模板参数的。
2.非类型的模板参数必须在编译期就能确认结果。
说到这里就要提一下STL中的容器:array(数组),少数场景可能会用到

cpp
#include<array>
int main()
{
array<int, 10> a1;
array<int, 100> a2;
return 0;
}

那么跟原生的数组有什么区别???
cpp
int main()
{
array<int, 10> a1;
array<int, 100> a2;
int arr1[10];
int arr2[100];
return 0;
}
除了迭代器的区别没有什么区别,但是容器array还有一点点作用,对越界的检查比较严格,而C语言对于越界的情况是抽查的行为,可能查到,也可能查不到具体要看平台和编译器,一般在数组的结束位置放一些检查位(本质上是在结束的位置放一些标志位,查看越没越界就看标志位修没修改,但是只能设置一部分的标志位),并且越界读是查看不了的:
cpp
int main()
{
int arr1[10];
//越界检查抽查,只能检查越界写
//arr1[10] = 10; //检查的到
//arr1[11] = 10; //检查的到
arr1[12] = 10; //检查不到
return 0;
}
cpp
//越界读------检查不到
cout << arr1[12] << endl;

原生数组本身访问本质就是解引用
但是对于STL容器:array来说检查越界很敏感,上面两种情况都会检查到(运算符重载)
cpp
cout << a1[10] << endl; //越界读

cpp
a1[12] = 10; //越界写

而对于array容器来说检查输入的值是否小于n,小于n就可以,大于等于n就报错(断言在debug下才会报错,release下就不会报错了)

array除了上面的一点点小优势外,其实并不好用,因为array是静态的数组,意味着要占用当前栈帧的空间,实际上的栈的空间是不大的,容易栈溢出。在想定义一个静态数组并且又要有更好的越界就选择array。
2. 模板的特化
2.1 概念
模板的特化就是针对模板进行特殊化处理。通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理,比如:实现一个专门用来进行小于比较的函数模板。模板特化分为函数模板特化和类模板特化。
2.2 函数模板特化
函数模板的特化步骤:
1.必须要有一个基础的函数模板
2.关键字template后面接一对空的尖括号<>
3.函数名后跟一对尖括号,尖括号中指定需要特化的类型
4.函数形参表:必须要和模板参数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
cpp
//日期类的实现
class Date
{
friend ostream& operator<<(ostream& _cout, const Date& d);
public:
Date(int year = 1900, 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);
}
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)
{
_cout << d._year << "-" << d._month << "-" << d._day;
return _cout;
}
cpp
template<class T>
bool Less(T left,T right)
{
return left < right;
}
int main()
{
cout << Less(1, 2) << endl;
Date d1(2025, 12, 23);
Date d2(2025, 12, 13);
//Date d1(2025, 12, 30);
cout << Less(d1, d2) << endl;
cout << Less(new Date(2025, 12, 23), new Date(2025, 12, 13)) << endl;
return 0;
}
运行结果:(多次运行就会出现问题)


这里的比较不是按照date来比较,而是按照指针来比较,new出来的指针就是不确定的一会大一会小,就会出现各种各样的问题。这个时候就可以用过模板特化来进行处理上面的情况。
特化需要注意的是,是在原模版的基础上进行特殊化处理,不能单独存在:
cpp
//特化:
//针对某些特殊类型进行特殊化处理
template<> //模板参数去掉了,后面也有不去掉模板参数的
bool Less<Date*>(Date* left, Date* right)
{
return *left < *right;
}
运行结果:

注意:一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出。(建议都用下面的普通函数)
cpp
//一个完整的函数------更好更方便
bool Less(Date* left, Date* right)
{
return *left < *right;
}
这种方式简单明了,代码的可读性高,容易书写 ,因为对于一些参数类型复杂的函数模板,特化时特别给出,因此函数模板不建议特化。
为什么函数特化可以用普通函数来替代,是因为函数大多数是不需要实例化的,自动匹配的。
函数的特化并没有多大的用,但是类模板的特化有用。
2.3 类模板特化
类模板是要显式实例化的,普通类是不存在传参数的概念,不存在自动匹配的概念,一定是要进行实例化的,所以特殊处理要特化。
2.3.1全特化
全特化:将全部的参数都进行特化(将模板参数列表中所有的参数都确定化)
cpp
template<class T1,class T2>
class Data
{
public:
Data()
{
cout << "Data<T1,T2>" << endl;
}
private:
T1 _d1;
T2 _d2;
};
//全特化
template<>
class Data<int*,int*>
{
public:
Data()
{
cout << "Data<int*,int*>" << endl;
}
};
int main()
{
Data<int, int>d1;
Data<int*, int*>d2;
return 0;
}
运行结果:

2.3.2 偏特化:
偏特化有以下两种变现方式:
1.部分特化:对部分参数进行特化
cpp
//全特化
template<>
class Data<int*,int*>
{
public:
Data()
{
cout << "Data<int*,int*>" << endl;
}
};
template<>
class Data<int, int>
{
public:
Data()
{
cout << "Data<int,int>" << endl;
}
};
//偏特化
template <class T1>
class Data<T1, int>
{
public:
Data()
{
cout << "Date<T1,int>" << endl;
}
private:
T1 _d1;
int _d2;
};
int main()
{
Data<int, int>d1;
Data<int*, int*>d2;
Data<int, char>d3;
Data<char, int>d4;
return 0;
}
运行结果:(在特化之间也会存在最匹配的问题)

2.参数更进一步的限制
偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本。
cpp
template <typename T1,typename T2>
class Data<T1*,T2*>
{
public:
Data()
{
cout << "Date<T1*,T2*>" << endl;
}
private:
T1 _d1;
int _d2;
};
cpp
int main()
{
Data<char*, int*>d5;
return 0;
}
运行结果:

有全特化走全特化,无全特化走偏特化。
偏特化中的两个参数为引用类型
cpp
template <typename T1, typename T2>
class Data<T1&, T2&>
{
public:
Data()
{
cout << "Date<T1&,T2&>" << endl;
}
};
int main()
{
Data<char&, int&>d6;
return 0;
}
运行结果:

偏特化中的一个参数为引用类型,一直为指针类型:
cpp
template <typename T1, typename T2>
class Data<T1*, T2&>
{
public:
Data()
{
cout << "Date<T1*,T2&>" << endl;
}
};
运行结果:

偏特化对于指针类型的比较:
cpp
int main()
{
ysy::priority_queue<int*> q3;
q3.push(new int(3));
q3.push(new int(1));
q3.push(new int(2));
while (!q3.empty())
{
std::cout << ' ' << *q3.top();
q3.pop();
}
std::cout << '\n';
return 0;
}
cpp
template<class T>
class Less<T*>
{
public:
bool operator()(T* const& x, T* const& y)
{
return *x < *y;
}
};
运行结果:

3.模板的分离编译
先看看普通函数支持声明和定义的分离:

模板(类模板和函数模板)不支持函数的声明和定义的分离:
cpp
//Func.h
template<class T>
void func2(const T& x);
cpp
//Func.cpp
template<class T>
void func2(const T& x)
{
cout << x << endl;
}
cpp
//Test.cpp
#include"Func.h"
int main()
{
func2(200);
return 0;
}
运行结果:

为什么模板不行呢???
模板距离实际被编译还差实例化。
在Func.i 里面不知道T被实例化成什么,Test.i 里面知道要被实例化成什么,但是它们不会交互,为了编译速度。知道要被实例化成什么的地方只有声明,有定义的地方不知道要被实例化成什么,所以就没有被编译成指令,没有放进符号表中,解决方法:
1.显式实例化(可以做到声明和定义分离,不是最优的解决方案,不推荐使用)
cpp
//Func.cpp
#include"Func.h"
template<class T>
void func2(const T& x)
{
cout << x << endl;
}
//显式实例化:可以做到声明和定义分离
template //表示为一个模板
void func2(const int& x);
运行结果:

上述的显式实例化是不够用的,因为只实例化了int,如果是double呢?就不行了


- 直接在.h文件中定义
cpp
//Func.h
template<class T>
void func2(const T& x)
{
cout << x << endl;
}
函数只有声明没有定义才会在链接的时候去找地址,但是当前.h文件中有定义就不会在链接的时候去找。
4. 模板总结
【优点】
- 模板复用了代码,节省资源,更快的迭代器开发,C++的标准模板库(STL)因此而产生
- 增强了代码的灵活性
【缺陷型】
- 模板会导致代码膨胀问题,模板多了实例化的过程,也会导致编译时间变长
- 出现模板编译错误时,错误信息非常凌乱,不易定位错误