大家好,在这一小节中我们来学习一下函数模版, 有同学看到这个标题呢,可能会觉得挺奇怪的, 我们已经在计算概论或是一些先前的课程当中呢 对于函数这个概念呢烂熟于心了, 什么时候又冒出一个叫做所谓模版的概念, 那么其实看到模版这个词呢,你就应该感到窃喜了, 因为对于大家学习呢,你已经进入到了一个更为高阶的阶段, 也就是说我们通过模版的使用就正式开始了所谓的叫做 泛型程序设计这样的一个模块的学习。 那么所谓泛型程序设计呢,它英文就叫做这Generic Programming, 实际上所谓的泛型程序设计就是指我的算法在实现的时候 并不会具体的要求操作的数据的类型, 啊我不去指定相应的这个数据类型, 那么所谓泛型呢,你可以认为说是算法实现一遍, 但是它可以适用于多种数据结构, 既然是泛化的,那么它并没有具体地指明某一个 数据类型只适用于我现在当前设计的这个算法,我这个算法呢是一个普适性的, 那么有了这样一个泛型程序设计呢,它的好处在于可以 非常大的程度上去减少重复代码的编写, 那么一般来说一个资深的程序员相对于那些刚刚 入行的这个新手来讲就在于他是否能够去编写大量的模板 利用这个模板去实现这样泛型的这样程序设计, 那么我们通常讲呢模板分为两类,一个呢叫做函数模板,一个叫做类模板。 那么泛型程序设计呢是我们可以认为跟之前所讲到的 这个叠加序设计当中,这个抽象,封装,继承和多态 这样四大类可以并体的一个非常具有特色的一个概念。 那么下面呢我们就具体来看一看函数模板。 所谓函数模板呢我们首先就要看一个具体的例子, 我们呢在为了实现这个交换两个int型变量的值的时候呢通常大家 第一步就会想到要去写一个叫做swap的一个函数啊,这个swap函数 它要做的事是什么呢,它就是把相应的这样两个需要交换的 这个变量的引用作为参数呢传递进来, 然后在这个函数体当中呢去定义一个临时的变量tmp, 然后去实现一个把x付给tmp,然后把y付给x,最后把tmp付给y的这样一个啊 交换的一个过程。那么这是一个最简单的去实现int型变量的一个函数, 但是同时你有时候可能会在同样一大段大量的程序当中遇到还要去交换两个 double型的一个变量,那么在没有其他操作的情况下, 那么我们只能再去写一个swap函数,啊因为我们有了函数重载这样一个 概念,所以我们可以让它的这个函数名是同名的, 但是呢因为传入的参数不一样, 我如果需要去交换两个double型的变量的话 那我们就当然传进来两个double变量的这个引用, 啊那么我们同样也是去建立了一系列的这样一个操作 去实现了这样两个double型变量值的交换, 那么这个时候就大家就会有一个想法了,既然呢我都是写的这样一个相应的swap的一个函数, 并且呢在函数体当中实际的一个操纵呢其是都是一致的, 都是通过去建立一个临时变量作为传递 交换了两个传入的这个参数的具体的值,那我是否能够直只写 一个swap函数就可以满足交换各种类型的变量呢? 这个想法呢实际上是可以实现的。那么这就 需要引入函数模版这个概念来进行解决, 那么所谓的函数模版呢就是我们定义在这个函数的时候 并不会去显示的去标记具体他传递参数的这个类型 而是标记了这样用这样一个关键字 template去标记说我是一个函数的模板啊 或者说是我在函数上层构建一个一般意义上的这样一个例子, 那么它被称为叫做模板,那么具体这个函数它传递的这个参数的类型是什么呢? 我就把它都声明在这个template这个关键字后面啊,给出一个尖括号, 那么它里面分别去标记,class类型参数一,class参数二,直到把所有的这个类型 依次地这个声明完为止,完了之后呢我就在这个相应的这个函数 模版当中去定义这个模版名,啊完了之后就去传递相应的形参表, 以及去定义函数所包含的返回值以及函数体, 那么这是一个函数模版的一般意义上的一个语法中间的一个定义, 我们来看一个具体的例子。还是刚才那个swap的函数, 还是我如果能把它延伸写成一个函数模版的时候, 我应该具体怎么样去干呢,我实际上就用template这样一个关键字 去申明说我有一个类型参数,叫做class T, 啊这个类型呢是一个t来标记的,那么我在这个swap 这样一个函数模版的这个具体这个模版的定义的时候呢, 我就不需要去定义我传进来的这两个参数的类型分别是什么, 我只需要去标记如果它们是同样类型那么我们都用T来进行标记, 也就是说我传进来了一个T类型的一个参数x和y, 那么同样的我在这个函数体里面也会定一个T类型的临时的 这样一个参数tmp 然后利用 其他的这个相应的一些操作把X付给tmp,把Y付给X,把temp付给Y 这样三个语句来实现我的这个函数模版swap这样一个交换 两个参数对应的这个变量值的一个具体的实现, 那么大家可以看到在这个函数模版当中我并没有具体显示的定义说它是 适用于哪一个类型的x和y,啊它可以既是T,既可以是int的, T也可以是double的, 所以呢我们就可以通过这个例子可以 显示的看到说我如果自己定义一个函数模板的话 那么我就可以通过写这样一个swap函数 来实现说去交换多种数据结构类型的这样一个变量的一个操作, 我们来看我们怎么样去用这个函数模版,啊我定义了这样一个swap一个函数模版之后, 那么如果我需要去交换相应的两个 int型的变量n和m的话那么我只需要在这个 本身的函数模版的使用的时候,传递参数n和m, 那么这个时候呢编译器就会自动生成去替我生成void 这个swap int, 两个int的这个对应的这个变量引用 作为参数的这样一个函数, 那么它实际上呢,就是去具体地将其 原来的这个t 分别呢作用为int类型的这样一个整形的这样一个变量, 那么有了这样两个int型的整型变量作为参数的话呢 那么编译器就会自动地生成相应的这个具体的这个函数是swap, 那么我就可以直接来进行后续的这个程序的这个交换,两个int型 成员变量,两个int类型的啊这个变量的这样一个操作, 那么同样的,如果我需要去交换两个double型的 这个变量f和g, 那么我的编译器也会相应地将这个T的替换成DOUBLE这个类型 那么我的这个程序实际在调用的时候它就是去掉用这个swap,double 这个x和double- y,这样一个函数,那么它实现的功能呢, 也是将这样两个double类型的变量实现了一个交换的这样一个工作。 那么我们刚才看到的例子当中呢,我们的这个template 后面定义的这个参数类型呢是只有一个就是T,class T, 那么我们有些时候呢在定义函数的时候其参数的 类型可能是不一致的话啊,我们可能有多种情况被传递进来 那么我们就可以相应地在函数模版中间定义不只一个类型的参数比如说这里 我们去定义了这个class t1和classt2, 那么通过定义两个类型的参数的话我就可以相应的 更为丰富的去定义我的这个定义的这个函数模版中间包含的参数啊, 比如说我认为这个arg 1的类型是T1, arg 2 的类型是T2, 并且呢这个函数的返回值呢也是一个t2类型的这样一个函数模版,print, 那么有了刚才这样一系列介绍之后呢我们来看一个具体的例子啊。 我可能需要去写一个,去求得数组中间的最大元素的这样一个函数模版称为叫做MaxElement, 那么MaxElement它呢只需要定义一个 类型参数,也就是Class T,因为我这一系列的数组的类型都是一致的, 那么在这个maxelement这样的一个函数模板当中传递的第一个参数呢就是T类型的这个数组A 除此之外呢我还传递了一个int size, 这个int size呢它本身是 用来标记整个这个数组元素中间包含的个数的。 那么除此之外呢我的这个maxelement因为我获得的是这个数组中间最大的那个元素, 所以我的返回值呢,返回的就是这个数组元素中间的一个,所以那么显然它就是以T这个形式来进行返回的。 那么有了这样的这个模板,函数模板的这样一个定义之后我 具体在这个函数模板里面实现的话呢,就是首先将第一个 这个数组的首元素赋值给我的定义的这个T类型的这个tmpmax,我定义的tmpmax去标记 我当前最大的那个元素,完了之后呢我依次去读取相应的 这个数组里面的每一个元素去和tmpmax比较, 如果本身这个tmpmax呢它是 发现比我当前读的这个数组元素要小的话,我就将 这个当前的这个数组元素赋给tmpmax, 那么经过这样一系列的比较之后呢我就得到了这个数组中间最大的那个,把这个tmpmax呢 返回给,作为返回值,返回出来 就可以了,所以这就是这个MaxElement这个函数模板的例子。 那么注意呢,函数模板本身它也是可以重载的, 也就是说,只要两个函数模板它们自身的形参表不同, 就可以,虽然它们是同名的,但是它也表达了是不同的模板, 比方说,我们在这个例子当中,我们写了两个print的 模板,那么这两个模板呢, 因为其参数本身的类型不一致, 第一个模板呢它包含了两种类型,T1和T2, 传递了两个不同的类型的参数,而在第二个例子当中呢它只有一个 T作为这个类型参数,那么我们就只传递了,虽然它传递的是两个 参数,但是其类型呢都是T类型,所以呢我们说这样两个Print函数的 模板呢它是不同的。 我们在函数模板,有了函数模板之后呢就会发现 我们在做重载的时候,可能会出现这样的一系列情况,也就是说,在程序里面 会有同名的这个普通的函数还有同名的这个模板函数, 那么具体如果我具体写了一个函数,C++编译器是如何去 匹配实现具体选用哪一个函数或者函数模板来进行这个 程序的这个具体的执行呢?我们说,编译器呢需要遵循这样的一个优先的方式,第一步呢 就是去寻找参数完全匹配 的普通函数,即便我有这个 通过模板函数实例化出来的函数,那么也是先不考虑的。 我们首先考虑的就是普通函数,如果参数完全匹配的话,那就优先去匹配这个普通函数, 那么如果没有这个普通函数,那么就会去匹配参数完全匹配的 模板函数。那么如果有一致的模板函数也可以。 如果既没有参数一致的普通函数也没有参数一致的模板函数呢 那么我们就具体来看一下,就是它会让这个 实参经过自动类型转换能够匹配的普通函数 也是可以的。那么如果通过上述三步操作 仍然没有找到合适的这个 函数来进行使用的话,那么就会报错。 我们来看一个具体,去看这个函数模板调用顺序的一个例子。 我们定义了一个模板,称为叫做max 那么它呢首先只包含了一个类型参数T, 那么我们在这个里头,在这个max第一个这个max的这个 函数模板当中呢它传载了两个参数,都是T类型的, 形参分别是T类型的A和T类型的B, 那么在这个max里头呢它只去cout了一个语句叫做template Max1 那么我们同样的还定义了一个同名的这个函数 模板,也称为叫做max。 但是呢它包含了两个不同的 这个类型参数,分别是T和T2, 那么用这个T和T2呢分别去定义了这个相应的参数,形参a和b, 那么同时它的返回值呢也是这个T类型。 在这个第二个函数模板里头的话呢那么它定义的就是 cout语句下templateMax2这样的语句的输出。 那么同样呢我的这个程序当中还包含了另外一个函数,称为叫做 max,这个max呢是一个普通的函数,那么这个 普通函数呢,它的参数呢,就是形参呢是两个double型的, 变量,那么返回值呢也是一个double。 那么在这个max普通函数的这个函数体里面呢 我们就让它去cout一下max这样的一个字串。 那么有了这样两个这个函数模板以及一个相应的普通函数 遵循我们刚才所讲的编译器所执行的一系列的操作,我们来看一看, 具体在这个main函数里头,我们定义了一个 这个首先定义了一个Int i和j,这样两个整型的变量, 那么接着呢我们就去调用了max括号1.2和3.4, 那么我们可以看到我们传递的这两个实参呢是两个double型的, 是两个double型的这样的一个 参数。所以呢那么我们通过去匹配相应的两个函数, 首先我们第一步想到的就是首先第一步要去匹配一个普通函数, 看看有没有具有参数完全一致的普通函数, 哎我们发现说我们恰巧在程序当中就定义了 一个这样的max函数,那么它的参数就是两个double型的 这个变量,所以呢对于这个语句而言我们去执行的就是 max(double, double)这样的一个普通的函数,接着呢我们去传递了一个 max,调用了一个max括号i,j这样的一个函数。那么i和j呢 因为它定义的都是int型的这样的变量,所以呢我们看到说 我们没有 相应匹配的这个普通函数能够直接进行调用,那么第二步呢 我们就去看有没有合适的参数类型完全匹配的这个模板函数 我们发现说我们在前面定义了两个模板, 分别呢,一个是传递的参数都是一致的T,还有一个呢是传递了一个T1和T2, 那么在这个语句里头我们显然呢是要去调用第一个模板 生成的这个具体的函数,因为它只传递了一个同样类型的这个T类型的参数。 那么相应的第三个max呢它就会去调用,因为它的参数本身是1.2和3, 一个是double型的,一个是int型的,所以呢它就去调用了第二个max的这个模板 生成的函数,那么它对应定义呢就是有 T和T2这样两个不同的参数。 那么我们看到,所以对于这样的一个函数来讲的话它的运行结果呢虽然都是调用了不同的max, 那么运行结果是不一样的,分别呢去执行了mymax,templatemax1, 和templatemax2,这样三个语句。 那么此外呢我们要注意我们在去编写这个函数模板的时候 一定要去注意这个赋值本身的这个兼容性的一个原则。不要因为类型参数的选取问题 引起了二义性。我们看到如果我们写了一个函数的模板称为叫做myfunction, 那么myfunction本身呢它包含了两个参数arg1和arg2, 那么这样两个参数呢它的类型是完全一致的,都是我们定义的这个class t, 那么这个时候如果我们要去使用这个myfunction 传递的参数是5和7的话,那么这个T自然就会被 实例化为成一个int类型。 同样的,如果我们这个实参分别是5.8和8.4的话,那么也没有问题。 T呢就会被自动的替换为成W,换成double, 但是呢如果这个时候你的这个myfunction传递的参数是5,和8.4的话 那么就会遇到问题了,编译器并不知道说这样的一个T 它因为既是arg1的类型又是arg2的类型,那么 当你的参数分别是两种不同类型的时候, 具体这个T是会应该赋值,会被替换为int还是会被替换为成double呢, 这样就会存在一个参数二义性的问题。 因此呢我们对于这样的函数模板的设计的时候就应该把它设计成为两种, 那么使用多个类型参数呢可以有效的避免 这个参数的二义性,当我们是从新写这个myfunction的时候 我们的参数的这个类型分别是T1和T2, 那么这时候这个函数模板本身就具有了更强的灵活性, 那么还是前两个语句没有问题,相应的T1,T2呢是可以被赋值成一致的, 但是当你遇到这两个参数类型不一致的时候, 那么T1就可以自然被赋值为int,被替换为int, T2呢就会被替换成double,那么这个程序本身呢就不会出错了。