我们都学习和使用过模板,而这篇文章我们来将一些更深入的知识。在此之前,我们在使用C++编程时可以看到模板是随处可见的,它能支持泛型编程。模板包括函数模板和类模板,我们有的人可能会说是模板函数和模板类,但严格讲这样说是错误的。而在我们实际使用中,类模板用的场景是比函数模板多的,如STL中vector,list等都是类模板,而算法中sort,find等是函数模板。
非类型模板参数
模板参数分为类型模板形参,非类型形参。
类型形参:出现在模板参数列表中,跟在class或typename关键字之后的参数类型名称。(我们之前一直使用的)
非类型形参:用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。
举个例子:
我们想要创建一个大小固定,可以存储不同类型元素的数组类,我们可以这样写:
#include<iostream>
using namespace std;
#define N 10template<class T>
class Array
{
private:int _a[N];
};
int main()
{Array<int> arr1;Array<int> arr2;return 0;
}
这时实例化出来的arr1,arr2都是大小为10的定长数组。但我们假如说想改变一个数组的长度呢?例如我们想将数组长度改为20,怎么办呢?有同学说我们可以将N define为20呀。但是假如我们想要一个数组长为10,另一个长为20呢?
这时候我们的非类型模板参数就有作用了,我们可以将Array定义为下面的形式:
#include<iostream>
using namespace std;//这里的N叫做非类型模板参数,它是一个常量
template<class T, size_t N>
class Array
{
private:int _a[N];
};
int main()
{Array<int, 10> arr1;Array<int, 20> arr2;int n = 10;Array<int, n> arr3;//error 这里非类型模板参数是常量,这里会报错return 0;
}
对于非类型模板参数的使用场景,STL中的deque和array容器中都用了。deque中的非类型模板参数用来传一个一个常量来控制buff的大小。
我们下面介绍一下array容器:
C++11支持array,它的结构类似于vector,只不过vector是动态数组,而array是静态的。它不提供头插,头插,尾插,尾删,任意位置插入删除这种操作,因为它根本不存在这种说法,而且它可以直接使用operator[ ] 访问修改任意位置的数据。
array的缺陷:
我们不推荐使用array,array底层是在栈上开辟空间的,而栈空间又是很有限的,例如:在32位的linux下栈空间只有8M,所以一般开大空间时极不推荐使用array,相比之下vector就很有优势。而且在知道要开多大空间的情况下,vector 也可以通过reserve一次性开好空间,在后续的使用中还可以自动增容,而array空间是固定的,不灵活。所以我们一般不使用array。
注意:
- 非类型模板参数只允许使用整形家族(int,short,long,long long,char),而浮点型,类对象,字符串是不允许作为非类型模板参数的
- 非类型模板参数需要在编译的时候就确认结果,因为编译器在编译阶段就需要根据传入的非类型模板参数生成对应的类或函数
模板的特化
我们先举个例子,理解一下模板的特化,看下面的代码:
template<class T>
bool IsEqual(const T& left, const T& right)
{ return left == right;
}
int main()
{cout << IsEqual(1, 2) << endl;char p1[] = "hello";char p2[] = "world";cout << IsEqual(p1, p2) << endl;return 0;
}
我们实现了IsEqual函数用来判断两个参数是否相等。我们总共调用了两次,第一次比较的是两个整数,是没问题的;而第二次实际上是有问题的,我们本意是想比较两个字符串是否相等,可是我们仔细看看:我们实际上比较的p1,p2两个指针。我们可能会想先判断一下传过来的参数的类型是什么,再进行不同的操作,就像下面这样:
template<class T>
bool IsEqual(const T& left, const T& right)
{if(T == char*)//error,C++中不支持类型比较{return *left==*right;}else{...}return left == right;
}
int main()
{cout << IsEqual(1, 2) << endl;//okchar* p1 = "hello";char* p2 = "world";cout << IsEqual(p1, p2) << endl;//errreturn 0;
}
但是很可惜,C++中不支持类型比较,所以上面写的是错误的。
那么这里就需要我们的模板特化出手了,模板特化的目的就是在原模板类的基础上,针对某些类型进行特殊化处理。模板特化又分为函数模板特化和类模板特化。
函数模板的特化
- 首先必须有一个基础的函数模板
- 关键字template后面接一对空的尖括号<>
- 函数名后跟一对<>,尖括号内指定需要特化的类型
- 函数形参表必须要和函数模板的基础参数类型完全相同,如果不同,编译器可能会报一些奇怪的错误
我们上面的代码就可以改为:
#include<iostream>
using namespace std;template<class T>
bool IsEqual(const T& left, const T& right)
{return left == right;
}template<>
bool IsEqual<const char *&>(const char*& left,const char*& right)
{return strcmp(left, right)==0;
}
int main()
{cout << IsEqual(1, 2) << endl;const char* p1 = "hello";const char* p2 = "hello";cout << IsEqual(p1, p2) << endl;return 0;
}
说明:其实一般情况下,如果函数模板遇到不能处理或者处理有误的类型,可以直接将该函数直接给出,这叫做模板的匹配原则:有现成的完全匹配的,就直接调用,没有现成调用的,实例化模板生成。上面的代码就可以这样写:
#include<iostream>
using namespace std;template<class T>
bool IsEqual(const T& left, const T& right)
{return left == right;
}bool IsEqual(const char*& left,const char*& right)
{return strcmp(left, right)==0;
}
int main()
{cout << IsEqual(1, 2) << endl;const char* p1 = "hello";const char* p2 = "hello";cout << IsEqual(p1, p2) << endl;return 0;
}
对于模板匹配原则和函数模板特化,两者底层没有任何差别,如果能使用函数模板特化的时候更推荐使用函数模板特化。
类模板特化
1.全特化
就是将模板参数列表中所有参数确定化,例如:
#include<iostream>
using namespace std;template<class T1,class T2>
class A
{
public:A(){cout << "T" << endl;}
private:T1 a1;T2 a2;
};template<>
class A<int,int>
{
public:A(){cout << "int" << endl;}
private:
};int main()
{A<double, double>a1;A<int, int>a2;return 0;
}
2.偏特化
也叫半特化:即对模板参数进行一定的确定化,例如:
template<class T1>//第二个参数是int类型就会调用这个
class A<T1, int>
{
public:A(){cout << "T1,int" << endl;}
private:
};template<class T2>//第一个参数是int类型就会调用这个
class A<int, T2>
{
public:A(){cout << "int,T2" << endl;}
private:
};template<class T1,class T2>//两个参数都是指针类型就会调用这个
class A<T1*, T2*>
{
public:A(){cout << "T1*,T2*" << endl;}
private:
};template<class T1, class T2>//两个参数都是引用类型就会调用这个
class A<T1&, T2&>
{
public:A(){cout << "T1&,T2&" << endl;}
private:
};template<class T1, class T2>//第一个参数是指针类型,第二个参数是引用类型就会调用这个
class A<T1*, T2&>
{
public:A(){cout << "T1*,T2&" << endl;}
private:
};int main()
{A<double, int>a;A<int, char>b;A<double*, int*>c;A<char&, int&>d;A<double*, char&>e;return 0;
}
上面这几种特化版本都是偏特化。
偏特化并不仅仅是指特化,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本,例如我们可以限制他的类型是引用,指针之类的。
关于特化的场景我们等到哈希表的时候会给大家介绍。
模板分离编译
我们在工作的时候写的项目都会由若干个源文件共同实现,每个源文件独立编译生成目标文件,最后将所有目标我呢见链接起来形成单一的可执行文件的过程称为分离编译模式。
模板的分离编译
在前面我们模拟实现string,vector,list等STL容器的时候,都没有将声明和定义分开。我们实际上是习惯将声明放在头文件中,将定义放在.cpp文件中的。我们没有分离的原因就是C++的模板不支持分离编译。
我们以这段代码为例:
//Func.h代码:
#pragma once
#include<iostream>
using namespace std;void Print();template<class T>
void F(T a);//Func.cpp代码
#include"Func.h"void Print()
{cout << "print" << endl;
}template<class T>
void F(T a)
{cout << "F(T a)" << endl;
}//test.cpp代码
#include"Func.h"int main()
{Print();F(10);//这里编不过return 0;
}
不支持分离编译的原因:
我们都知道:程序的编译过程分为:预处理,编译,汇编,链接四个步骤
其中预处理会做的事情就是:头文件展开,宏替换,条件编译,去掉注释,在linux环境下就生成了.i文件
编译:检查语法错误后,生成汇编代码,在linux环境下生成.s文件
汇编:将汇编代码转成二进制机器码,在linux环境下生成.o文件
链接:会把.o文件里F或Print这样没有地址的地方,用被修饰过的函数名去Func.o中的符号表找对应的地址,然后填上地址就像上图中的地址一样。然后再把目标文件合并成可执行程序。
而问题就出现在链接的时候,编译器拿着我们的函数名去符号表中找时,能找到Print()函数,但是却找不到F()函数,这是因为在编译阶段,Print()函数有定义,可以生成。但是F()函数是一个函数模板,他不能生成,因为我们不知道T是什么类型。模板其实是调用时生成的。
如何解决
一、
这种方法实际上是不可行的,就是让编译器在编译的时候去各个地方查找实例化,例如我们在Func.i中看到一个模板,我们就去Test.i中找实例化,但是这样的话,如果是一个大项目的化,有几十几百个文件,对于编译器的实现就复杂了。所以实际在链接前,各文件间是不会交互的
二、
显示指定实例化,编译器看到后就知道要把这个模板参数T实例化成什么类型。但是这样做的问题也很大:就是我换个类型就又链接不上了,那我们就只能使用一种类型就实例化一次,这样就很麻烦。
三、
最后这种方法就很粗暴,STL中用的也是这种方法,就是不分离编译,声明和定义都放在一个.h文件里。这样的话,.h文件中就包含了模板的定义,就不需要链接的时候去查找了,在编译的阶段就能直接填地址了。而我们在项目中一般把这种文件命名为后缀为.hpp的文件,也就是说它即是.h文件,又是.cpp文件。
同样我们的类模板也不支持分离编译,最好的办法也是不分离,写在同一个文件中。
我在这里还要在解释我上文中的一句话:模板其实是调用时实例化生成的。我们在一个类模板或者函数模板中,假如写出了语法错误,假如我们没有实例化运行的话,编译器是检查不出错误的,这就是因为模板是调用时实例化的,没有实例化的时候,编译器不会去检查函数模板或类模板内部的语法错误。
总结
模板优点:
1.模板复用了代码,节约资源,更快的迭代开发,有了模板才有了C++的标准模板库STL的诞生
2.增强了代码的灵活性
模板缺点:
1.模板会导致代码膨胀问题,也会导致编译时间变长
2.假如我们使用模板时出现错误,编译器提示的错误信息会非常凌乱,而且准确度不高,我们不能盲目地相信模板的报错。可能只是一点小错误,最后却报出了一大堆错误,这时候我们要优先看第一个错误。
以上就是本章的全部内容,谢谢大家!