一.仿函数再探
stl_stack/queue-CSDN博客
在priority_queue中,我们介绍了仿函数作为第三个参数来改变堆的类型,而仿函数还有其他的用处。
那么我们是否可以借助优先级队列来对日期类进行排序呢?
答案是可以的,但前提是该日期类必须重载><号。
那什么时候需要使用仿函数呢?
1、类类型不支持比较大小
2、但比较逻辑不是我们想要的
第二点是什么意思呢?
当我们优先级队列中存储的是一个个Date*的数据时,我们期望它们依旧可以完成日期类的比较,但是却不尽人意: 我们看到这里是打印的不是我们想要的结果,而是指针,那么我们对堆顶的数据进行解引用拿到日期本身:
优先级队列默认是大堆,依次取出堆顶数据应该时降序,这里的顺序却不是。并且这些顺序不是固定的,每次运行都不同: 出现这种现象的原因是因为这里比较的时候比较的是指针而不是日期本身,而我们的比较逻辑是由第三个模板参数实现的,所以我们可以改变第三个模板来到达我们比较日期本身而不是指针的目的:
template<typename T>
struct DateLess
{bool operator()(const T& x, const T& y){return *x < *y;}
};xsc::priority_queue<Date*,vector<Date*>, DateLess<Date*>> q1;
对于整型也一样,如果不借助仿函数来实现自己希望的比较逻辑,就会默认采用已有的:
二.模板
1.非类型模板参数
模板参数分为:类型模板参数和非类型模板参数
类型模板参数即:定义模板时,尖括号里typename/class后面跟着的类型,它既可以内置类型也可以是类类型。
非类型模板参数即:就是用一个类型作为类/函数模板的一个参数,前面没有typename/class,在其中常作为常量使用,需要注意的是:只有整型家族的类型才可以作为非类型模板参数,如int、char、bool,也可以有缺省值
template<size_t N = 10>
class Stack
{
private:int _a[N];int _top;
};
我们可以借助上面的类来构建一个定长数组,但并不会进行初始化:
Stack<1> s1;
Stack<5> s2;
Stack<100> s3;
那么如何使用非类型模板参数的缺省值呢?
//Stack s0;//错误
Stack<> s0;//正确
C++-20支持了double作为非类型模板参数
2.array
stl中也有一个容器用到了非类型模板参数——array,它其实就是一个静态数组。array - C++ Reference
它实现的接口和vector类似,但少了push和pop之类的接口,因为就是一个定长的数组,不需要频繁的插入删除。
array<int, 10> a1;
array<int, 100> a2;
array<int, 1000> a3;
那么它和定长数组有什么区别呢?
定长数组:
定长数组对于越界读不进行检查,而对于越界写设置了标志位,也就是进行抽查
越界读:
越界写:
array:
array对越界读和越界写都进行了强制的检查
array是一个类模板,内部重载了operator[],在operator[]内可以进行断言检查,来避免越界。
有了vector为什么还要有array?array的接口vector都有,且两个底层都是数组,而且vector是动态的。
array确实很鸡肋,但是他在一些需要频繁申请小块空间时的效率是比vector要高的,array的空间在栈上,而且没有初始化,它在开辟函数栈帧时就开好了;vector空间开在堆上,还对其进行了初始化,所以array的申请效率比vector要高。
三.模板的特化
特化的意思就是,当前的模板在遇到一些特殊情况时会发生意想不到的错误,如果想避免这种错误,可以对该模板进行特化,生成一个特殊的模板,这个模板可以解决那些特殊情况。
1.函数模板特化
函数模板特化的步骤:
- 必须要有一个基础的函数模板
- 特化出的函数模板template后面加一个空的<>
- 函数名后面加上一个<>,里面指定需要特化的类型
- 函数形参表必须要和基础的函数模板的基础参数类型完全相同,如果不同可能会爆出一些奇怪的错误
现在这里有一个函数模板,用来比较,两个变量的大小
template<class T>
bool lessfunc(T left,T right)
{return left < right;
}
我们可以用它来比较内置类型和类类型的对象
但是当比较的不是变量/对象时,就会发生一些错误:
d1,d2调用lessfunc时就会调到类的运算符重载 ,而p1,p2是指向d1,d2的指针,我们期望他能完成值的比较,而实际上它是用指针进行了比较。
为了解决这种特殊情况,我们就可以对该函数模板进行特化,特化出一个专门比较Date*的lessfunc
//特化
template<>
bool lessfunc<Date*>(Date* left, Date* right)
{return *left < *right;
}
但是我们在这些内部不会改变对象的函数,且对象很大内部涉及深拷贝,我们这样传参就不合适了,需要传const 引用,const表明对象不可修改,传引用可以减少拷贝
但是这样修改的话,特化出来的函数就会报错
template<class T> //bool lessfunc(T left,T right) bool lessfunc(const T& left, const T& right) {return left < right; }//特化 //报错,不是函数模板的专用化 template<> bool lessfunc<Date*>(Date* left, Date* right) {return *left < *right; }
我们先前在特化的前提中提到了,特化出来的函数必须和原函数模板的参数类型保持一致,否则会出一些奇怪的错误,原函数模板参数是const T&,所以函数模板也得是Date*的const &。
//特化 template<> //bool lessfunc<Date*>(const Date* & left, const Date* & right) //错误 bool lessfunc<Date*>(Date* const & left, Date* const & right) //正确 {return *left < *right; }
这里就有两种写法:第一种直接加const,这种是错误的,原函数模板const修饰的是对象本身即left,而当类型时指针时,且const加在前面,修饰的是指针指向的内容,即*left。所以我们应该将const加在*之后,修饰指针本身。
但是当出现const对象时,这里调用结果又会出错,因为其并没有调用特化的版本,原对象时只读,而特化的版本是可读可写,这里权限放大,所以不会调到这个函数中,而是基础的函数模板。
const Date* p3 = &d1; const Date* p4 = &d2; cout << lessfunc(p3, p4) << endl;
为了解决这个问题还得特化出一个const Date* 类型的函数
//特化 template<> bool lessfunc<const Date*>(const Date* const & left, const Date* const & right) {return *left < *right; }
我们看到特化函数模板非常复杂,如果特化到指针const的使用也很麻烦,所以我们还不如直接写一个针对该类型的函数
bool lessfunc(const Date* & left, const Date* & right)
{return *left < *right;
}
这种方式也是比较推荐的。
2.类模板特化
类模板也可能遇到一些特殊情况,这里就需要我们特化出一个类模板,专门解决这类问题。
现在这里有一个基础类:
template<typename T1, typename T2>
class Data
{
public:Data() { cout << "Data<T1, T2>" << endl; }
private:T1 _d1;T2 _d2;
};
2.1全特化
全特化即将类模板的模板参数全部都确定化。
//全特化
template<>
class Data <int, char>
{
public:Data() { cout << "Date<int, char>" << endl; }
};
2.2偏特化/半特化
任何针对模版参数进一步进行条件限制设计的特化版本
对其中一部分参数进行偏特化既可以特化后面的,也可以特化前面的
//偏特化/半特化
template<typename T1>
class Data<T1, double>
{
public:Data() { cout << "clsss Data<T1, double>" << endl; }};//偏特化
template<typename T2>
class Data<char,T2>
{
public:Data() { cout << "class Data<char,T2>" << endl; }};
2.3特殊的偏特化
template<typename T1, typename T2>
class Data<T1*, T2*>
{
public:Data() { cout << "class Data<T1*, T2*>" << endl; }};template<typename T1, typename T2>
class Data<T1&, T2&>
{
public:Data() { cout << "class Data<T1&, T2&>" << endl; }};
我们可以对模板参数做进一步的限制,传的类型是指针/或者引用。
但需要注意的是,在这两个类中,T1和T2还是基础类型,不是指针或者引用,T1*,才是指着;T1&才是引用
template<typename T1, typename T2>
class Data<T1*, T2*>
{
public:Data() { cout << "class Data<T1*, T2*>" << endl; }T1 x;T2 y;T1* m;T2* n;
};
当我们实例化模板时,会调用最匹配的模板。
四.模板的分离编译
1.什么是分离编译
当一个项目很大时,有很多的源文件。我们通过会采取声明和定义分离的方式来进行实现,即类/函数的声明都放在.h文件中,定义放在.cpp/.c文件中。
分离编译(Separate Compilation)是指将程序的源代码分为多个独立的模块(或文件),每个模块可以单独进行编译生成目标文件。这些目标文件可以在后续的链接阶段合并成最终的可执行文件。分离编译的主要目的是提高程序的开发效率和可维护性。
2.模板的分离编译
我们之前提到,模板是不可以声明和定义分离的,但其他的类/函数就可以,这是为什么呢?
如果分离就会发生链接错误:
编译和链接-CSDN博客
编译的过程分为四个阶段预处理——编译——汇编——链接
预处理阶段会将.cpp中的.h文件展开,并进行宏替换,条件编译,去掉注释等工作,然后生成.i文件
编译阶段会进行词法分析,语法分析,语义分析及优化,生成相应的.s为后缀的汇编代码
汇编阶段将编译阶段生成的汇编代码进行翻译,转换成机器能够识别的二进制机器码,生成相应的.o文件
链接阶段会生成要给符号表,记录这一些函数的地址,并且将目标文件合并在一起生成可执行程序
我们在完成项目时通常会分为三个文件:
.h放声明,.cpp放实现,另一个.cpp用来进行测试
当我们编译时,就会进行上面四个步骤:
然后我们在主函数中去调用这两个函数时,汇编代码会调用call指令去找该函数的地址,但是在test.cpp中只有函数的声明而没有定义,不知道该函数的地址,所以不知道call什么。但是在func.cpp中,声明和定义是同时存在的,这里就有函数的地址,所以在生成符号表的时候就将func的地址放到了符号表中,但是add函数此时还没有实例化,没有生成对应的代码,不知道其地址,所以符号表中并没有其地址,然后等到最后一步链接的时候,主函数中的函数调用会去符号表中寻找对应的函数,找到了func,但是没有add,此时就会爆出链接错误。
之所以会在链接时报错时因为,在编译阶段,.h已经展开,而在函数调用出会开始向上搜索函数定义,但是搜索到了声明之后并不会报错,编译器认为定义在另外的文件中。
解决方法:
1、对于模板来说,将声明和定义放到一个文件中
2、在模板定义的位置显式实例化模板
显式实例化:
五.模板总结
优点
1、模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生
2、增强了代码的灵活性
缺陷
1、模板会导致代码膨胀问题,也会导致编译时间变长
2、出现模板编译错误时,错误信息非常凌乱,不易定位错误