1. 首页
  2. c++

你好,C++(52)写请假条有请假条模板,写函数也有函数模板——8.2 用模板实现通用算法,

8.2 用模板实现通用算法

在程序中大家都愿意使用STL,那是因为STL很方便实用,大大提高了开发效率。而STL之所以能够提高开发效率,这恐怕跟当初STL是由一个不太勤快的程序员小A发明的分不开。

一天,小A接到一项任务,要求写一个函数从两个整数中选取较大的一个数。这个任务太简单了,他很快就有了如下的结果:

// 取得两个整数中较大的一个  int max(int a,int b)  {      return a > b ? a : b;  } 

到了第二天,老板又给了他一项新的任务:要求写一个函数从两个浮点数中选取较大的一个浮点数。小A心想,幸亏我学过函数重载,要不然还麻烦了。于是想都不想,直接copy了第一天的代码,修改了max()函数的返回值和参数类型就有了第二天的结果:

// 从两个浮点数中选取较大的一个  float max(float a, float b)  {      return a > b ? a : b;  } 

可第三天新任务又来了:要求写一个从两个字符串中选取较大字符串的函数。这下小A的头大了,整数、浮点数、字符串,那还有字符类型、短整型、长整型、还有自定义类型……这无穷无尽的max函数何时是个头啊。小A心中暗想,要是可以像写工作总结一样,有个模板就好了。这样可以把max函数做成一个模板,针对不同的数据类型,直接替换其中的返回值和参数的类型就可生成不同版本的max函数,多省事啊。正是这位不太勤快的程序员的偷懒想法使得C++世界中模板的概念诞生了,STL开始萌芽。

就像利用文档模板实现文档格式的重用一样,C++中的模板是实现代码重用的一种重要机制。它可以实现数据类型的参数化,而把算法当成是一个通用的模板,通过将具体的数据类型带入模板中以实现具体的针对特定数据类型的算法,而STL也正是借助模板的威力而构建起来的。

知道更多:STL的天才创意是如何产生的

在20世纪70年代末,Alexander Stepanov第一个发现算法不依赖于数据结构的特定实现,而仅和数据结构的一些基本语义属性相关。这些属性表达了一种能力,比如可以从数据结构的一个成员取得下一个成员、可以从头到尾遍历结构中的所有元素等。以排序算法为例,它不关心元素到底是存放在数组中还是线性表中。Alexander Stepanov研究之后发现,一些通用算法可以用一种抽象的方式实现,而且不会影响效率。正是他的这个发现,成为STL的思想源起。

1985年,Alexander Stepanov开发了基本Ada库。由于当时C++开始流行,于是人们希望他在C++中也这样做,但直到1987年,模板(template)在C++中还未实现,所以他的工作不得不推迟了。1988年,Alexander Stepanov到惠普实验室工作,并在1992年被任命为一个算法项目的经理。在此项目中,Alexander Stepanov和Meng Lee编写了一个巨大的库——标准模板库,意图定义一些通用算法而不影响效率。这个标准模板库就是STL的雏形。后来,这个库逐渐发展成熟,最终在1994年7月14日,ANSI/ISO C++标准化委员会采纳STL为草案标准,从此,STL成为C++的重要组成部分。

8.2.1 函数模板

在第5章中,我们将一个函数比作一个箱子。通常,箱子都是专用的,比如装衣服的箱子只能用来装衣服,装零食的箱子只能用来装零食。同理,函数通常也是专用的,比如获取两个int类型数中较大值的max()函数只能处理int类型数据,而获取两个float类型数中较大字的max()函数也同样只能处理float类型数据。这么说来,难道我们要为处理成千上万种类型的数据而编写成千上万个max()函数?

当然不用,因为C++中已经有了小A所希望的“模板”。借助模板机制,我们可以创建一种万能箱子——函数模板,它以一个或者多个数据类型作为参数,因此它可以用来处理各种类型的数据。当编译器发现一个函数模板的调用后,它将根据函数模板的类型参数自动生成一个此种类型的重载函数,称该重载函数为模板函数。根据参数类型的不同,一个函数模板可以随时变成各种不同的重载函数,从而实现对各种数据类型的处理,达到一个函数模板应对万千数据类型的效果。

在C++中,有很多算法需要能够同时处理各种不同数据类型的数据,比如一个获取两个数据中较大值的算法,既要能够处理两个整型数,同时也要能够处理两个字符串。虽然这些算法处理的数据类型不同,但是算法本身是相同的,这就像我们写请帖,请帖要送给很多人,所以请帖上的名字是不同的,但是各个请帖的内容都是相同的。这种情况下,我们写请帖有请帖模板,用的时候只要填上名字就行了。而写函数同样也有函数模板,用的时候只需要填上数据类型就行了。一般来讲,函数模板可以用来创建一个通用功能的函数,以支持各种不同的数据类型,简化重载函数的设计。函数模板的定义非常简单,其语法格式如下:

template <typename 标识符1,typename 标识符2…>  返回值类型 函数名(形参表)  {    // 函数体…  } 

从这里可以看到,函数模板的定义和普通函数的定义非常相似,同样需要指定一个函数的返回值类型、函数名以及形参表。两者所不同的是,在定义函数模板时,需要用template关键字表示这是一个函数模板,然后在“<>”中用typename关键字来定义一个或者多个标识符,这就是函数模板中抽象的数据类型,它们被称为函数模板的类型参数。在函数模板(包括返回值类型和参数类型)中,可以使用这些类型参数当作数据类型来使用,而在模板函数被实际调用的时候,它们会被实际的数据类型所代替,从而成为某个特定类型的重载函数。在定义函数模板时,这些标识符只是起一个占位的作用而已。

例如,想让前面的max()函数既能够处理int类型数据,还能够处理float类型数据,甚至还包括string等类型数据,我们就可以把它定义成一个函数模板:

// 获取两个数中的较大值  // T就是函数模板的类型参数  // 为了与标准库中的max()函数相区别,用mymax作函数名  // 为了防止数据被修改,使用const对参数和返回值进行修饰  template <typename T>  const T& mymax(const T& a, const T& b )  {      return a > b ? a : b ;  } 

在上面的函数模板定义中,我们首先在“<>”中用typename关键字定义了一个类型参数T,接下来就可以将T这种抽象的数据类型当作实际的数据类型来使用,可以用来做函数返回值和参数的类型,也可以在函数内用它来定义变量等等。跟调用函数时需要指定它的实际参数一样,我们在调用函数模板时,也同样需要在函数名之后用“<>”指定其类型参数的实际类型,这就相当于我们得到了一个针对特定类型的重载函数,然后直接调用这个特定版本的重载函数就可以了。例如,我们可以使用刚刚定义的mymax()函数模板来处理两个int类型数据和两个string类型数据:

// …  // 定义函数模板 template <typename T>  // T为模板的类型参数 const T& mymax(const T& a, const T& b) {     return a > b ? a : b; // 返回两者中较大的一个 }  int main() {     // 两个int类型数据     int a = 4;     int b = 5;     cout<<a<<"和"<<b<<"之间较大的是"         <<mymax<int>(a,b)<<endl;// 调用int版本的mymax()处理int类型数据     // 两个string类型数据         string strA = "good";     string strB = "afternoon";     cout<<strA<<"和"<<strB<<"之间较大的是"         // 调用string版本的mymax()处理string类型数据        <<mymax<string>(strA,strB)<<endl;      return 0; } 

在这段程序中,我们分别使用了int和string作为mymax()函数模板的模板类型参数,函数模板根据不同的类型参数生成不同版本的模板函数来处理相应类型的数据,最终利用一个函数模板完成对多种类型数据的处理。很明显,使用函数模板后,减少了对函数的重载,增强了代码的复用,提高了开发的效率。要不然,针对每种数据类型都要形成一个特定的重载函数,整个程序将陷入一片同质化的重载函数的汪洋大海之中。这里大家一定会觉得函数模板可以根据调用时的模板类型参数动态生成相应的模板函数非常神奇,那么它背后到底是如何运作的呢?

实际上,编译器在编译mymax()函数调用时,会以函数名之后“<>”内的数据类型作为函数模板定义时模板类型参数的实际类型,然后编译器会以函数模板中的定义为样板,用实际的数据类型替换函数模板中的类型参数,从而形成针对特定类型的重载函数。例如,在编译“mymax<int>(a,b)”时,编译器会用实际的类型int替换掉函数模板中的类型参数T,自动为mymax()函数调用生成一个整型数的版本:

// 整型数版本的mymax()模板函数  const int& mymax( const int& a, const int& b )  {      return a > b ? a : b ;  } 

在主函数中,当我们以int作为实际的类型参数调用mymax()函数模板时,实际上执行的是上面生成的整型数版本的mymax()函数。同理,当以string作为类型参数调用mymax()函数时,编译器也会为这个函数调用生成一个string版本的mymax()函数,进而调用这个函数来完成对两个string类型数据的处理。特别地,如果调用函数模板时,编译器能够根据实际参数的类型推断出函数模板的类型参数,函数调用中“<>”内的类型参数也可以省略。函数模板生成重载函数如图8-2所示。

你好,C++(52)写请假条有请假条模板,写函数也有函数模板——8.2 用模板实现通用算法,

图8-2 函数模板生成重载函数

从上面代码的输出结果中可以发现,用mymax()函数模板处理“good”和“morning”这两个字符串从中选取较大值时,得到的结果是“good”字符串,也就是字符串中字符的ASCII值比较的结果,而实际上我们期望的结果是“afternoo”字符串,也就是字符串长度比较的结果。这就意味着,虽然函数模板的意义是为不同的数据类型提供通用的算法,但是有的时候,这些算法也无法做到完全通用。例如,mymax()函数使用“>”操作符来比较两个参数的大小,在大多数情况下,比如参数类型是int、float等,这种比较都是合理的。但是,当参数是string类型时,它会逐个比较字符串中字符的ASCII值来决定两个string类型参数的大小,而这并不是我们所期望的结果。在这种情况下,就需要对函数模板进行特化,实现特定类型的模板函数。通过这种方式,可以使得函数模板及能够适应大多数的情况,同时也能满足个性化的特殊需求。例如:

// 利用模板特化,实现特定的string类型的模板函数  template <>   // 类型参数留空  // 使用实际类型string代替类型参数T  string mymax<string>( const string a, const string b )  {      // 通过长度比较决定字符串大小      return a.length() > b.length() ? a : b ;  } 

有了某个特定类型的模板特化之后,当使用这一类型的参数调用函数模板时,编译器将使用特化后的函数模板,而如果是其他类型的参数,仍将使用函数模板的普通版本。例如:

// 未特化的类型,依然使用“>”比较大小  cout<<a<<"和"<<b<<"之间较大的是"           <<mymax<int>(a,b)<<endl;  // …  // 特化后的类型,使用特化的模板函数,通过字符串长度比较大小  cout<<"使用string类型特化版本:"<<strA<<"和"<<strB<<"之间较大的是"       <<mymax<string>(strA,strB)<<endl; // 返回结果是afternoon 

原文始发于:你好,C++(52)写请假条有请假条模板,写函数也有函数模板——8.2 用模板实现通用算法,

主题测试文章,只做测试使用。发布者:杀手梦三刀,转转请注明出处:http://www.cxybcw.com/7727.html

联系我们

13687733322

在线咨询:点击这里给我发消息

邮件:1877088071@qq.com

工作时间:周一至周五,9:30-18:30,节假日休息

QR code