泛型编程(Generic Programming,简称GP)---模板

来源:互联网 发布:coc飞龙升级数据 编辑:程序博客网 时间:2024/04/29 14:31

泛型就是通过参数化类型来实现在同一份代码上操作多种数据类型。利用“参数化类型”将类型抽象化,从而实现灵活的复用。

C++引入模板的概念,主要包括函数模板类模板

1、函数模板

1.1 函数模板定义及其实例化

所谓函数模板,实际上是建立一个通用函数,其函数类型和形参类型中的全部或部分类型不具体指定,用一个虚拟的类型来代表。这个通用函数就称为函数模板。凡是函数体相同的函数都可以用这个模板来代替,不必定义多个函数,只需在模板中定义一次即可。函数调用时系统会根据实参的类型来取代模板中的虚拟类型,从而实现了不同函数的功能。

求两个数中的大者,分别考虑整数、长整数、实数的情况

如果没有采用模板,一般用函数重载,如下:

#include <iostream>using namespace std;int Max(int x,int y)                      //整数比较 {  return x>y? x:y ;  }                 //长整数比较long Max(long x, long y) {  return x>y? x:y ;  }double Max(double x, double y) //实数比较{  return x>y? x:y ;  }int main(){    int a=12,b=34,m;    long c=67890,d=67899,n;    double e=12.34,f=56.78,p;    m=Max(a,b);    n=Max(c,d);    p=Max(e,f) ;    cout<< "int_max="<<m<<endl;    cout<< "long_max="<<n<<endl;    cout<< "double_max="<<p <<endl ;    return 0;}

程序运行结果如下:

int_max=34

long_max=67899

double_max=56.78

如果采用函数模板,只需写一次即可

#include <iostream>using namespace std;template <class T>T Max(T x,T y ) {  return x>y? x:y ;  }int main(){  int a=12,b=34,m;  long c=67890,d=67899,n;  double e=12.34,f=56.78,p;  m=Max(a,b);//调用函数模板,此时T被int取代  n=Max(c,d); //调用函数模板,此时T被long取代  p=Max(e,f) ; //调用函数模板,此时T被double取代  cout<< "int_max="<<m<<endl;  cout<< "long_max="<<n <<endl;  cout<< "double_max="<<p <<endl ;  return 0;  }
定义函数模板的一般形式为:
template <typename T>  或         template <class T>
返回类型函数名(形参表)         返回类型函数名(形参表)
{                                                  {
       函数体                                   函数体
}                                                     }

说明:
1)在定义模板时,不允许template语句与函数模板之间有任何其他语句。下面的模板定义是错误的:

   template <typename T>

   int a;//错误,不允许在此位置有任何语句

  T Max(T x,T y){ … }

2)不要把这里的class与类的声明关键字class混淆在一起,虽然它们由相同的字母组成,但含义是不同的。为了区别类与模板参数中的类型关键字class,标准C++提出了用typename作为模板参数的类型关键字,同时也支持使用class。如果用typename其含义就很清楚,肯定是类型名而不是类名。 
3)函数模板的类型参数可以不止一个,可根据实际需要确定个数,但每个类型参数都必须用关键字typenameclass限定。
template<class T1,class T2,class T3>
T1 Func (T1 a, T 2 b, T3 c){ … }
4)当一个名字被声明为模板参数之后,它就可以使用了,一直到模板声明或定义结束为止。模板类型参数被用做一个类型指示符,可以出现在模板定义的余下部分。它的使用方式与内置或用户定义的类型完全一样,比如用来声明变量和强制类型转换。

当编译器遇到关键字template和跟随其后的函数定义时,它只是简单地知道:这个函数模板在后面的程序代码中可能会用到。除此之外,编译器不会做额外的工作。在这个阶段,函数模板本身并不能使编译器产生任何代码,因为编译器此时并不知道函数模板要处理的具体数据类型,根本无法生成任何函数代码。

当编译器遇到程序中对函数模板的调用时,它才会根据调用语句中实参的具体类型,确定模板参数的数据类型,并用此类型替换函数模板中的模板参数,生成能够处理该类型的函数代码,即模板函数

即函数模板--->实例化----->模板函数。

例如,在程序中,当编译器遇到

    template <typename T>

   T Max(T x,T y){ … }

   时,并不会产生任何代码,但当它遇到函数调用Max(a,b),编译器会将函数名Max与模板Max相匹配,将实参的类型取代函数模板中的虚拟类型T,生成下面的模板函数:

int Max(int x,int y) {  return x>y? x:y;  }
然后调用它。

编译器只在第1次调用时生成模板函数,当之后遇到相同类型的参数调用时,不再生成其他模板函数,它将调用第1次实例化生成的模板函数。
可以看出,用函数模板比用函数重载更方便,程序更简洁。但它只适用于函数的参数个数相同而类型不同,且函数体相同的情况,如果参数的个数不同则不能用函数模板。

1.2 模板参数的匹配问题

C++在实例化函数模板的过程中,只是简单地将模板参数替换成调用实参的类型,并以此生成模板函数,不会进行参数类型的任何转换。这种方式与普通函数的参数处理有着极大的区别,在普通函数的调用过程中,C++会对类型不匹配的参数进行隐式的类型转换

例如,在main()函数中再添加如下语句
cout<<"2,2.3两数中的大者为:"<<Max(2,2.3)<<endl;
cout<<"'a',2两数中的大者为:"<<Max('a',2)<<endl;
编译程序,将会产生2个编译错误:
error C2782: 'T __cdeclMax(T,T)' : template parameter 'T'is ambiguous
could be 'double' or 'int'
error C2782: 'T __cdeclMax(T,T)' : template parameter 'T'is ambiguous
could be 'int' or 'char'
这种问题的解决方法有以下几种:
(1)在模板调用时进行参数类型的强制转换,如下所示:
cout<<"2,2.3两数中的大者为:"<<Max(double(2),2.3)<<endl;
cout<<" 'a',2两数中的大者为:"<<Max(int( 'a'),2)<<endl;
(2)通过提供<>里面的参数类型来调用这个模板,直接模板实例化,然后类型不匹配的参数会发生隐式的类型转换,如下所示
cout<<"2,2.3两数中的大者为:"<<Max<double>(2,2.3)<<endl
cout<<" 'a',2两数中的大者为:"<<Max<int>('a', 2)<<endl;
(3)指定多个模板参数
对于Max函数模板来说,我们可以为它指定两个不同的类型参数。

#include <iostream>using namespace std;template <typename T1,typename T2>T1 Max(T1 x,T2 y){  return x>y? x:y ;  }int main(){cout<<"2,2.3两数中的大者为:"<<Max(2,2.3)<<endl;cout<<" 'a',2 两数中的大者为:"<<Max('a', 2)<<endl;return 0;   }

编译该程序,将不再会产生编译错误。但函数的运行结果并不精确,甚至存在较大的误差,但它并不表示程序有什么错误。其原因是:Max函数模板的返回值类型依赖于模板参数T1。如果在调用时将精度高的数据类型作为第一个参数,结果将是正确的。上述语句如果改写成如下形式:
cout<<"2,2.3两数中的大者为:"<<Max(2.3, 2)<<endl;
cout<<" 'a',2两数中的大者为:"<<Max(2,'a')<<endl;

由此可见,这样做对函数的调用方式产生依赖性。一般来说要显式表现出转换的目的,保证不出错。如第一种方法。

1.3  模板形参表

函数模板形参表中除了可以出现用typenameclass关键字声明的类型参数外,还可以出现确定类型参数,称为非类型参数。例如:
template <class T1, class T2, classT3,int T4>
T1 Func (T1 a, T 2 b, T3 c)
{    …   }
模板非类型参数名代表了一个潜在的值。它被用做一个常量值,可以出现在模板定义的余下部分。它可以用在要求常量的地方,或是在数组声明中指定数组的大小或作为枚举常量的初始值。在模板调用时只能为其提供相应类型的常数值。非类型参数是受限制的,通常可以是整型、枚举型、对象或函数的引用,以及对象、函数或类成员的指针,但不允许用浮点型(或双精度型)、类对象或void作为非类型参数。

用函数模板实现数组的冒泡排序,数组可以是任意类型,数组的大小由模板参数指定。
#include <iostream>using namespace std;template <typename T,int size> void BubbleSort(T a[]){   int i,j;    bool change;    for(i=size-1,change=true;i>=1 && change;--i)    {  change=false;       for( j=0;j<i;++j)  if(a[j]>a[j+1])           { T temp;  temp=a[j];a[j]=a[j+1];a[j+1]=temp;             change=true;             }      }    }int main(){   int a[]={9,7,5,3,1,0,2,4,6,8};    char b[]={'A','C','E','F','D','B','U','V','W','Q'};    int i;    cout<<"********a数组********"<<endl;    cout<<"排序前:"<<endl;    for(i=0;i<10;i++)  cout<<a[i]<<"  ";    cout<<endl;    BubbleSort<int,10>(a);    cout<<"排序后:"<<endl;    for(i=0;i<10;i++)  cout<<a[i]<<"  ";    cout<<endl;cout<<"********b数组********"<<endl;cout<<"排序前:"<<endl;for(i=0;i<10;i++)   cout<<b[i]<<"  ";cout<<endl;BubbleSort<char,10>(b);     cout<<"排序后:"<<endl;for(i=0;i<10;i++)   cout<<b[i]<<"  ";cout<<endl;return 0;}

程序运行结果如下:

********a数组********

排序前:

9  7 5  3  1 0  2  4 6  8

排序后:

0  1 2  3  4 5  6  7 8  9

********b数组********

排序前:

A  C E  F  D B  U  V W  Q

排序后:

A  B C  D  E F  Q  U V  W

本例中,size被声明为BubbleSort函数模板的非类型参数后,在模板定义的余下部分,它被当做一个常量值使用。由于BubbleSort函数不是通过引用来传递数组,故在模板调用时,必须显式指定模板实参,明确指定数组的大小。如果BubbleSort函数是通过引用来传递数组,则在模板调用时,就可以不显式指定模板实参,而由编译器自动推断出来,如下所示:

template<typename T, int size>
void BubbleSort(T(&a)[size]) {  …  }
main()函数中的两个函数调用语句改为:
BubbleSort(a);
BubbleSort(b);
不过,该程序用VC++6.0的C++编译器不能编译通过,可改用其他编译器,如:VC++7.0的C++编译器、GNUC++编译器。

另外,本程序也可以不使用模板非类型参数来指定数组的大小,而在BubbleSort()函数的参数表中增加一个传递数组大小的int型参数。修改后的函数模板代码如下:
template<typename T>
voidBubbleSort(Ta[], int n)
{   …//此处代码省略不写
   for(i=n-1,change=true;i>=1&&change;--i)
    //此处代码省略不写
}
同时,修改main()函数中的两个函数调用语句为:
BubbleSort(a,10);
BubbleSort(b,10);

综上所述:函数模板形参表中可以出现用typename或class关键字声明的类型参数,还可以出现由普通参数声明构成的非类型参数。除此之外,函数模板形参表中还可以出现类模板类型的参数,在模板调用时需要为其提供一个类模板

1.4 函数模板重载

像普通函数一样,也可以用相同的函数名重载函数模板。实际上,max函数模板并不能完成两个字符串数据的大小比较,如果在main函数中添加如下语句:
char*s1="Beijing 2008",*s2="Welcome to Beijing";
cout<<"Beijing 2008Welcome to Beijing两个字符串中的大者为:"<<Max(s1,s2)<<endl;
运行程序,可以发现上述输出语句的执行结果为:
Beijing2008Welcometo Beijing两个字符串中的大者为:Beijing 2008

结果是错误的。其原因是:函数调用Max(s1,s2)的实参类型为char*,编译器用char*实例化函数模板T Max(T x,Ty),生成下面的模板函数:
char*Max(char *x,char *y){  return x>y? x:y ;  }
这里实际比较的不是两个字符串,而是两个字符串的地址。那一个字符串的存储地址高,就输出那个字符串。从输出结果看,应该是Beijing 2008的地址高。为了验证这一点,我们用语句:
cout<<&s1<<"  "<<&s2<<endl;
输出s1s2的地址,结果为:
0012FF7C  0012FF78
处理这种异常情况的方法可以有如下两种:
1)对函数模板进行重载,增加一个与函数模板同名的普通函数定义:

    char *Max(char *x,char *y)

    {   cout<<"This is the overload function with    char*,char*!maxis:";

        return strcmp(x,y)>0?x:y;

     }

此外,还要在程序开头增加如下的include命令:
#include<string>
2)改变函数调用Max(s1,s2)的实参类型为string,这样编译器就用string实例化函数模板T Max(T x,Ty),生成下面的模板函数:
string Max(string x, string y)
{  return x>y? x:y ;  }
此外,还要注意在程序开头增加如下的include命令:
#include<string>

优先处理重载函数,如果没有重载函数匹配,然后在处理模板的实例化。

编写求2个数、3个数和一组数中最大数的函数模板。

#include <iostream>#include <string>using namespace std;template <typename T>                 //声明函数模板T Max(T x,T y) {  return x>y? x:y;  }   template <typename T>                //函数模板重载T Max(T x,T y,T z){  if(x<y) x=y;    if(x<z) x=z;    return x;   } template <typename T>  //函数模板重载T Max(T a[],int n)            //求数组a[n]中的最大数{  T temp=a[0];   for(int i=1;i<n;i++)          if (temp<a[i]) temp=a[i];   return temp;}int main(){    string s1="Beijing 2008",s2="Welcome to Beijing!";    int a[]={1,2,3,4,5,6,7,8,9};    cout<<Max(2,3)<<endl;    cout<<Max(2.02,3.03,4.04)<<endl;    cout<<Max(s1,s2)<<endl;    cout<<Max(a,9)<<endl;    return 0;}
2、类模板

运用函数模板可以设计出与具体数据类型无关的通用函数。与此类似,C++也支持用类模板来设计结构和成员函数完全相同,但所处理的数据类型不同的通用类。在设计类模板时,可以使其中的某些数据成员、成员函数的参数或返回值与具体类型无关。


2.1 类模板的定义及实例化应用 

模板在C++中更多的使用是在类的定义中,最常见的就是STLStandard Template Library)和ATLActiveX Template Library),它们都是作为ANSI C++标准集成在VC++开发环境中的标准模板库。
类模板的一般定义格式如下:

    template<classT1,classT2 , … >

    class类名

    {

        类体

    } ;

class 也可以用 typename 代替

//Stack.hconst int SSize=10;     //SSize为栈的容量大小template <class T >    //声明类模板,T为类型参数class Stack{public:       Stack(){top=0;}       void Push(T e);   //入栈操作       T Pop();              //出栈操作       bool StackEmpty(){return top==0;}       bool StackFull(){ return top==SSize;} private:      T data[SSize];   //栈元素数组,固定大小为SSize       int top;             //栈顶指针};template <class T >  //push成员函数的类外定义void Stack<T >::Push(T e){   if(top==SSize)    {    cout<<"Stack is Full!Don't push data!"<<endl;         return;     }    data[top++]=e;}
template <class T>  inline T Stack<T >::Pop(){   if(top==0)    {   cout<<"Stack is Empty!Don't pop data!"<<endl;        return 0;    }    top--;    return data[top];}

说明:
1)类模板中的成员函数既可以在类模板内定义,也可以在类模板外定义。
如果在类模板内定义成员函数,其定义方法与普通类成员函数的定义方法相同,如Stack的构造函数、判断栈是否为空的StackEmpty函数、判断栈是否已满的StackFull函数的定义。
如果在类模板外定义成员函数,必须采用如下形式:
template<模板参数列表>
返回值类型 类名<模板参数名表>::成员函数名(参数列表)
{  …  };
例如,上例StackPush成员函数的定义:
template<class T >   //类模板声明
void Stack<T>::Push(T e){  …  }

注意:在引用模板的类名的地方 必须伴有该模板的参数名表。如:void Stack<T>::Push(T e){  …  }

2)如果要在类模板外将成员函数定义为inline函数,应该将inline关键字加在类模板的声明后。例如,上例StackPop成员函数的定义:
template<class T >   //类模板声明
inline T Stack<T >::Pop(){…  } //指定为内联函数

3类模板的成员函数的定义必须同类模板的定义在同一个文件中。因为,类模板定义不同于类的定义,编译器无法通过一般的手段找到类模板成员函数的代码,只有将它和类模板定义放在一起,才能保证类模板正常使用。一般都放入一个.h头文件中。

在声明了一个类模板后,怎样使用它?
Stack<intint_stack;
该语句用Stack类模板定义了一个对象int_stack。编译器遇到该语句,会用int去替换Stack类模板中的所有类型参数T,生成一个int型的具体的类,一般称之为模板类。该类的代码如下:

class Stack{

public:

    Stack(){top=0;}

    void Push(int e);          //入栈操作

    int Pop();                     //出栈操作

  boolStackEmpty(){returntop==0;}//判断栈是否为空

  boolStackFull(){return top==10;} //判断栈是否已满

private:

    int data[10];

    int top;                  //栈顶指针

};

类模板、模板类及模板对象之间的关系图


类模板、模板类及模板对象之间的关系为:由类模板实例化生成针对具体数据类型的模板类,再由模板类定义模板对象。
用类模板定义对象的形式如下:

    类模板名<实际类型名表对象名;

    类模板名<实际类型名表对象名(实参表);

类模板创建其实例模板类时,必须为类模板的每个模板参数显式指定模板实参。然而由函数模板创建其实例模板函数时,可以不显式指定模板实参,这时编译器会自动根据函数调用时的实参来推断出

注意:在类模板实例化的过程中,并不会实例化类模板的成员函数,也就是说,在用类模板定义对象时并不会生成类成员函数的代码。类模板成员函数的实例化发生在该成员函数被调用时,这就意味着只有那些被调用的成员函数才会被实例化。或者说,只有当成员函数被调用时,编译器才会为它生成真正的函数代码。

例如,对于8-8】Stack类模板,假设有下面的main()函数:
int main(){

      Stack<int> int_stack;

      for(int i=1;i<10;i++) int_stack.Push(i);

      return 0 ;

}
作为验证,可以将Stack.hPop()成员函数的类外定义删掉,同时将Stack中的StackEmpty()StackFull()这两个函数的定义修改为如下的声明,然后再编译运行该程序,可以发现程序同样可以正确执行。
class Stack{
    …
    boolStackEmpty(); //判断栈是否为空
   boolStackFull();       //判断栈是否已满
};
由于类模板包含类型参数,因此又称为参数化的类。如果说类是对象的抽象,对象是类的实例,则类模板是类的抽象,模板类是类模板的实例。利用类模板可以建立含各种数据类型的类。

2.2 类模板参数

2.2.1 非类型参数

与函数模板的模板参数一样,类模板的模板参数中也可以出现非类型参数。对于堆栈类模板Stack,也可以不定义一个int型常量SSize来指定栈的容量大小,而改成为其增加一个非类型参数。

//Stack.htemplate <class T,int SSize>class Stack{public:       Stack(){top=0;}       void Push(T e);     //入栈操作       T Pop();                //出栈操作       bool StackEmpty(){return top==0;}       bool StackFull(){ return top=SSize;} private:      T data[SSize]; //栈元素数组,固定大小为SSize      int top;    //栈顶指针};//Push成员函数的类外定义 template <class T,int SSize>void Stack<T,SSize>::Push(T e){   if(top==SSize)    {   cout<<"Stack is Full!Don't push data!"<<endl;        return;     }    data[top++]=e;}// Pop成员函数的类外定义,指定为内联函数template <class T,int SSize> inline T Stack<T,SSize>::Pop(){    if(top==0)     {   cout<<"Stack is Empty!Don't pop data!"<<endl;         return 0;     }     top--;     return data[top];}

当需要这个模板的一个实例时,必须为非类型参数SSize显式提供一个编译时常数值。例如:
Stack<int,10>  int_stack;
2.2.2 默认模板参数

在类模板中,可以为模板参数提供默认参数,但是在函数模板中却不行。例如,为了使上述的固定大小的Stack类模板更友好一些,可以为其非类型模板参数SSize提供默认值,如下所示:

template<class T,int SSize=10>

class Stack

{public:

     

private:

     Tdata[SSize];//栈元素数组,固定大小为SSize

     int top;           //栈顶指针

};

说明:
(1)作为默认的模板参数,它们只能被定义一次,编译器会知道第1次的模板声明或定义。
(2)指定默认值的模板参数必须放在模板形参表的右端,否则出错。
template<class T1,class T2,class T3=double,int N=100> //正确
template<class T1,class T2=double,class T3,int N=100> //错误

template<class T=int,int SSize=10>

classStack

{public:

  …

private:

    T data[SSize];//栈元素数组,固定大小为SSize

      int top;        //栈顶指针

};

Stack<> mystack;  //same as Stack<int,10> 

模板类型的模板参数
(3)可以为所有模板参数提供默认值,但在声明一个实例时必须使用一对空的尖括号,这样编译器就知道说明了一个类模板。
2.2.3 

类模板的模板形参表中的参数类型有3种:类型参数、非类型参数、类模板类型的参数,函数模板的模板参数类型也与此相同。下面看一个类模板类型的模板参数的例子。

类模板类型的模板参数举例

#include <iostream>using namespace std;template <class T,size_t size>class Array{    T data[size];     size_t count;                 //数组元素个数public:    Array(){count=0;}         //构造函数    void PushBack(const T& t)     {  if(count<size)  data[count++]=t;  }    void PopBack()    { if(count>0)  - -count; }    T* Begin(){return data;}          T* End(){return data+count;} };
//声明Container类模板,//它有一个类模板类型的模板参数Seqtemplate <class T,size_t size,template                      <class,size_t> class Seq>class Container{    Seq<T,size> seq;public:    void Append(const T& t){ seq.PushBack(t);}    T* Begin(){return seq.Begin();}    T* End(){return seq.End();}};
int main(){   const size_t N=10;    container<int,N,Array> container;    container.Append(1);    container.Append(2);    int *p=container.Begin();    while(p!=container.End())         cout<<*p++<<endl;    return 0;}





0 0
原创粉丝点击