C++的模板是什么?有什么用?如果你想知道问题的答案,那么看这篇博客就对了,在这篇博客中,我们将探讨泛型编程,C++模板的具体内容
模板概念
在了解泛型编程之前,我们先回顾一下生活常识,就是在质量差不多的情况下,手工产品一般都是比具有规模性统一生产的产品是要贵一些的。因为能规模性生产的东西一般都是有模型或模具的,工厂中的机器只要按照这个模具不停的生产就可以了,而人工不一样,人工的速度哪能比得上机器呢?完成同样的工作,人工就要花费远高于机器的成本,可见这种具有大量重复性的工作,还是得交给机器来完成
这其实是和我们编程类似,比如我们想写一个交换函数swap(),要是考虑到各种类型的话,就先拿内置类型,char, int, double等来说,交换char类型的要写一个,交换int类型的要写一个等等,我们就要实现很多个函数,但这些函数的代码基本都是一样的,只是数据类型不同罢了,这样具有重复性的代码编写,应该交给编译器去编写
void Swap(char& a, char& b) { char tmp = a; a = b; b = tmp; } void Swap(int& a, int& b) { int tmp = a; a = b; b = tmp; } void Swap(double& a, double& b) { double tmp = a; a = b; b = tmp; } //.......很多,剩下的我就不写了 int main() { int x = 10, y = 20; Swap(x, y); cout << x << " " << y << endl; return 0; }
函数模板
那怎样让编译器去替我们做这样的重复性工作呢?首先我们同样先编写上面这个交换函数的代码,但是这段代码没有数据类型,也并不是说真的没有数据类型,而是我们用一个符号来代替这个数据类型,假设我们使用符号T,那么就可以把上面代码中的char, int, double 全给换成T,然后我们就只管调用就行,至于T具体是什么类型,到时候编译器根据传过来的参数自己推测是什么类型,而这有点像模板一样的东西其实就是C++中的函数模板
当然这个符号T可不是随便就搞出来的,我们得先声明一下,符号T是一个模板符号,具体怎么操作呢? 我们先使用模板的关键字template,用这个模板关键字来创建出一个符号T,这个符号T就是用来替代具体代码中的数据类型
template<typename T> //注意这里可以写成 template<class T> //也就是说,typename 和 class 是等价的 void Swap(T& a, T& b) { T tmp = a; a = b; b = tmp; }
就这样我们就完成了一个交换函数模板的创建,接下来我们趁热打铁,再把函数模板巩固一下,接下来我们写一个add函数的模板
template<class T> T Add(T& a, T& b) { return a + b; } int main() { int tmp1 = 10, tmp2 = 20; double tmp3 = 1.1 ,tmp4 = 2.2; cout << Add(tmp1, tmp2) << endl; cout << Add(tmp1, tmp3) << endl; return 0; }
这段代码看着没有什么毛病,但是只有第一次调用Add可以编译通过,而第二次调用Add是没有办法编译通过的,这是什么情况导致的呢?
仔细看可以发现,我们第一次调用Add函数传过去的参数是两个int类型的,而第二次调用Add函数传过去的参数一个是int 类型,另一个是double类型,要了解这个原因,我们就要先了解一下模板的是如何进行工作的,我们拿上面的交换函数来说
从上面推演的过程中我们可以得知,假设我们传过去的是int类型的参数,在读取第一个参数时,T就被推演成了int类型,并且T被绑定成int类型
我们在定义add函数时,两个参数使用的都是数据类型T,因此这两个参数的类型应该保持一致,如果第一个参数是int类型,那么T被推演并绑定成int类型,第二个参数是double类型,而T又被推演成double类型,但是T已经是int类型了,这就与之前推演的结果产生冲突,从而导致编译失败
我们要想解决这个问题,可以使用多个模板参数类型,如下代码即可解决
template<typename T1, typename T2> T1 add(const T1& value_1, const T2& value_2) { return value_1 + value_2; } int main() { int a = 10; double b = 10.5; cout << add(a, b) << endl; return 0; }
在上述代码中,你传过去一个int 和 一个double ,T1会被推演并绑定成int,T2会被推演并绑定成double,这样编译就不会产生冲突
但是这样吧,返回值你又该规定成什么类型呢?是返回T1类型,还是返回T2类型?
所以像add这种函数就老老实实使用同类型参数进行操作,不要搞一些没必要的骚操作
从上面的过程中,我们可以体会到,实际上函数模板只是一张蓝图,就像你定义结构体或者定义一个类一样,你不创建一个相关的对象,是没有实体的,函数模板同样如此,你没有进行实例化之前,编译器是不会创建这个函数的
显示实例化与隐式实例化
我们就先了解什么是隐式实例化,其实我们上述过程中,举的各种例子使用的都是隐式实例化,因为我们在使用函数模板时,我们自己没有指定模板参数T的类型,而是靠编译器自己去推演,这个由编译器自行推演,并实例化出一个相关类型的函数就是隐式实例化
那么显示实例化就是我们明确给出了T的类型,不需要你编译器去给我推演,你只要根据我提供的类型,把这个函数给实例化出来就行,我们来看一下显示实例化的使用
显式实例化:在函数名后的<>中指定模板参数的实际类型
template<typename T> T add(const T& value_1, const T& value_2) { return value_1 + value_2; } int main() { int a = 10; int b = 20; //注意在调用这个函数时,这里我明确给出了T的类型为int cout << add<int>(a, b) << endl; return 0; }
一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数
template<typename T> T add(const T& value_1, const T& value_2) { return value_1 + value_2; } int add(int v1, int v2) { return v1 + v2; } int main() { int a = 10; int b = 20; cout << add(a,b) << endl; //这个是调用我们自己定义的add函数 cout << add<int>(a, b) << endl;//这个是调用函数模板推演出的函数 return 0; }
对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板
在上面的代码中,函数模板推演出的函数和我们自定义的函数一模一样,那么编译器就会优先调用我们自定义的函数
但是如果我们传过去一个int类型和一个double类型,而函数模板又有两个模板参数类型,那么在调用我们自定义的函数时,需要将double强转成int类型,而使用函数模板,可以将第一个T推演并绑定成int,第二个T推演并绑定成double,这个过程显然是函数模板推演出的函数更好的匹配我们传过去的函数,因此这里会优先调用函数模板推演出的函数
template<typename T1, typename T2> T1 add(const T1& value_1, const T2& value_2) { return value_1 + value_2; } int add(int v1, int v2) { return v1 + v2; } int main() { int a = 10; double b = 20.5; cout << add(a,b) << endl; //编译器会优先调用函数模板推演出的函数 return 0; }
模板不支持声明和定义分离
不同于普通的函数,模板是不支持声明和定义分离的,因为模板的实例化过程是在编译的阶段完成的,也就是说在编译阶段,模板的定义和声明必须处于同一个文件中
//假设我们有一个头文件 `template.h`,其中包含一个模板函数的声明: // template.h template <typename T> void foo(T t); //我们还有一个源文件 `main.cpp`,其中包含了对模板函数的调用: // main.cpp #include "template.h" int main() { int i = 10; foo(i); return 0; } //现在,如果我们把模板函数的定义放在一个单独的源文件 `template.cpp` 中: // template.cpp template <typename T> void foo(T t) { // do something }
上面这段代码是无法编译通过的,因为编译器在编译main.cpp时,虽然知道该函数模板的声明,但是在main.cpp中没有该函数模板的定义,定义是在template.cpp文件中,于是编译器只能寄希望于链接阶段,希望能够在链接阶段找到main.cpp中引用的模板函数
但是,当编译器在编译template.cpp文件时,函数模板的定义确实在这里,不过模板的特性就是不使用是绝对不会实例化的,编译器在template.cpp中并没有找到对该函数模板的调用和传参,因为对函数模板的调用和传参在main.cpp文件中,那么编译器就不会把template.cpp中的函数模板给实例化了,那就意味着这个函数不会被载入到函数映射表,意味着在最终的链接阶段,main.cpp仍然无法找到template.cpp中函数模板的定义,然后就会导致编译不通过,程序报错
这里讲得简略,想对这个过程有详细了解的可以看下面这篇博客
http://blog.csdn.net/pongba/article/details/19130
因此我们在编写模板时,通常要求模板的声明和定义放在同一个头文件中
类模板
类模板并不是一个真正的类,只有其被实例化出来才能是一个具体的类,也就是说类模板其实就是类的一个蓝图,但是不同于函数模板,类模板在实例化成一个类时,需要我们使用<>给出明确的参数类型,比如一个栈类,如果我们想将栈中存放int类型的数据,就要给出明确的int类型,stack st; 这样编译器会实例化出一个数据类型为int的栈类,并创建一个该类的对象st
类模板的使用,极大提高了代码的可重复利用率,C++中的stl标准库都是采用类模板的写法,让string,vector,stack,queue等等能够嵌套各种数据类型
类模板的成员函数如果要定义在类外的话,必须要加上关键字template和相应的参数列表
需要注意的时,类模板的成员函数在未使用的情况下是没有相关的代码的
//这里就不包含头文件了 template <typename T> class test{ public: void test_fun(); private: vector<T> v; }; //成员函数定义在类外,需要这样定义,假设是int类型 void test<int>:: test_fun() { //do something }
非类型模板参数
模板参数不仅可以作为类型来使用,同时还存在非类型模板参数,非类型模板参数不再表示一个类型,而是来指定一个数值,但是需要注意,非类型模板参数指定的数值只能是整形
不能是浮点数,字符和字符串
看概念不好理解,举个例子就能明白了
using namespace std; template<unsigned N = 20> class test { public: test() :n(N) { cout << n << endl; } private: int n = 10; }; int main() { test<-2> p; return 0; }
上面代码的结果为-2,需要注意在声明非类型模板参数时,这个参数类型只能使用整形的参数类型名,例如上面我们就使用了unsigned,还可以使用int,long等
using namespace std; template<unsigned n, unsigned m> bool compare(const char(&p1)[n], const char(&p2)[m]) { cout << n << " " << m; return strcmp(p1, p2); } int main() { compare("hello", "world"); return 0; }
上面的代码中,p1 和 p2是对两个字符串的引用,对数组的引用规定n的值要和被引用对象的大小一致,但是我们传的字符串是不确定大小的,这个时候就可以使用非类型模板参数,编译器会根据传过来字符串的大小,自动推出n和m的大小,实现p1和p2对传过来的两个字符串的引用
模板的特化
什么是模板的特化呢?模板的特化也分为两种,分别是函数模板特化和类模板特化,接下来通过一个例子看看什么是模板特化
template<class T> bool Less(T left, T right) { return left < right; } int main() { int a = 10; int b = 20; cout << Less(a, b) << endl; int* p1 = new int(10); int* p2 = new int(20); cout << Less(p1, p2) << endl; delete p1, p2; return 0; }
如上面的代码,我们要写一个比较大小的函数,我们传int类型,double类型,或是其他支持">"重载符的自定义类型,都可以用这个函数来比较大小
但是当我们传指针过去呢?Less函数的T会被推演成某个类型的指针,然后会拿指针的地址来进行大小比较,而不是拿指针指向的数据比较,这没有达到我们的预期
像这样遇到模板无法正常处理的特殊类型时,就需要对模板进行特化。即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式,以达到我们预期的效果
函数模板特化
函数模板特化的处理步骤
1. 必须要先有一个基础的函数模板
2. 关键字template后面接一对空的尖括号<>
3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型
4. 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误
接下来演示一下,对上面给出的函数模板进行指针类型的特化处理
template<class T> bool Less(T left, T right) { return left < right; } //对int指针类型进行特化处理 template<> bool Less<int*>(int* left, int* right) { return *left < *right; } int main() { int a = 10; int b = 20; cout << Less(a, b) << endl; int* p1 = new int(10); int* p2 = new int(20); cout << Less(p1, p2) << endl; delete p1, p2; return 0; }
上面就是对int指针类型的特化处理,不过这样其实实用性并不是很大,我们完全可以用函数重载来代替,而且比特化还简洁一些,因此函数模板不建议进行特化处理
类模板特化
全特化
全特化即是将模板参数列表中所有的参数都确定化
template<class T1, class T2> class test { public: test(T1 tmp1 = 0, T2 tmp2 = 0):_value1(tmp1), _value2(tmp2) { cout << _value1 << " " << _value2 << endl; } private: T1 _value1; T2 _value2; }; template<> class test<int, char> { public: test(int tmp1 = 0, char tmp2 = 0) :_value1(tmp1), _value2(tmp2) { cout << _value1 << " " << _value2 << endl; } private: int _value1; char _value2; }; int main() { test<int, int> p1(10, 20); test<int, char> p2(10, 'a'); return 0; }
上面的实例代码就包含了对类模板的全特化,我们把类模板的参数全部确定了
对于p1的调用,大家肯定都知道是调用基础类模板,因为两个模板类型相同
那么请猜一猜对于p2的调用,编译器会调用哪个函数呢?好像基础类模板和全特化类模板都符合调用条件,这就引出了调用匹配的问题
这里编译器会选择调用全特化类模板,可以想象编译器也是一个懒汉,如果编译器选择调用基础类模板,那么它还要分别推出T1和T2的类型,而p2的参数和全特化类模板的参数一样,那干嘛还费劲去推呢?直接调用全特化类模板
偏特化
偏特化是任何针对模版参数进一步进行条件限制设计的特化版本
这就导致偏特化有两种特化类型
1.对基础类模板的部分模板参数进行特化
template<class T1, class T2> class test { public: test(T1 tmp1 = 0, T2 tmp2 = 0):_value1(tmp1), _value2(tmp2) { cout << _value1 << " " << _value2 << endl; } private: T1 _value1; T2 _value2; }; //这里对基础类的第二个参数进行部分特化,将第二个参数特化为char类型 template<class T1> class test<T1, char> { public: test(T1 tmp1 = 0, char tmp2 = 0) :_value1(tmp1), _value2(tmp2) { cout << _value1 << " " << _value2 << endl; } private: T1 _value1; char _value2; }; int main() { test<int, int> p1(10, 20); test<int, char> p2(10, 'a'); return 0; }
上面的代码实例就是对基础类进行部分特化,将第二个参数给特化成char类型
2.参数更进一步的限制
偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本,看下面的实例代码
template<class T1, class T2> class test { public: test(T1 tmp1 = 0, T2 tmp2 = 0):_value1(tmp1), _value2(tmp2) { cout << _value1 << " " << _value2 << endl; } private: T1 _value1; T2 _value2; }; //将两个参数类型偏特化为指针类型 template<typename T1, typename T2> class test<T1*, T2*> { public: test(T1 tmp1 = 0, T2 tmp2 = 0) :_value1(tmp1), _value2(tmp2) { cout << _value1 << " " << _value2 << endl; } private: T1 _value1; T2 _value2; }; int main() { test<int, int> p1(10, 20); //只有指明参数类型为指针,才能够调用指针偏特化 test<int*, char*> p2(10, 'a'); return 0; }
将两个模板参数类型偏特化为指针类型,其实就相当于对参数进行了限值
只有在创建类的对象时明确是指针类型,才会调用到指针类型的偏特化