当我们在编写一个通用的加法时,首先我们会想到函数重载,就像下面这段代码
int Add(const int left,const int right) { return left + right; } float Add(const float left,const float right) { return left + right; } int main() { cout<<Add(1,2)<<endl; cout<<Add((float)1.2,(float)2.0)<<endl; system("pause"); return 0; }但是当函数需要的类型多了,就要不断的重载不同类型重新实现它。这样做存在一些问题:
1、因为除类型外所有函数的函数体都相同,所以代码复用率不高。
2、只有返回值不同不能重载。
3、只要有新类型出现,就要重新添加对应函数。
4、一个方法有问题,所有方法都有问题,不好维护。
那我们考虑是否有别的办法,使用公共基类,将通用的代码放在公共的基类中。但是这是借助公共基类来编写通用代码,将失去类型检查的优点;而且对于以后实现的许多类,都必须继承自某个特定的基类,代码维护更加困难,速度也比较慢。
并且函数重载和公共基类都需要我们自己来写容易出错。
那么我们还可以使用宏函数哈哈但是宏函数不进行参数类型检测,安全性不高。
所以,综上所述 我们可以采用的最好的办法就是泛型编程了。
什么是泛型编程呢,泛型编程就是编写与类型无关的逻辑代码,是代码复用的一种手段。模板是泛型编程的基础。模板中包含了函数模板和类模板。
函数模板:代表了一个函数家族,该函数与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。但模板本身不是一个类/函数,编译器用模板产生指定的类或函数。
下面来看一个模板实例
注:这段代码中模板被编译了两次:
1、实例化之前,检查模板代码本身,查看是否出现语法错误,如:遗漏分号
2、在实例化期间,检查模板代码,查看是否所有的调用都有效,如:实例化类型不支持某些函数调用
实参推演:从函数实参确定模板形参类型和值的过程称为模板实参推断,多个类型形参的实参必须完全匹配。
类型形参转换:一般不会转换实参以匹配已有的实例化,相反会产生新的实例。
如:cout<<Add(1,/*(int)*/'1')<<endl;//必须有带两个参数的模板,会产生新的实例:int Add(int left,char right)
编译器只会执行两种转换:
1、const转换:接收const引用或者const指针的函数可以分别用非const对象的引用或者指针来调用
template <typename T> void FunTest(const T& a)//这里的const修饰a,表示a不能被修改,a的类型为int* const { //const int* p ;const修饰*p //int* const p ;const修饰p //int b = 5; //a = &b;//a被const修饰,a相当于int*类型,它的指向不能被修改 *a = 20;//解引用不会出错,因为a的内容可以被修改 cout<<*a<<endl; } int main() { int a = 10;//这里的a没有用const修饰,可以用a调用FunTest(const T& a)即可以转化 FunTest(&a); system("pause"); return 0; }
2、数组或函数到指针的转换:如果模板形参不是引用类型,则对数组或函数类型的实参应用常规指针转换。数组实参将当做指向其第一个元素的指针,函数实参当做指向函数类型的指针。
void Fun() { cout<<"Fun()"<<endl; } template<typename T> void FunTest(T p) { cout<<typeid(p).name()<<endl;//数组实参当做指向其第一个元素的指针,p为int*类型 //(*p)();//p为void (__cdecl*)(void)类型,函数指针调用函数 p();//同(*p)()一样 cout<<"FunTest()"<<endl; } int main() { //int arr[10] = {0}; //FunTest(arr); FunTest(Fun); system("pause"); return 0; }
模板参数:
函数模板有两种类型参数:模板参数和调用参数。模板形参包括类型形参和非类型形参。
//模板形参名字只能在模板形参之后到模板声明或定义的末尾之间使用,遵循名字屏蔽规则 typedef int T; T g_a; template<typename T> void FunTest(T p) { T a; cout<<typeid(a).name()<<endl;//int } int main() { FunTest(1);//优先用模板参数列表中的T cout<<typeid(g_a).name()<<endl;//int system("pause"); return 0; } 模板形参的名字在同一模板形参列表中只能使用一次,template<typename T,typename T>会报错重定义参数T所有模板形参前面必须加上class或者typename关键字修饰。
在函数模板的内部不能指定缺省的模板实参。
【非模板类型参数】
template<class T, int N> //void FunTest(T array)-->用FunTest<int[3],3>(a)调用; void FunTest(T (&array) [N]) { int idx = 0; for(idx = 0; idx < N; idx++) { array[idx] = 0; } } int main() { int a[3]; float b[5]; FunTest(b); FunTest(a); //FunTest<int,3>(a);//相当于FunTest(a) system("pause"); return 0; }
模板形参一些说明:
1、模板形参表使用<>括起来。
2、和函数参数表一样,跟多个参数时必须用逗号隔开,类型可以相同也可以不相同。
3、定义模板函数时模板形参表不能为空。
4、模板形参可以是类型形参,也可以是非类型新参,类型形参跟在class和typename后。
5、模板类型形参可作为类型说明符用在模板中的任何地方,与内置类型或自定义类型使用方法完全相同,可用于指定函数形参类型、返回值、局部变量和强制类型转换。
6、模板形参表中,class和typename具有相同的含义,可以互换,使用typename更加直观。但关键字typename是作为C++标准加入到C++中的,旧的编译器可能不支持。
模板函数重载及模板函数特化:
【重载说明】函数的所有重载版本的声明都应该位于该函数被调用位置之前。
1、一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数。
2、对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调动非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数,那么将选择模板。
3、显式指定一个空的模板实参列表,该语法告诉编译器只有模板才能来匹配这个调用,而且所有的模板参数都应该根据实参演绎出来。
4、模板函数不允许自动类型转换,但普通函数可以进行自动类型转换。
///模板重载// template <typename T> T Max(const T& left,const T& right) { cout<<typeid(left).name()<<endl; return (left > right) ? left : right; } template <typename T> T Max(const T& left,const T& right,const T& mid) { T temp = Max(left,right); return (temp>mid) ? temp : mid; } //const char* Max(char* const left, char* const right) //{ // if(strcmp(left,right)>0) // return left; // return right; //} int main() { cout<<Max(1,2,3)<<endl; cout<<Max<int>(2,4.5)<<endl; cout<<Max(2.4,4.6)<<endl; char* p1 = "hello"; char* p2 = "world"; cout<<Max(p1,p2)<<endl; system("pause"); return 0; }
上面这段代码就是模板函数的重载,但是比较两个字符串会出错,出错的原因如下图:
模板总结:
【优点】模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生。增强了代码的灵活性。【缺点】模板让代码变得凌乱复杂,不易维护,编译代码时间变长。出现模板编译错误时,错误信息非常凌乱,不易定位错误。
