💖作者:小树苗渴望变成参天大树🎈
🎉作者宣言:认真写好每一篇博客💤
🎊作者gitee:gitee✨
💞作者专栏:C语言,数据结构初阶,Linux,C++ 动态规划算法🎄
如 果 你 喜 欢 作 者 的 文 章 ,就 给 作 者 点 点 关 注 吧!
文章目录
- 前言
- 一、非类型模板参数
- 二、类模板的特化
- 2.1 函数模板特化
- 2.2类模板特化
- 2.2.1 全特化
- 2.2.2 偏特化
- 三、模板的分离编译
- 四、模板总结
- 五、总结
前言
今天我们来学习模板的进阶知识,在初阶的时候我们讲解到的就是定义模板参数,使用的就是class,那时候学习这些就够学习STL了,但是通过仿函数我们知道了,就之前的模板知识显然是不够的,所以我们今天学习的模板进阶就是为了补充一些其他场景下该如何定义模板,每个知识点都是一个语法,所以这篇信息量还是特别大的,话不多说,我们开始进入正文
本章重点:
- 非类型模板参数
- 类模板的特化
- 模板的分离编译
一、非类型模板参数
我们来看一个案例:
//静态的栈
template<class T>
class stack
{
private:
int _a[10];
};
stack<int> s1;//向存储10个元素
stack<int> s2;//向存储100个元素怎么办??
如果我们想要两个不同大小的栈,应该怎么办??我们不可能会再定义一个类,这样太冗余了,所以这个时候我们的非类型参数模板就出来了
template<class T,size_t N>//使用非模板类型形参
class Stack
{
private:
int _a[N];
};
Stack<int,10> s3;
Stack<int,100> s4;
这样就可以了,我们再来准确的说明一下什么是非类型模板参数,什么是类型模板参数
类型形参即: 出现在模板参数列表中,跟在class或者typename之类的参数类型名称。
非类型形参: 就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。
注意:
- 再上面的概念中说到非类型形参是作为类的模板常量进行使用的,所以不可以修改他的值
- 必须是整型家族的(char,int ,size_t),其余的像double,string都不行,这个再C++20行,目前我们学的是C++98
库里面使用的场景:
我们再库里面有一个容器叫array
我们在之前,要是定义一个数组int a[10];
有了这个array我们可以这么定义array<int,10> a
我们发现非类型模板在这个场景进行了应用,但是就这个设计就有点鸡肋,和普通的数组效果是一样的,有一点不一样,普通数组在读数据的时候越界不能检查出来,而array可以,相信大家学了STL的一部分模拟实现应该都知道,在实现[]的时候进行了下标的断言检查
二、类模板的特化
类模板的特化,其实就是对一些特殊情况单独处理
2.1 函数模板特化
我们来看一个例子:
template<class T>
bool Less(T left, T right)
{
return left < right;
}
int main()
{
cout << Less(1, 2) << endl; // 可以比较,结果正确
return 0;
}
我们传任意两个相同的类型都可以进行比较,
但是如果我传指针进去,会出现什么情况:
我们发现比较结果不正确,因为比较的是指针,它没有按照指向的内容进行比较,我们按照指针的比较毕竟是少数,这就是一种特殊情况,我们就要特殊处理。
我们要写一个函数模板的特化:(前面哪个函数模板不能删除,知识有特殊情况我们才走这个特化)
函数模板的特化步骤:
- 必须要先有一个基础的
函数模板
- 关键字template后面接一对空的
尖括号<>
- 函数名后跟一对尖括号,尖括号中
指定需要特化的类型
- 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误
template<>
bool Less<int *>(int* left, int* right)
{
return *left < *right;
}
//这种是不对的,这就不符合特殊化了,就是模板化了,所以不行
template<class T>
bool Less<T*>(T* left, T* right)
{
return *left < *right;
}
其实我们完全可以使用函数重载来写:
//函数重载
bool Less(int* left, int* right)
{
return *left < *right;
}
//函数重载,适用于任何指针类型
template<class T>
bool Less(T* left, T* right)
{
return *left < *right;
}
第一个肯定匹配第一个函数模板,第二个有现成的int*,第三个只能走函数重载的那个
总结:
我们的函数模板特化比较局限,而且看起来比较复杂,所以我们这时候使用函数重载实现简单明了,代码的可读性高,容易书写,因为对于一些参数类型复杂的函数模板,特化时特别给出,因此函数模板不建议特化。
2.2类模板特化
2.2.1 全特化
全特化即是将模板参数列表中所有的参数都确定化。
我们来看一个例子:
template<class T1,class T2>
class Date
{
public:
Date() { cout << "Date<T1, T2> " << endl; }
private:
T1 a;
T2 b;
};
Date<int, int> d;
我们来假设一下传int,char或者int* ,double*这是两个特殊情况,对类进行一个特化
template<>
class Date<int,char>
{
public:
Date() { cout << "Date<int, char> " << endl; }
private:
int a;
char b;
};
template<>
class Date<int*, double*>
{
public:
Date() { cout << "Date<int*, double*> " << endl; }
private:
int* a;
double* b;
};
使用方法和函数特化一样,而且找现成的匹配,第二个或者第三个不会去找第一个去匹配的,有现成的特化
2.2.2 偏特化
偏特化也分两种:部分偏特化,限制偏特化
- 部分偏特化,对其中一部分类型进行特化
template<class T>
class Date<T, double>//只有第二个参数是double的才能匹配
{
public:
Date() { cout << "Date<T, double> " << endl; }
private:
int* a;
double* b;
};
- 限制偏特化:对参数进行限制,其他限制也是可以的
template<class T>
class Date<T*, T*>//限制只能传相同的指针类型
{
public:
Date() { cout << "Date<T*, T*> " << endl; }
private:
int* a;
double* b;
};
总结:
- 我们的类模板特化就是在写一个类,这个类指定特殊的类型,那么我们传的特殊类型的参数,就会匹配现成的,而且新写的类里面不需要按照原类模板一样,把所有的函数和变量都写进去,根据功能需求写进去就行
- 我们新写的特化类,其实也不叫一个新的类,因为它不能独立存在,还需要依赖原始模板类,但是可以理解为新类。
类模板特化的应用:仿函数的简单介绍
看一下这篇博客最后说的仿函数,我们大部分都是日期对象本身传进去进行比较,但是万一要是传引用进去比较呢??
在那一篇我们提到,我们可以重新写两个类,来达到传引用也可以进行比较的目的,但是这个时候不管是排升序还是降序的指针,都需要传下面两个类进去.
例如:
Priority_queue<Date*,vector<Date*>, LessPNode<Date*>> pq;
Priority_queue<Date*, vector<Date*>, GreaterPNode<Date*>> pq;
我们不想要这么多名字的类模板,我们就像使用Less和Greater来实现,这时候就必须使用类模板,我们可以这样去实现:
这样我们就可以像普通的比较大小一样去传参比较了:
Priority_queue<int> pq;
Priority_queue<int, vector<int>, Greater<int>> pq1;
Priority_queue<Date*> pq2;
Priority_queue<Date*, vector<Date*>, Greater<Date*>> pq3;
//就不需要像这样写的麻烦,大部分用的都是less,所以在写的时候也希望简单一些,类模板特化就可以很好的解决这个问题
Priority_queue<Date*,vector<Date*>, LessPNode<Date*>> pq2;
Priority_queue<Date*, vector<Date*>,GreaterPNode<Date*>> pq3;
那篇没有介绍到类模板的特化,所以还不能使用最好的办法解决我们那个时候遇到的问题,今天为大家解决了问题,希望大家可以很好的理解
三、模板的分离编译
细心的同学应该发现博主在之前的模拟实现的时候只写了一个.h文件,没有把类中函数的定义和声明分离,但是说到是模板定义的函数,定义和声明不能分离,这小节带大家分析为什么不能分离,有没有分离的办法,我们一起来看
看一个例子:
我们在来回顾一下什么情况下会发生链接性错误:
在程序预处理阶段说过,我们进行汇编之后就会进行链接,链接的步骤是,在编译的时候就形成符号表,是通过声明来获得定义地址的符号表,在链接的时候通过符号表上的地址去找定义的地方,没有找到就会报错,而最重要的一点是定义位置的地址什么时候有点,答案是预处理的时候就已经存在,如果没有定义就没有地址。
讲解完成这个我们再来看看类模板参数如果定义和声明分离会发生什么:
通过此案例来看,我们类模板如果讲定义和声明分离就会出现链接性错误,就是找不到定义的地方,而我们看到的是确实定义了啊,为什么会出现这种情况??
答案:我们在启动一个程序的时候,是对每个先将头文件进行展开到源文件当中,然后头文件不参与编译,对单独的每个源文件进行编译,此时我们单独对test.cpp和stack.cpp进行单独编译,此时的stack.cpp里面的定义是模板,不知道具体是什么类型,所以就没有具体的地址,只有是确定的类型才可以进行编译形成地址,因为是单独,也检查不到Test.cpp里面创建的类型,此时形成符号表,就没有刚才定义的地址,所以就会发生链接性错误,而把定义和分离都放在头文件里面,在预处理的是,第一和声明一起展开,把类型替换成创建对象时候的类型,这时候类型就确定,这样就不会报错。
此处的定义和分离是在不同的文件当中,有两种解决办法
-
将声明和定义放到一个文件 “xxx.hpp” 里面或者xxx.h其实也是可以的。推荐使用这种。
-
模板定义的位置显式实例化。这种方法不实用,不推荐使用。
进行显式实例化之后,就明确类型了,就要地址,这样就不会报错
注意:
我们在进行分离和定于的时候,不能指定模板参数的内容
至此我们模板的分离编译就讲解完毕了。
四、模板总结
【优点】
- 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生
- 增强了代码的灵活性
【缺陷】
4. 模板会导致代码膨胀问题,也会导致编译时间变长
5. 出现模板编译错误时,错误信息非常凌乱,不易定位错误
五、总结
我们关于模板的进阶也讲解完毕了,至此我们的C++初阶知识也就讲解完毕了,不知道同学们是否有所收获呢??接下来博主将讲解C++进阶相关的知识,比如继承多态,map和set等有难度的知识,也是提升大家水平的东西,希望大家能来支持博主,我们下一个专栏再见