目录
前言
1. 仿函数
1.1 什么是仿函数
1.2 仿函数的应用
2. 模板
2.1 非类型模板参数
非类型模板参数的应用
2.2 模板特化
概念
函数模板的特化
类模板特化
全特化
偏特化
3. 模板分离编译问题
解决办法
4. 模板总结
总结
前言
我们已经基本学习完了C++的一些基础特性,后续也会继续深入学习C++,对于后续的内容,仿函数和模板是非常重要的基础,所以本文我将会向大家更深层面的介绍C++模板以及仿函数的应用;
1. 仿函数
前文我们提到在实现priority_queue时与STL库存在差异,想要实现priority_queue的完整封装需要对模板有更深入的了解,以及仿函数的应用;
1.1 什么是仿函数
仿函数(Functor)是一种行为类似函数的对象,可以像函数一样被调用。在C++中,仿函数是一个类或者结构体,重载了函数调用运算符operator(),使得对象可以像函数一样被调用;
它本质上就是一个只重载 “ ( ) ” 的类,它的出现是为了代替C语言中的函数指针;C语言中的回调函数在使用时用函数指针非常难受,函数指针的使用导致代码可读性较差,并且使用函数指针有一些局限性,比如只能传递静态函数或全局函数,无法传递非静态成员函数等;
1.2 仿函数的应用
在priority_queue中就使用了仿函数如下图:
通过传递模板参数来控制大堆小堆;
举个简单的例子:
// 可以加模板,加模板后它可以比较int类型,也可以比较double类型
class Greater
{
public:bool operator()(const int& x, const int& y){return x > y;}
};int main()
{Greater com;//com是一个对象// 它可以像函数一样去使用cout << com(10, 100) << endl; // false->0return 0;
}
它就是一个仿函数;operator()的具体内我们可以根据自己的需求实现(比如比较两指针指向内容的大小);它也常用于STL的算法函数中;
vector<int> v = { 1,9,3,6,7,5,8,2 };Greater com;//默认情况了下是升序std::sort(v.begin(), v.end(), com);//降序//也可以传匿名对象
我们可以利用仿函数的特性,做到通过传模板参数达到priority_queue大堆小堆的调整:
//增加一个模板参数
template<class T, class container = vector<T>, class Compare = Less<T>>
class priority_queue
{
public:private:container _con;
};
将向上调整向下调整中比较大小的地方修改即可
Compare com;//实例化一个对象
// 把_con[parent] < _con[child]修改为使用仿函数
com(_con[parent] , _con[child])
这里就不再展示完整的代码,完整封装后的代码我已上传我的代码仓库,详细可见:priority_queue模拟封装
注意:
函数传参和模板传参这里很容易混淆,sort是一个函数,调用时传的对象是形参:
std::sort(v.begin(), v.end(), com);
priority_queue 建小堆时传的是模板参数
priority_queue<int, vector<int>, Greater<int>> pq;
//Greater<int>是类型
2. 模板
前边已经对模板的基本使用做了一些介绍,本文将会更深入的介绍模板;
2.1 非类型模板参数
模板参数可分为两类:
- 类型形参:在模板定义中使用的类型参数,可以用任何合法的类型来实例化模板(也就是之前的基础用法,跟在class或者typename之后的参数类型)
- 非类型形参:在模板定义中使用的非类型参数,通常是一个常量表达式(template <int N>)
比如:
template <class T, size_t N = 10>
class A {T data[N];
};
T
是一个类型形参,N
是一个非类型形参
注意:非类型模板参数只支持整形
非类型模板参数的应用
非类型模板参数有什么用,比如我们需要两个固定大小的栈,一个容量设为10,一个容量设为100;我们用类模板实现的栈可以存储任何类型的数据,但是如何创建出两个不同大小的栈?只用类型参数无法解决,这时就需要用到非类型模板参数;
template<class T, size_t N>
class Stack
{private:T _a[N];
};int main()
{Stack<int, 10> st1; Stack<double, 100> st2; //Stack<double, n> st2; // 这样不行,必须是常量return 0;
}
C++的容器也有使用,C++11添加了新的容器叫array,它是一个定长数组;
使用如下:
array<int, 10> a1;
它是C++11新出的容器,目的就是为了代替C语言的静态数组,C语言的数组对越界没有严格的检查(越界不一定会报错),array的检查比较严格,一旦越界就会报错;
不用它的主要原因是因为array不会对数组进行初始化,大家也更倾向于使用vector;
2.2 模板特化
概念
模板特化是指为特定类型或值参数提供定制化的模板实现。通常情况下,模板是通用的,可以适用于多种类型或值参数。但有时候我们需要针对特定的类型或值参数提供特定的实现这时就可以使用模板特化来实现。
举个例子:
我们实现一个Less,它可以比较任何类型,但对于指针类型却没办法正确比较;
template<class T>
bool Less(T left, T right)
{return left < right;
}
这时就可以用模板的特化,这样写:
template<>
bool Less<int*>(int* left, int* right)
{return *left < *right;
}
函数模板的特化
上述的解决办法使用的就是函数模板的特化,函数模板特化有以下要求:
- 必须要先有一个基础的函数模板
- 关键字template后面接一对空的尖括号<>
- 函数名后跟一对尖括号,尖括号中指定需要特化的类型
- 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误
但是平时我们不会怎么使用函数特化,如果出现函数模板无法解决的情况,一般情况下都会用函数重载(函数重载更为简洁明了,代码的可读性高,容易书写)不建议使用函数模板特化;
类模板特化
类模板特化的规则和函数模板特化的规则类似:
- 必须要先有一个基础的类模板
- 关键字template后面接一对空的尖括号<>
- 类名后跟一对尖括号,尖括号中指定需要特化的类型
全特化
全特化是将模板参数列表中所有的参数都确定化
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; }
private:int _d1;char _d2;
};void test()
{Data<int, int> d1;Data<int, char> d2;
}
在调用模板实例化时会调用更加相符的模板;
偏特化
偏特化:任何针对模版参数进一步进行条件限制设计的特化版本,比如:
template<class T1, class T2>
class Data
{
public:Data() {cout<<"Data<T1, T2>" <<endl;}
private:T1 _d1;T2 _d2;
};
这是一个简单的类模板,偏特化有以下两种表现方式:
- 部分特化
将模板参数类表中的一部分参数特化:
// 将第二个参数特化为int
template <class T1>
class Data<T1, int>
{
public:Data() {cout<<"Data<T1, int>" <<endl;}
private:T1 _d1;int _d2;
};
- 参数更进一步的限制
偏特化并不仅仅是指特化部分参数,它还可以对模板对模板参数做更进一步的限制:
template <typename T1, typename T2>
class Data <T1*, T2*>
//还可以特化为class Data <T1&, T2&>
{
public:Data() {cout<<"Data<T1*, T2*>" <<endl;}
private:T1 _d1;T2 _d2;
};
3. 模板分离编译问题
在使用模板封装完一个类时我们会发现,当声明和定义分离时,编译会报错;主要报错是因为链接错误,为什么会这样?
比如:
// a.h
template<class T>
T Add(const T& left, const T& right);// a.cpp
template<class T>
T Add(const T& left, const T& right)
{return left + right;
}// main.cpp
#include"a.h"
int main()
{Add(1, 2);Add(1.0, 2.0);return 0;
}
我们将一个类声明和定义分离,C/C++程序运行主要分为以下阶段:
- 预处理
- 编译
- 汇编
- 链接
在编译阶段,编译器对多个源文件分离开单独编译,头文件不会参与编译,问题就出在这里;a.cpp中的Add是函数模板,它在编译时没有明确的数据类型,所以Add不会生成汇编代码;
到了链接阶段,由于Add没有生成汇编代码,链接时就会找不到Add(地址),进而造成了链接错误;
解决办法
- 将声明和定义放到一个文件 "xxx.hpp" 里面或者xxx.h
- 模板定义的位置显式实例化(比较不实用,类型多的时候需要实例化很多次,不推荐)
最简单快捷的方式就是将声明和定义写在同一个文件中,编译时,文件已经展开,向上找就可以找到函数;
4. 模板总结
优点:
使用模板可以更好的复用代码,更快的迭代开发,C++的STL库也因此而产生,增强代码的灵活性
缺点:
使用模板后,具体的工作都交给了编译器,编译器需要推导类型然后实例化,这会导致编译的时常增加,出现错误时,报错信息也比较凌乱,不易定位错误;
总结
后续的容器封装会用到很多模板相关的知识,模板对于后续的学习至关重要;好了,以上便是本文的全部内容,希望可以对你有所帮助,感谢阅读!