泛型编程:编写与类型无关的逻辑代码,是代码复用的一种手段。
模板是泛型编程的基础。
模板:函数模板和类模板。
模板是一个蓝图,它本身不是类或者函数,编译器用模板产生指定的类或者函数的特定类型版本,产生模板特定类型的过程称为模板实例化。
一、函数模板
函数模板参数:模板类型参数和非模板类型参数。
模板类型参数:
1.在模板的定义中,模板参数不能为空;
2.模板类型形参可作为类型说明符用在模板的任何地方,与内置类型和自定义类型完全相同。(注意:在模板作用域内)
3.模板形参的名字在同一模板形参列表中只能使用一次。
4.在函数模板的内部不能指定缺省的模板实参。(在类模板内可以)
5.所有模板形参前面都应该加上class或者typename关键字修饰。
非模板类型参数:
非模板类型参数是模板内定义的常量,在需要常量表达式时,可以使用非模板类型参数。
注意:浮点数和类对象是不允许作为非类型模板参数的。
模板实例化:
例如:Add(1,3);——>这个过程称为模板的实例化,也就是调用模板函数过程,通过函数的实参类型来确定模板类型参数。
模板被编译了两次:
①在编译时:也就是实例化之前,检查模板代码本身,查看是否出现语法错误;如:遗漏分号
②在实例化阶段:生成具体类型的函数,检查模板代码,查看是否所有的调用都有效。如:实例化类型不支持某些函数调用。
实例化:
①隐式的—->根据函数实参的类型去推演函数模板中的参数(实参推演)。
②显式的—->不考虑函数的实参类型
模板函数在(隐式的)实例化时,不会进行参数的隐式类型转换,而是会产生新的实例。
除了以下两种情况可以发生转化:
①const转换:可以将一个非const对象的引用或指针传递给一个const的引用(或指针)形参。
注意:const T a;—->假如这里的T为int *
,按照我们一般的想法则是替换:const int* a
; 这时的a为一个指向常量的指针,指向的内容不能修改,而它本身的指向却可以修改,这样想的话你就错了,在这里,T代表了一个整体,const 永远修饰的都是变量a,意思也就是说const T a;就类似于int* const a;是一个常指针,自己的指向不能修改,但是它指向的内容却可以改变。
②数组或函数到指针的转换:如果模板形参不是引用类型,则对数组或函数类型的实参应用常规指针转换。
数组实参可以转换为一个指向其首元素的指针,函数实参可转换为一个该函数类型的指针。
3.模板函数形参(T)—>的作用域是从模板参数列表之后开始,到模板声明或定义结束之前。
模板形参名字只能在模板参数列表到模板声明或定义的末尾之间使用,与任何其他名字一样,模板参数会隐藏外层作用域
中声明的相同名字。
遵循名字屏蔽规则;
由于参数名不能重用,所以一个模板参数名在一个模板参数列表中只能出现一次。
4.一个非模板函数可以与一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数;
template <typename T>
T FunTest2(T a, T b)
{
cout << "T FunTest2()" << endl;
return a + b;
}
int FunTest2(int a, int b)
{
cout << "int FunTest2()" << endl;
return a + b;
}
void Test2()
{
int a = 1;
int b = 2;
FunTest2<int>(a, b);//会调用实例化出函数
FunTest2(a, b);//调用全局的普通函数
}
5.显式指定一个空的模板实参列表,该语法告诉编译器只有模板才能来匹配这个调用,而且所有的模板参数都应该根据实参推演出来。
如果不是特别理解这句话的意思的话,那就看下我下面的例子吧,相信聪明的你一定可以明白的。
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
template<typename T>
T FunTest(const T& a, const T& b)
{
T return a + b;
}
int FunTest(int a, int b)
{
return a + b;
}
void Test1()
{
FunTest(1, 3);//会调用普通函数
FunTest<>(1, 3);//会调用模板函数
}
int main()
{
Test1();
return 0;
}
6.对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调动非模板函数,而不会从该模板产生出一个实例。
如果模板可以产生一个具有更好匹配的函数,那么将选择模板。
7.模板函数不允许自动类型转化(除了上面所述两种情况),但是普通函数支持自动类型转换。
让我们一起来看一个函数模板实例化的小例子:
template<typename T1, typename T2>
void FunTest1(const T1& a,const T2& b)
{
cout << "FunTest1()" << endl;
cout << typeid(a).name() << endl;
cout << typeid(b).name() << endl;
}
void Test1()
{
FunTest1<int>(1, '1');//虽然只是显式给出了第一个类型,但是编译器会根据实参的类型推演出第二个类型。
}
结果如下:
函数模板特化:
当通用模板不能处理一种类型或者处理后所得结果明显的不正确时,这时需要我们专门为了这种类型写出一个特化后的函数,
这样就可以处理特别类型了。
模板函数特化形式如下:
①关键字template后面接一对空的尖括号<>;
②后面跟上模板函数的返回值,接着是模板名加上一对尖括号,尖括号中指定这个特化定义的模板形参。
③函数形参表;
④函数体。
template<typename T>
int FunTest2(T p1, T p2)
{
if (p1 < p2)
{
return -1;
}
if (p1 > p2)
{
return 1;
}
return 0;
}
template <>//特化
int FunTest2<const char*>(const char* const pStr1, const char* const pStr2)
{
cout << "strcmp()" << endl;
return strcmp(pStr1, pStr2);
}
void Test2()
{
char *pStr1 = "1234";
char *pStr2 = "abcd";
cout << FunTest2(pStr1, pStr2) << endl;
}
特化的参数类型必须要与实参的类型严格一致,否则,编译器会通过参数类型推演一个模板类型,从而实例化一个函数。
示例如下:
template<typename T>
T compare(T t1,T t2)
{
cout << typeid(t1).name() << endl;
if (t1 < t2)
{
return t2;
}
else
return t1;
}
template<>
const char* compare<const char*>(const char* p1, const char* p2)
{
if (strcmp(p1, p2) > 0)
{
return p1;
}
return p2;
}
void Test2()
{
const char* p1 = "hello";
const char* p2 = "world";
cout << compare(p1, p2) << endl;//调特化后的版本
cout << compare((char*)p1, (char*)p2);//会调用实例化后的模板函数
}
函数模板特化小结:
①函数模板首先要存在;
②特化出的函数返回值及参数类型与函数模板的一模一样。
注意:当我们使用一个类类型的对象时,类定义必须是可用的,但是成员函数的定义不必已经出现,
因此,我们一般将类定义和函数的声明放置在头文件中,而普通函数和类的成员函数的定义放在源文件中。模板则是不同的,为了生成一个实例化版本,编译器需要掌握函数模板或类模板成员函数的定义。
因此,与非模板的代码不同,模板的头文件既包括声明,也包括定义。二、类模板
类模板是用来生成类的蓝图的。
1.与函数模板不同的是:
编译器不能为类模板推演模板参数类型,所以,为了使用类模板,我们必须在模板名后
的尖括号中提供额外的信息(显式模板实参列表)——->用来代替模板参数的模板实参列表。
特别的,如果一个类模板为其所有模板参数都提供了默认实参,且我们希望可以使用这些默认参数,就必须在模板名后跟一个空尖括号对。
2.类模板成员函数的实例化:
如果一个成员函数没有被使用,则它不会被实例化。
成员函数只有在被用到时才进行实例化,这一特性使得即使某种类型不能完全符合模板操作的要求,
我们仍然能用该类型实例化类。
3.在类代码内简化模板类名的使用:
当我们使用一个类模板时必须提供模板实参,但这一规则有一个例外,在类模板自己的作用域中,我们可以直接使用模板名而不提供实参。
4.模板类型别名:
由于模板不是一个类型,所以我们不能定义一个typedef。引用一个模板。
即就是,无法定义一个typedef引用一个Date(这里的Date是一个模板类)。
但是,新标准允许我们为类模板定义一个别名.
template<typename T>using twin = pair<T>;
twin<string> authors;//authors是一个pair<string, string>.
就像使用类模板一样,当我们使用twin时,需要指出希望使用哪种特定类型的twin。
5.当我们希望通知编译器一个名字表示类型时,必须用关键字typename,而不能使用class。
例如,当编译器遇到如下情况时:
T::size_type *p
编译器需要知道我们是正在定义一个名为P的变量还是还是将一个名为size_type的static数据成员与一个变量p相乘。
三、模板的优缺点小结:
优点:模板复用了代码,节省资源,更快的迭代开发,c++的标准模板库(STL)因此而产生。
增强了代码的灵活型。
缺点:
模板让代码变得凌乱复杂,不易维护,编译代码时间变长。
出现模板编译错误时,错误信息非常凌乱,不易定位错误。