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

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

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 )函数模板的类型参数可以不止一个,可根据实际需要确定个数,但每个类型参数都必须用关键字 typename class 限定。
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两数中的大者为:"<< M ax(2,2.3)<< endl ;
cout <<"'a',2两数中的大者为:"<< M ax('a',2)<< endl ;
编译程序,将会产生2个编译错误:
error C2782: 'T __ cdecl M ax(T,T)' : template parameter 'T'is ambiguous
could be 'double' or 'int'
error C2782: 'T __ cdecl M ax(T,T)' : template parameter 'T'is ambiguous
could be 'int' or 'char'
这种问题的解决方法有以下几种:
(1)在模板调用时进行 参数类型的强制转换 ,如下所示:
cout <<"2,2.3两数中的大者为:"<< M ax ( double(2) ,2.3)<< endl ;
cout <<" 'a',2 两数中的大者为 :"<< M ax ( int( 'a') ,2)<< endl ;
(2)通过 提供<>里面的参数类型 来调用这个模板,直接模板实例化,然后类型不匹配的参数会发生隐式的类型转换,如下所示
cout <<"2,2.3两数中的大者为:"<< M ax <double> (2,2.3)<< endl
cout <<" 'a',2 两数中的大者为 :"<< M ax <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  模板形参表

函数模板形参表中除了可以出现用 typename class 关键字声明的类型参数外,还可以出现确定类型参数,称为 非类型参数 。例如:
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被声明为 B ubble S ort函数模板的非类型参数后,在模板定义的余下部分,它被当做一个常量值使用。由于 B ubble S ort函数不是通过引用来传递数组,故在模板调用时,必须显式指定模板实参,明确指定数组的大小。如果 B ubble S ort函数是通过 引用 来传递数组,则在模板调用时,就可以不显式指定模板实参,而由编译器自动推断出来,如下所示:

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

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

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

1.4 函数模板重载

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

结果是错误的。 其原因是:函数调用 Max(s1,s2) 的实参类型为 char* ,编译器用 char* 实例化函数模板 T Max(T x,T y) ,生成下面的模板函数:
char*Max(char * x,char *y){  return x>y? x:y ;  }
这里实际比较的不是两个字符串,而是两个字符串的地址。那一个字符串的存储地址高,就输出那个字符串。从输出结果看,应该是 Beijing 2008 的地址高。为了验证这一点,我们用语句:
cout <<&s1<<"  "<<&s2<< endl ;
输出 s1 s2 的地址,结果为:
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,T y) ,生成下面的模板函数:
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++ 中更多的使用是在类的定义中,最常见的就是 STL Standard Template Library )和 ATL ActiveX Template Library ),它们都是作为 ANSI C++ 标准集成在 VC++ 开发环境中的标准模板库。
类模板的一般定义格式如下:

    template<classT1, classT2 , … >

    class类名

    {

        类体

    } ;

class 也可以用 typename 代替

//Stack.h
const 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< 模板参数列表 >
返回值类型 类名 < 模板参数名表 >:: 成员函数名 ( 参数列表 )
{  …  };
例如,上例 Stack Push 成员函数的定义:
template<class T >   // 类模板声明
void Stack <T>: :Push(T e){  …  }

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

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

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

在声明了一个类模板后,怎样使用它?
Stack< int int_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.h Pop() 成员函数的类外定义删掉,同时将 Stack 中的 StackEmpty () StackFull () 这两个函数的定义修改为如下的声明,然后再编译运行该程序,可以发现程序同样可以正确执行。
class Stack {
    …
     bool StackEmpty ();  // 判断栈是否为空
    bool StackFull ();        // 判断栈是否已满
};
由于 类模板 包含类型参数,因此又称为 参数化的类 。如果说类是对象的抽象,对象是类的实例,则类模板是类的抽象,模板类是类模板的实例。利用类模板可以建立含各种数据类型的类。

2.2 类模板参数

2.2.1 非类型参数

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

//Stack.h
template <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类模板,
//它有一个类模板类型的模板参数Seq
template <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
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值