一.模板参数分类类型形参与非类型形参。
类型形参即:出现在模板参数列表中,跟在class或者typename之类的参数类型名称。
非类型形参,就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。
#include<iostream>
using namespace std;
#define N 100
template<class T>//静态的栈 也就是要控制大小固定
class stack
{
private:int _a[N];int _top;
};
int main()
{stack<int> s1;return 0;
}
这里我们是通过定义宏来固定栈的大小的 但是如果我们想要同时实现两个大小不同的栈要如何实现
#include<iostream>
using namespace std;
//#define N 100
template<class T,size_t N >//静态的栈 也就是要控制大小固定
class stack
{
private:int _a[N];int _top;
};
int main()
{stack<int,10> s1;//10stack<int,100>s2;//100return 0;
}
这里通过非类型的模版参数来实现同时定义不同大小的栈
这里的s1和s2两个栈 在本质上还是两个不同的类实现的 是两个不同的类型 只不过是将写不同类的工作交给了编译器去实现
这里需要注意的是 在c++20之前只允许整型作非类型模版的参数
c++20之后也支持double等内置类型 内置类型的指针也是可以的
这里N已经是常量了 无法修改 但是这里没有报错是因为按需实例化
#include<iostream>
using namespace std;
//#define N 100
template<class T,size_t N >//静态的栈 也就是要控制大小固定
class stack//这里N已经是常量了 无法修改 但是这里没有报错是因为按需实例化
{
public:void func(){N++;}
private:int _a[N];int _top;
};
int main()
{stack<int,10> s1;//10stack<int,100>s2;//100//s1.func();return 0;
}
这里没有报错是因为按需实例化的原因 因为这里系统为了节约资源 不调用
是不会去检查很多的细节的 只有调用了 才会进行具体检查
int main()
{stack<int,10> s1;//10stack<int,100>s2;//100s1.func();return 0;
}
类型模版可以给缺省值 非类型模版也可以给缺省值
template<class T,size_t N =1000 >//静态的栈 也就是要控制大小固定
class stack//这里N已经是常量了 无法修改 但是这里没有报错是因为按需实例化
{
public:void func(){N++;}
private:int _a[N];int _top;
};
int main()
{stack<int> s3;//这里使用了缺省值 1000大小//stack<int,10> s1;//10//stack<int,100>s2;//100//s1.func();return 0;
}
在我们的std库中也是有使用了非类型模版参数的结构的 可以看做用一个类去封装了静态数组
使用array去与普通的数组去做对比
int main()
{array<int,15> a1;int a2[15];return 0;
}
他们两者最大的区别是什么? 最大的区别是检查是否越界的能力
对于普通的数组 他的越界检查能力是非常敷衍的
int main()
{array<int,15> a1;int a2[15];a2[15];a2[16];a2[17];return 0;
}
这里对普通数组进行只读检查是无法检查出越界的 即便是第一个越界的位置15也无法检测
只有检查写时才能检查出来
并且在稍微远一点的越界位置无论读写都是无法检查出来的
而如果是array数组的话 则无论远近无论读写 都可以检查出来
这就是array与普通的数组最大的区别
那么为什么array能够检查的这么仔细呢 因为array是自定义类型 在调用operator[]时 可以设立很多的检查
array的缺点 1.不负责自动初始化 2.他的开空间是不在堆上面 而是开在栈针上面 堆很大但是 栈针不大 所以array的设计也是非常鸡肋的 不如用vector
#include<iostream>
#include<array>
#include<vector>
using namespace std;
int main()
{vector<int> v = {1,2,3,4,5,6,7};vector<double> v1 = {1.1,2.2,3.3,4.4,5.5,6.6,7.7};int i = 1;int j = { 1 };int k { 1};return 0;
}
这里的初始化方法是多样性的 这里有好有坏 是被使用者诟病的地方 这里会导致别人的误解
这里我们想要打印vector的内容 制作了一个遍历器函数
void printvector(vector<int> v)
{std::vector<int>::iterator it = v.begin();while (it != v.end()){cout << *it << " ";it++ ;}cout << endl;
}
int main()
{vector<int> v = {1,2,3,4,5,6,7};vector<double> v1 = {1.1,2.2,3.3,4.4,5.5,6.6,7.7};int i = 1;int j = { 1 };int k { 1};printvector(v);//printvector(v1);return 0;
}
这里单个的类型的遍历是没有问题的 如果想要进行多个类型的遍历 就需要用到模版
using namespace std;
template <class T>
void printvector(vector<T> v)
{std::vector<T>::iterator it = v.begin();while (it != v.end()){cout << *it << " ";it++ ;}cout << endl;
}
int main()
{vector<int> v = {1,2,3,4,5,6,7};vector<double> v1 = {1.1,2.2,3.3,4.4,5.5,6.6,7.7};int i = 1;int j = { 1 };int k { 1};printvector(v);printvector(v1);return 0;
}
这里却会发生错误 这里正确写法需要再迭代器之前写一个typename
oid printvector(vector<T> v)
{typename std::vector<T>::iterator it = v.begin();while (it != v.end()){cout << *it << " ";it++ ;}cout << endl;
}
int main()
{vector<int> v = {1,2,3,4,5,6,7};vector<double> v1 = {1.1,2.2,3.3,4.4,5.5,6.6,7.7};int i = 1;int j = { 1 };int k { 1};printvector(v);printvector(v1);return 0;
}
那么这里的报错的原因是什么呢 而typename的作用又是什么呢?
这里的报错是因为编译器从上向下检查 检查到这个函数内部时 由于vector::iterator 这个还没有实例化 所以系统不会进行 仔细检查 所以iterator对于编译器来说现在是未知的 而vector类域指定下 可能会取到静态变量 类型 无法确定 所以报错
而typename的作用就是给编译器声明一下 这里是一个类型 先跳过这里等到实例化之后再次进行检查编译
还有另一种不加typename的方式 那就是直接使用auto
void printvector(vector<T> v)
{//typename std::vector<T>::iterator it = v.begin();auto it = v.begin();while (it != v.end()){cout << *it << " ";it++ ;}cout << endl;
}
int main()
{vector<int> v = {1,2,3,4,5,6,7};vector<double> v1 = {1.1,2.2,3.3,4.4,5.5,6.6,7.7};int i = 1;int j = { 1 };int k { 1};printvector(v);printvector(v1);return 0;
}
这时我们的代码也是可以正常运行的
二.模版的特化
函数模版的特化 特化就是进行特殊化处理
template<class T>
bool Less(T left, T 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;}
这里是对日期类的比较大小
这里的第三个比较使用的是指针 那么日期类的指针比较的大小不在是日期的的大小比较 而是变成了比较指针地址的大小 这是我们不想要的结果 这时就需要用到函数模版特化去解决
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;}
这时通过特化版本专门为指针设计了一个版本来比较大小实现
函数模版特化也是有缺点的
template<class T>
bool Less(const T &left,const T & right)
{return left < right;
}//对Less函数模板进行特化
template<>
bool Less<Date*>(const Date* & left,const Date* &right)
{
return *left < *right;}
这时const修饰下 对于特化模版的处理是要不同的
template<class T>
bool Less(const T &left,const T & right)
{return left < right;
}//对Less函数模板进行特化
template<>
bool Less<Date*>(Date* const & left, Date* const &right)
{
return *left < *right;}
这才是正确的写法
原因是原模版中const修饰的T类型的内容 也就是指针指向的内容 而将const放在data*前面 则会修饰到指针 并没有修饰到指针指向的内容 所以要将data*放在const之前 让const修饰到指针指向的内容
最好是不使用特化模版 而是直接去创建一个解决日期类指针比较大小的函数
template<class T>
bool Less(const T &left,const T & right)
{return left < right;
}//对Less函数模板进行特化
//template<>
//bool Less<Date*>(Date* const & left, Date* const &right)
//{
//return *left < *right;
// }
bool Less(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;}
这样也可以正常使用
编译器的调用规则时有现成就用现成的之后在使用模版
所以一个模版一个特化模版 一个函数的情况下会优先使用 函数
接下来介绍类的特化模版
// 类模板
template<class T1, class T2>
class Data
{
public:Data() { cout << "Data<T1, T2>-原模板" << endl; }
private:T1 _d1;T2 _d2;
};// 特化:针对某些特殊类型,进行特殊化处理
// 全特化
template<>
class Data<int, char>
{
public:Data() { cout << "Data<int, char>- 全特化" << endl; }
};int main(){Data<int, int>d1;Data<int, char>d2;return 0;}
在原模版中写过私有成员之后 在特化模版的类中就不需要在写一次了
这里的特化模版是通过不同的参数类型去调用的
还有偏特化 或者称为半特化
template<class T1>
class Data<T1, char>
{
public:Data() { cout << "Data<T1, char>- 半特化" << endl; }
};
二级指针也算是指针
template<>
class Data<int, char>
{
public:Data() { cout << "Data<int, char>- 全特化" << endl; }
};
//偏特化 /半特化
template<class T1>
class Data<T1, char>
{
public:Data() { cout << "Data<T1, char>- 半特化" << endl; }
};
//两个参数偏特化为指针类型
template <typename T1, typename T2>
class Data <T1*, T2*>
{
public:Data() { cout << "Data<T1*, T2*>" << endl; }private:T1 _d1; T2 _d2;
};
template <typename T1, typename T2>
class Data <T1&, T2&>
{
public:Data(const T1&d1,const T2& d2):_d1(d1),_d2(d2){ cout << "Data<T1&, T2&>" << endl; }private:const T1 &_d1;const T2& _d2;
};int main(){Data<int, int>d1;Data<int, char>d2;Data<double, char>d3;Data<int*, int*>d4;Data<int&, int&> d5(1,2);return 0;}
这里不仅可以特化普通的类型还可以特化指针和引用 指针中的特化是一个大类 其中的二级指针三级指针都可以使用这个大的特化类
3.模版分离编译
//func.h
template<class T>
T Add(const T& left, const T& right);
//func.cpp
template<class T>
T Add(const T& left, const T& right)
{return left + right;
}
// test.cpp
#include"func.h"
int main()
{Add(1, 2);Add(1.0, 2.0);return 0;
}
这里我们采用类比法来进行比对
//func.h
void func();
//func.cpp
void func()
{cout << "func" << endl;
}
// test.cpp
#include"func.h"
int main()
{//Add(1, 2);//Add(1.0, 2.0);func();return 0;
}
这时我们可以发现普通的函数声明和定义分离是可以正常使用的
但是对于函数模版来说声明和定义分离却无法正常使用
在test.cpp中调用add函数 这里有声明知道要将参数定义为什么样的类型 但是对于func.cpp中只有定义却没有声明不知道要将其参数设置为什么类型 因此add不会被编译 生成指令也就没有将add的地址放入到符号表当中去 所以链接也就找不到
解决方法 显示实例化
//func.cpp
#include<iostream>
using namespace std;
template<class T>
T Add(const T& left, const T& right)
{return left + right;
}
template
int Add(const int& left, const int& right);template
double Add(const double & left , const double &right);
这时候代码就可以正常使用了
但是函数模版声明和定义分离也是有弊端的 需要不停的去加入显示实例化 所以最好的解决方法就是不要去进行声明和定义分离
对于类模版来说 长点的成员函数 声明和定义分离 写到当前文件类外面 短的可以直接定义在类里面 默认就是inline
模板总结 【优点】
1. 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生
2. 增强了代码的灵活性
【缺陷】
1. 模板会导致代码膨胀问题,也会导致编译时间变长
2. 出现模板编译错误时,错误信息非常凌乱,不易定位错误