一、非类型模板参数
1.1本质
非类型模板参数本质是一种常量,他的底层和其他模板参数一样,都是交给编译器实现多个类
1.2出现原因
假设我们要实现两个静态栈,他们的模板类型一样但是空间不一样:
stack<int> st1;//希望空间大小是15
stack<int> st2;//希望空间大小是200
那么我们在定义栈的时候用#define来定义一个符号代表大小,这个大小该多少合适呢?很明显,15或200都是不合理的
因此,提出了非类型模板参数的概念:(支持缺省参数)
template<class T,size_t N=4>
stack<int,15> st1;//希望空间大小是15
stack<int,200> st2;//希望空间大小是200
1.3非类型模板参数允许的类型范围
C++20之前只支持整形作为非类型模板参数,但之后就可以支持非整形(如double,float,int*等等,可以推广到内置类型),但是自定义类型还是不支持的
1.4封装静态数组的类:array
1.4.1模板参数和使用方式
array就采用了非类型模板参数,它的模板参数有
<class T,sie_t N>
使用的时候可以
array<int,10> aa;
1.4.2出现原因
普通数组对于越界的检查方式都是"抽查"(往往只能查最大值附近),如
a[10] = { 0 };
a[10] = 1;//此时会直接报错
a[13] = 2;//此时往往不会报错
例子中是修改数据,如果只是访问数组中数据,报错的概率会更低,如打印数组数据时的访问
1.4.2补:
位图bitset也会用到非类型模板参数
1.4.3缺点
①array完全可以用vector来代替
②array创建完成后并不会直接对数组空间初始化
③array是在当前栈中开辟空间,而不是在堆里,例如要存10个int类型的1,
array的sizeof结果会是40;vector的sizeof结果却只是16(因为指针的缘故,在32位下的结果),
而栈的空间十分有限
1.4补:C++11中的"一切皆可花括号赋值"
C++11支持了很多情况下的{}直接赋值,如
vector<int> v1={1,2,3,5};
vector<double> v2={1.1,2.2,3.3,5.5};
//下面三个效果一样
int i=1;
int j={1};
int k{1};
当然也包括多参数构造函数使用的{}
1.5关于实现不同模板类型对应vector的打印
1.5.1解决方法
显式实现打印函数的时候用模板即可,但需要注意在定义首位迭代器的时候必须
typename vector<T>::const_iterator it=v.begin();
1.5.2原因
类模板在没有实例化的时候,编译器不会去查里面的细节内容
而在类的外面出现vector<T>::const_iterator这种指定方式对应了两种可能:
①静态成员变量在类外的访问
②一种类型,用来创建变量
只有在指定了typename之后,明确其为"一种类型名称",来让编译器实例化时再查看
(当然,定义首位迭代器时也可以用auto关键字来避开这一问题)
1.5补:取别名相关问题
引用是给变量起别名
typedef是给类型起别名
#define是给常量起别名
注意不要混淆
二、模板的全特化与偏特化
2.1模板特化的本质
其实是对特定模板类型的特殊化处理
2.2函数模板的全特化
2.2.1使用举例
假如我预先准备了一个比较大小的模板
template <class T>
bool less(T left, T right)
{
return left<right;
}
此时我实现了一个日期类Date,并重载了它的' < '符号,此时我如果希望传入两个Date*类型的指针也能比较出Date本身的大小,我该怎么做呢?
此时便可以使用函数模板的特化:
template <Date*>
bool less(Date* left, Date* right)
{
return *left < *right;
}
2.2.2不推荐使用的原因
其实函数模板特化的坑很多,比如const修饰下,指针的特化
如果原模版函数是这样的:
template <class T>
void func(const T& t1,const T& t2)
{}
我们在特化Date*的时候是否是这样呢?
template <Date*>
void func(const Date*& t1,const Date*& t2)
{}
仔细分析我们就会发现,这样是错误的:特化中的const修饰的是Date*指向的内容,但实际在原模版函数中,我们的const是用来修饰Date*本身的,因此我们需要修改为这样
template <Date*>
void func(Date* const& t1,Date* const& t2)
{}
可以发现与习惯性思维完全不同,很容易出错
2.3类模板的全特化
使用举例:
template <class T1, class T2>
class A
{
//...
protected:
T1 _a;
T2 _b;
}
//特化的时候
template <>
class A<int , char>
{//...}
再比如之前利用仿函数来实现区分大小堆的时候,最后提到用一个新的类作为仿函数完成对指针指向内容的比较大小,我们完全可以对已有仿函数进行特化
2.3补:
对于类模板,我们也需要注意当const修饰指针时候const的位置问题
2.4偏特化(半特化)
2.4.1特化部分模板参数
如:
template <class T>
class A<T , char>
{//...}
2.4.2特化指针
这是一种比较特殊的特化,可以达到只要传指针都走他的目的
template <typename T1, typename T2>
class A<T1* , T2*>
{//...}
在使用的时候,如果传参
int** ppa;
double* pb;
A(ppa, pb);
再对T1进行typeid打印名字,会出现:int*
T2的打印会出现:double
而sizeof的结果也是对应int*与double的大小
2.4.3特化引用
如:
template <typename T1, typename T2>
class A<T1& , T2&>
{//...}
效果与指针极其相似,T1与T2对应的仍然是原类型,只有在传参都是引用类型的别名时起作用。
2.4补:
指针和引用的特化比较特殊,而且我们除了单独特化,也可以把两者混搭起来进行特化
三、模板的声明与定义分离
3.1函数模板
3.1.1与一般函数声明与定义分离的区别
普通函数在调用时,从汇编层面看会call一个地址,这个地址对应了函数实现中第一句语句的位置
但函数模板因为没有被实例化,所以不会被编译,不会生成指令,也不会有地址放到符号表中这一过程
假设我们实现一个项目,有三个文件:
①Func.h ->在此处声明函数模板
②Func.cpp ->在此处实现函数模板
③Test.cpp ->在此处调用模板函数
如果我在Test.cpp中调用该函数,而此时我只包含了Func.h头文件,(我们都知道,在进行链接过程之前,各个.cpp文件之间都是互相不可见的)我就会因为编译的时候知道实例化成什么,但是没有定义 而无法编译通过;此时Func.cpp中,我虽然有了函数模板的定义,但是我不知道应该实例化成什么而无法编译通过,导致同时报两个错
3.1.1补:普通函数实现过程
普通函数实现时会被编译,生成指令,函数的地址也会放到符号表里
3.1.2解决方案:总结可知他们的问题是两个.cpp中,定义与实例化之间缺少交互
①方式一:在模板定义的地方显式实例化,完成两者交互
template <class T>
T ADD(const T& left, const T& right)
{//...}
template
int ADD(const int& left, const int& right);
template
Date ADD(const Date& left, const Date& right);
但是这种方式需要我们反复补充实例化,需要知道都用到什么什么类型,故不推荐使用
②方法二:声明与定义不要分离到两个文件,把定义全都放在Func.h中,定义可以被实例化找到,自然可以直接完成交互
3.1补:.hpp文件
是.h文件和.cpp文件的聚合体,通常用于模板书写+实例化调用
3.2类模板
方法:
实现的时候,短的成员函数直接放在类的内部,长的可以放到外部,外部定义的时候带上类模板的模板参数即可:
假如有类A:
template <class T1,calss T2>
class A
{//...}
此时要在类外实现函数func
template <class T1,calss T2>
void A<T1,T2>::func(const T1& t1,const T2& t2)
{//...}
四、对于模板的总结
4.1优点
①实现了代码的复用,减少了书写成本,可以更快的进行开发且C++标准库STL也因此产生
②增强了代码的灵活性
4.2缺点
①本质上是把代码生成的任务交给编译器,会导致代码膨胀的问题,导致编译时间加长
②一旦出现编译出错的问题,报错会十分杂乱无章