C++ 模板函数 知多少?

来源:互联网 发布:大数据分析报告案例 编辑:程序博客网 时间:2024/04/30 20:58

要讲模板函数,我们先来看个例子,假如老板要让你写一个排序函数,假定最开始用户的要求可能只是 int .那么你花了大把功夫写了一段代码:

void Sort(int* begin, int* end, bool(*cmp)(int, int))   //第三个参数是优先级函数,在这里我就简单用的冒泡排序。如果不会排序的请不要深究排序实现过程。{int* i, *j;for (i = begin; i != end; i++){int* max = i;for (j = i + 1; j != end; j++){if (cmp(*j, *max)) max = j;}int temp = *max;*max = *i;*i = temp;}}bool k(int a, int b){return a > b;}int main(){int arr[6] = { 1, 5, 3, 2, 6, 4 };Sort(arr, arr + 6, k);for (int i = 0; i < 6; i++) cout << arr[i] << " ";cout << endl;return 0;}

好,当你写完代码准备向老板邀功的时候,结果老板说:用户又提要求了,要求不仅可以对int 排序还可以对 double 排序。

然后你又屁颠屁颠跑去敲:

void Sort(double* begin, double* end, bool(*cmp)(double, double)){double* i, *j;for (i = begin; i != end; i++){double* max = i;for (j = i + 1; j != end; j++){if (cmp(*j, *max)) max = j;}double temp = *max;*max = *i;*i = temp;}}


嗯,你巧妙地利用了函数的重载实现了这个伟大的需求(额,貌似并不伟大)。当你又准备邀功的时候,老板说:哎呀不好意思,用户那边又要提要求了。他要求不仅排序要对int,double 还要可以对c风格字符串(char*)、c++风格字符串(string),自定义结构排序。哦对了,用户还想不一定是对数组排序也可以是容器。

这下你傻眼了:我靠,这要写多少个重载呀,这个时候你该怎么办呢? 好吧,我来告诉你:

                      用模板!!!

经过上面这个例子大家对模板这个语法糖的出现的必要性可能不存有怀疑了。(不然那么多重载谁来写啊。又麻烦,还容易出错)。


一.模板函数的定义

那么我们来看下下面这个简单的模板例子:

template<typename T>void Say(T a){cout << "这就是; " << a << endl;}int main(){Say(1);Say(2.3);Say('a');Say("爱情");return 0;}

在这里我们定义了一个模板函数,我们发现在主函数中我们可以对这个函数传 int, double ,char ,const char* 等等。并且能够将传进去的参数以正确的形式输出。

好,看到这里大家看到了模板函数的好处,那么我们怎么使用模板函数呢?

我们对于上面那个例子:我们先只是让这个Say 只能传入 int 类型,那么代码就是:

void Say(int a){cout << "这就是; " << a << endl;}

这个时候我们要想这个Say可以传入double,char等等类型的时候怎么办呢?我们想: 是不是能够将类型也作为一个参数?这样的话类型就可以由用户定呢?

模板出现就是源于这种思想,我们来看,在刚才的函数之前加上一句话,template<typename T>  。

这句话的意思是:

1.表示接下来这个函数是一个模板函数.

2.这个函数有一个类型参数T.

那么是不是我们函数的参数列表中的int  a 的 int 就可以被 T 这个类型参数去替换了?

template<typename T>  //这里的typename可以写为class,但是尽量使用typenamevoid Say(T a){cout << "这就是; " << a << endl;}


这样的话你再用一个字符去调用这个函数的时候 Say(‘c’).  那么模板自动就把这个 T  翻译成 char. 然后就生成一个对于char的函数.

//注意:这个函数原型是由编译器在遇到Say('a')之后才自动生成的.所以Say('a')调用的其实是下面这个函数。void Say(char a){cout << "这就是; " << a << endl;}

所以代码包含函数模板本身斌不会生成函数定义,它只是一个用于生成函数定义的方案。让我们去调用这个函数的时候,编译器才会对于我们给定的参数类型去生成对应的函数实例。


从上面这句话我们可以看出来我们调用模板函数的时候可以不用像实参一样给出类型参数。编译器会自动根据实参类型来判断。

类型参数可以不仅仅为一个可以多个。

template<typename T,typename F>void Func(T a,F b){}

而且在模板参数列表中除了可以定义类型参数,还可以定义普通的参数。不过如果是普通的参数那么在函数里这个变量为常量,并且调用函数的时候必须显式给出类型:

template<typename T,int num>void Func(T arr){for (int i = 0; i < num; i++)cout << arr[i] << " ";cout << endl;}int main(){int arr[5] = { 1, 2, 3, 4, 5 };Func<int*, 3>(arr);  //这里必须在<>内显式给出类型return 0;}

基本上模板函数的简单定义就是如此了,下面我们来看下使用模板函数的一些需要注意的地方吧:


1.模板形参不能为空。一但声明了模板函数就可以用模板函数的形参名声明类中的成员变量和成员函数,即可以在该函数中使用内置类型的地方都可以使用模板形参名。

2.对于模板参数列表中如果定义的是普通的参数,那么传入实参的时候只能给常量表达式。并且该参数作为常量在函数中存在。

3.变量覆盖问题:全局变量 < 模板参数列表的变量 < 函数参数列表的变量(函数内部的局部变量) . 左部的变量可以被右部的同名变量所覆盖。 

4.模板参数名在同一模板参数表中只能被使用一次,但是模板参数名可以在多个函数模板声明或定义之间被重复使用。

5.如同非模板函数一样函数模板也可以被声明为inline 或extern 应该把指示符放在模板参数表后面而不是在关键字template 前面。


好到了这里想必你已经能很好的解决老板留下给你的问题了吧。下面是代码:

template<typename T>void Sort(T begin, T end, bool(*cmp)(T, T)){T i, j;for (i = begin; i != end; i++){T max = i;for (j = i + 1; j != end; j++){if (cmp(j, max)) max = j;}auto temp = *max;*max = *i;*i = temp;}}bool Cmp1(double* a, double* b){return *a > *b;}bool Cmp2(vector<int>::iterator a, vector<int>::iterator b){return *a > *b;}int main(){double arr[6] = { 1, 5, 3, 2, 6, 4 };Sort(arr, arr + 6, Cmp1);                   //可以对数组排序vector<int> varr = { 1, 5, 3, 2, 6, 4 };Sort(varr.begin(), varr.end(), Cmp2);      //也可以对容器排序for (int i = 0; i < 6; i++) cout << arr[i] << " ";cout << endl;for (int i = 0; i < 6; i++) cout << varr[i] << " ";cout << endl;return 0;}

看过代码后其实你会发现对于我们的Sort函数的第三个参数(优先级函数)这样写感觉也不够重用,所以说C++引入了仿函数(函数对象)的概念。 详情请看———  仿函数详解


二.模板函数的特化(显式具体化)和显式实例化


话说上次你巧妙利用模板的特性快速的解决了用户的需求,老板对此对你大加赞赏。

这次,老板又找到你了,说有个用户想让你帮他们写一个加法函数(如果是字符串应当作为连接函数)。对于传进来的两个参数实现相加后作为返回值能够传出。

你想了想,这不简单吗?模板函数已经学会,这样的要求简直是小case。所以不一会儿你就写好了代码:

template<typename T>T void Add(T a, T b){return a + b;}

当你准备交给老板的时候你想了想,不对啊,如果传入的是 char*(c风格的字符串)怎么办呢?无法将两个char*相加呀!

所以在这里你需要将char*这个特殊的类型来单独处理!   为了解决这样的问题,模板函数给出了对应解决问题的方案————  显式具体化

显示具体化的语法是这样的:

//注意函数模板显式中是不存在类型参数的template<>    //这里template后面跟着的<>表示这是Add函数模板的一个实例化char* Add<char*>(char* a, char* b)    //这里的函数名后面跟了一个<> 里面填写的是特化的指定类型. {char* newStr = new char[strlen(a) + strlen(b) + 1];for (int i = 0; i < strlen(a); i++)newStr[i] = a[i];for (int i = 0; i < strlen(b); i++)newStr[i + strlen(a)] = b[i];newStr[strlen(a) + strlen(b)] = '\0';return newStr;}

这样的话你就交给老板的代码就是模板和显式具体化的整合:

// C++Test.cpp : 定义控制台应用程序的入口点。//#include "stdafx.h"#include <typeinfo>#include <string.h>#include <vector>using namespace std;template<typename T>T Add(T a, T b){return a + b;}//注意函数模板显式中是不存在类型参数的template<>    //这里template后面跟着的<>表示这是Add函数模板的一个实例化char* Add<char*>(char* a, char* b)    //这里的函数名后面跟了一个<> 里面填写的是特化的指定类型. {char* newStr = new char[strlen(a) + strlen(b) + 1];for (int i = 0; i < strlen(a); i++)newStr[i] = a[i];for (int i = 0; i < strlen(b); i++)newStr[i + strlen(a)] = b[i];newStr[strlen(a) + strlen(b)] = '\0';return newStr;}int main(){Add(1, 3);Add("123", "123");return 0;}

下面来看一下什么是显式实例化:

我们还是来看下上面那个加法的函数模板:

template<typename T>T void Add(T a, T b){return a + b;}

如果我们在源文件中这样去调用: Add(2,2.3).  会发生什么事情呢?

噢,有的人就可能会这样想:int 类型的可以隐式转化为 double 所以其实调用的是 Add<double>(2,2.3);

其实不是这样的,在函数模板的实参和形参匹配中是利用了实参推演的。意思就是说。调用Add(2,2.3)的时候,编译器会先到非模板函数中去找,发现找不到的时候就会到模板函数去找可以实例化为这样一个函数的模板  Add(int,double) .发现找不到,于是报错!!

是不是这样就没有解决办法呢? 其实是有的。我们可以显式去指定模板类型参数!!

Add<double>(2,2.3). 这样的话 编译器就不会进行实参推演,直接根据前面给出的double生成Add模板的double实例化。然后再将实参传入进去。这时候int就可以进行隐式转化了。

template<typename T>T Add(T a, T b){return a + b;}int main(){Add<double>(1, 2.3);    //这里叫做显式实例化。return 0;}

那么问题又来了。我们知道 string 和 char 是可以相加(在string库中重载了+运算符的),并且相加结果为string类型。

对于下面这个加法模板我们可以这样调用 Add(‘a’,string('bc')) 或者 Add(string("ab"),'c'). 

<pre name="code" class="cpp">template<typename T,typename F>___ Add(T a, F b){return a + b;}

大家可以看到上面这个模板,我把返回值空了下来。为什么呢?因为这个返回值并不是 T 或者是 F。而是 T + F 后应该得到的值。

比如:我这样调用的时候Add(‘a’,string('bc'))  对应的  T 为 char  ,F 为 string .那么返回值类型就为 F(string).相反如果我这样调用Add(string("ab"),'c'). 返回值类型就为T.

所以我们更希望我们的模板能智能地识别到应该返回的类型:所以有了后置返回类型的语法 

template<typename T,typename F>auto Add(T a, F b) -> decltype(a+b){return a + b;}


一般函数模板都是放在头文件申明中,而如果有特殊的类型需要让函数有特别的操作即可在源文件中进行显式具体化。这样的话调用该函数,编译器就会先到显示具体化中去找匹配函数。


那么编译器如何知道去哪儿找定义或者说,有多个可以使用的定义的时候优先选择哪个呢?

详情请看 ———————— 重载解析

1 0
原创粉丝点击