文章目录
前言
模板是搭建 STL 的基本工具,同时也是泛型编程思想的代表,模板用好了可以提高程序的灵活性,以便进行更高效的迭代开发,本文就来重点介绍模板的一些高阶操作
正文开始!
一、非类型模板参数
之前所使用的模板参数都是用来匹配不同的类型,如 int、double、Date 等,模板参数除了可以匹配类型外,还可以匹配常量(非类型)
使用方法
在定义模板参数时,不再使用 class 或 typename,而是直接使用具体的类型,如 size_t,此时称为 非类型模板参数
另外,非类型模板参数必须为常量,即在编译阶段确定值
利用 非类型模板参数 定义一个大小可以自由调整的 整型数组 类
再加入一个模板参数:类型,此时就可以得到一个 泛型、大小可自定义 的数组
另外,非类型模板参数也支持缺省
类型要求
非类型模板参数要求类型为 整型家族,其他类型是不行的(C++20后可以,但是C++20还没有普及)
//整型家族(部分)
template<class T, int N>
class arr1 { /*……*/ };
template<class T, long N>
class arr2 { /*……*/ };
template<class T, char N>
class arr3 { /*……*/ };
//浮点型,非标准,err
template<class T, double N>
class arr4 { /*……*/ };
array
在 C++11 标准中,引入了一个新容器 array,它就使用了 非类型模板参数,为一个真正意义上的 泛型数组,这个数组是用来对标传统数组的
C++11后才有,其实你可以注意一下,这个比vector还晚,但是比vector差多了
array 是泛型编程思想中的产物,支持了许多 STL 容器的功能,比如 迭代器 和 运算符重载 等实用功能,最主要的改进是 严格检查越界行为,(重载运算符中添加 assert(pos >= 0 && pos < N) )我们以前C语言的数组其实对于内存越界的管理不是特别好
#include <iostream>
#include <cassert>
#include <array>
using namespace std;
int main()
{
int arrOld[10] = { 0 }; //传统数组
array<int, 10> arrNew; //新标准中的数组
//与传统数组一样,新数组并没有进行初始化
//新数组对于越界读、写检查更为严格
arrOld[15]; //老数组越界读,未报错
arrNew[15]; //新数组则会报错
arrOld[12] = 0; //老数组越界写,不报错,出现严重的内存问题
arrNew[12] = 10; //新数组严格检查
return 0;
}
但是实际开发中,很少使用 array,因为它对标传统数组,连初始化都没有,vector 在功能和实用性上可以全面碾压,并且 array 使用的是 栈区 上的空间,存在栈溢出问题,可以说 array 是一个鸡肋的容器
二、模板特化
函数模板特化
模板除了可以根据传入的类型进行实例化外,还可以指定实例化,以适合各种不同的特殊化需要
还记得我们上篇的优先级队列处理日期类的特殊处理么?因为我们队列里面放的是指针,而指针的大小计较没有意义,因为地址的使用是随机的,于是我们对这个情况再单独实现了一个类,这实在是不太雅观
两者内容相同,可泛型在这里比的是地址了!
这时候,就是模板特化发力的时候了
这时候利用模板的特化,对接受的如果是int*指针,那专门进行处理
其实这里有个坑,特化版本的const得放在 * 符号的后面,因为原先我们修饰的是T类型的变量x,放到这里就是修饰int*类型的变量x,而如果把const放到 * 之前,那就是修饰 * x的了,变成了修饰所指向的内容
其实有时候,这个函数的特化不是那么好用,甚至有时候我感觉还不如函数重载
类模板特化
模板特化主要用在类模板中,它可以在泛型思想之上解决大部分特殊问题,并且类模板特化还可以分为:全特化和偏特化
在开始讲解之前,我们先来回顾一下日期类
class Date
{
public:
Date(int year = 1970, 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;
};
全特化
全特化指将所有的模板参数特化为具体类型,将模板全特化后,调用时,会优先选择更为匹配的模板类
//原模板
template<class T1, class T2>
class Test
{
public:
Test(const T1& t1, const T2& t2)
:_t1(t1)
,_t2(t2)
{
cout << "template<class T1, class T2>" << endl;
}
private:
T1 _t1;
T2 _t2;
};
//全特化后的模板
template<>
class Test<int, char>
{
public:
Test(const int& t1, const char& t2)
:_t1(t1)
,_t2(t2)
{
cout << "template<>" << endl;
}
private:
int _t1;
char _t2;
};
int main()
{
Test<int, int> T1(1, 2);
Test<int, char> T2(20, 'c');
return 0;
}
对模板进行全特化处理后,实际调用时,会优先选择已经特化并且类型符合的模板
另外,你需要注意
- 在进行全特化前,需要存在最基本的泛型模板
- 全特化模板中的模板参数可以不用写
- 需要在类名之后,指明具体的参数类型,否则无法实例化出对象
偏特化
指将泛型范围进一步限制,可以限制为某种类型的指针,也可以限制为具体类型
偏特化(尤其是限制为某种类型)在 泛型思想 和 特殊情况 之间做了折中处理,使得限制范围式的偏特化也可以实现 泛型
//原模板---两个模板参数
template<class T1, class T2>
class Test
{
public:
Test()
{
cout << "class Test" << endl;
}
};
//偏特化之一:限制为某种类型
template<class T>
class Test<T, int>
{
public:
Test()
{
cout << "class Test<T, int>" << endl;
}
};
//偏特化之二:限制为不同的具体类型
template<class T>
class Test<T*, T*>
{
public:
Test()
{
cout << "class Test<T*, T*>" << endl;
}
};
int main()
{
Test<double, double> t1;
Test<char, int> t2;
Test<Date*, Date*> t3;
return 0;
}
输出结果如图:
借助偏特化也可以用来解决指针无法正确比较大小问题,请你自行尝试一下吧
三、模板的分离编译
模板不能声明和定义分离,会出现编译问题,这是我们之前就了解过的事情
失败原因
声明与定义分离后,在进行链接时,无法在符号表中找到目标地址进行跳转,因此链接错误,当模板的 声明 与 定义 分离时,因为是 泛型,所以编译器无法确定函数原型,即 无法生成函数,也就无法获得函数地址,在符号表中进行函数链接时,必然失败
解决方法
- 在函数定义时进行模板特化,编译时生成地址以进行链接
- 模板的声明和定义不要分离,直接写在同一个文件中
//定义
//解决方法一:模板特化(不推荐,如果类型多的话,需要特化很多份)
template<>
int add(const int x, const int y)
{
return x + y;
}
//定义
//解决方法二:声明和定义写在同一个文件中
template<class T>
T add(const T x, const T y)
{
return x + y;
}
你可能会想,为什么不在链接的时候尝试匹配一下正确类型?
答案是这样的话,太耗费资源了,编译太慢是坏事,万一有好几百份文件就坏事了
总结
模板是 STL 的基础支撑,它也是一把双刃剑,既有优点,也有缺点,只有把它用好了,才能使代码 更灵活、更优雅
标签:类型,int,18,Test,year,Cpp,模板,特化 From: https://blog.csdn.net/2301_80392199/article/details/143156843