第6章 模板

学习目标

★ 掌握模板的概念

★ 掌握函数模板的定义与实例化

★ 掌握函数模板重载

★ 掌握类模板的定义与实例化

★ 掌握类模板的派生

★ 掌握模板类与友元函数

★ 掌握模板的参数

★ 了解模板特化

模板是C++支持参数化多态的工具,是泛型编程的基础。模板可以实现类型参数化,即把类型定义为参数,真正实现了代码的可重用性,减少了编程及维护的工作量,并且降低了编程难度。模板是类或函数在编译时定义所需处理和返回的数据类型。一个模板是类或函数的描述,即模板分为函数模板和类模板,本章将针对函数模板和类模板的相关知识进行详细讲解。

6.1 模板的概念

在C++程序中,声明变量、函数、对象等实体时,程序设计者需要指定数据类型,让编译器在程序运行之前进行类型检查并分配内存,以提高程序运行的安全性和效率。但是这种强类型的编程方式往往会导致程序设计者为逻辑结构相同而具体数据类型不同的对象编写模式一致的代码。例如,定义一个求和函数add(int,int),add()函数可以计算两个int类型数据的和,但是对于double类型的数据就无能为力了,此时,程序设计者还需要定义一个函数add(float,float),计算两个double类型的数据之和,但是这样不利于程序的扩充和维护。

为此,C++标准提供了模板机制,用于定义数据类型不同但逻辑结构相同的数据对象的通用行为。在模板中,运算对象的类型不是实际的数据类型,而是一种参数化的类型。带参数类型的函数称为函数模板,带参数类型的类称为类模板。例如,定义函数add(),计算两个数之和,可以将类型参数化,如add(T,T),其中,T就是参数化的类型,在调用add()函数时,可以传入任意类型的数据,函数可以根据传入的数据推导出T的值是哪种数据类型,从而进行相应的计算。这样程序设计者就可以专注于逻辑代码的编写,而不用关心实际具体的数据类型。模板就像生产模具,例如,中秋生产月饼,生产月饼的模具就是模板,在做模具时,只关心做出什么样式的月饼,而不用关心月饼具体的原料是什么(如面粉、糯米粉、玉米粉等)。

程序运行时,模板的参数由实际参数的数据类型决定,编译器会根据实际参数的数据类型生成相应的一段可运行代码,这个过程称为模板实例化。函数模板生成的实例称为模板函数,类模板生成的实例称为模板类。

6.2 函数模板

函数模板是函数的抽象,它与普通函数相似,唯一的区别就是函数参数的类型是不确定的,函数参数的类型只有在调用过程中才被确定。本节将针对函数模板的用法进行详细讲解。

6.2.1 函数模板的定义

如果定义一个实现两个数相加的函数add(),要实现int、float、double等多种类型的数据相加,则要定义很多个函数,这样的程序就会显得非常臃肿。但使用模板就无须关心数据类型,只定义一个函数模板就可以。定义函数模板的语法格式如下所示:

template<typename 类型占位符>  
返回值类型 函数名(参数列表) 
{ 
 //函数体; 
} 

上述语法格式中,tem plate是声明模板的关键字,<>中的参数称为模板参数;typenam e关键字用于标识模板参数,可以用class关键字代替,class和typenam e并没有区别。模板参数不能为空,一个函数模板中可以有多个模板参数,模板参数和普通函数参数相似。tem plate下面是定义的函数模板,函数模板定义方式与普通函数定义方式相同,只是参数列表中的数据类型要使用<>中的参数名表示。

下面通过案例演示函数模板的用法,如例6-1所示。

例6-1 templateFunc.cpp

 1  #include<iostream> 
 2  using namespace std; 
 3  template<typename T>       //定义函数模板 
 4  T add(T t1,T t2) 
 5  { 
 6       return t1+t2; 
 7  } 
 8  int main() 
 9  { 
 10      cout<<add(1,2)<<endl;      //传入int类型参数 
 11      cout<<add(1.2,3.4)<<endl;   //传入double类型参数 
 12      return 0; 
 13 }

例6-1运行结果如图6-1所示。

图6-1 例6-1运行结果

在例6-1中,第3~7行代码定义了函数模板add(),用于实现两个数据相加。第10~11行代码调用add()函数,分别传入两个int类型数据和两个double类型数据。

由图6-1可知,当调用add()函数传入int类型参数1和2时,参数T被替换成int,得到结果为3;当传入double类型参数1.2和3.4时,参数T被替换成double类型,得到结果为4.6。这就避免了为int类型数据定义一个求和函数,再为double类型数据定义一个求和函数的问题,实现了代码复用。

需要注意的是,不能在函数调用的参数中指定模板参数的类型,对函数模板的调用应使用实参推演。例如,只能进行add(2,3)这样的调用,或者定义整型变量int a=2,b=3,再将变量a、b作为参数,进行add(a,b)这样的调用,编译器会根据传入的实参推演出T为int类型,而不能使用add(int,int)方式,直接将类型传入进行调用。

6.2.2 函数模板实例化

函数模板并不是一个函数,它相当于一个模子,定义一次即可使用不同类型的参数来调用该函数模板,这样做可以减少代码的书写,提高代码的复用性和效率。需要注意的是,函数模板不会减少可执行程序的大小,因为编译器会根据调用时的参数类型进行相应的实例化。所谓实例化,就是用类型参数替换模板中的模板参数,生成具体类型的函数。实例化可分为隐式实例化与显式实例化,下面分别介绍这两种实例化方式。

1. 隐式实例化

隐式实例化是根据函数调用时传入的参数的数据类型确定模板参数T的类型,模板参数的类型是隐式确定的,如例6-1中函数模板add()的调用过程。

在例6-1中第一次调用add()函数模板时,传入的是int类型数据1和2,编译器根据传入的实参推演出模板参数类型是int,就会根据函数模板实例化出一个int类型的函数,如下所示:

int add(int t1,int t2) 
{ 
    return t1 + t2; 
} 

编译器生成具体类型函数的这一过程就称为实例化,生成的函数称为模板函数。生成int类型的函数后,再传入实参1和2进行运算。同理,当传入double类型的数据时,编译器先根据模板实例化出如下形式的函数:

double add(double t1,double t2) 
{ 
    return t1 + t2; 
} 

这样,每一次调用时都会根据不同的类型实例化出不同类型的函数,最终的可执行程序的大小并不会减少,只是提高了代码的复用性。

2. 显式实例化

隐式实例化不能为同一个模板参数指定两种不同的类型,如add(1,1.2),函数参数类型不一致,编译器便会报错。这就需要显式实例化解决类型不一致的问题。显式实例化需要指定函数模板中的数据类型,语法格式如下所示:

template 函数返回值类型 函数名<实例化的类型>(参数列表); 

在上述语法格式中,<>中是显式实例化的数据类型,即要实例化出一个什么类型的函数。例如,显示实例化为int类型,则在调用时,不是int类型的数据会转换为int类型再进行计算,如将例6-1中的add()函数模板显式实例化为int类型,代码如下所示:

template int add<int>(int t1, int t2); 

下面通过案例演示函数模板add()显式实例化的用法,如例6-2所示。

例6-2 explicit.cpp

 1  #include<iostream> 
 2  using namespace std; 
 3  template< typename T> 
 4  T add(T t1,T t2) 
 5  { 
 6       return t1+t2; 
 7  }
 8  template int add<int>(int t1,int t2);   //显式实例化为int类型 
 9  int main() 
 10 { 
 11      cout<<add<int>(10,'B')<< endl;     //函数模板调用 
 12      cout<<add(1.2,3.4)<< endl; 
 13      return 0; 
 14 }

例6-2运行结果如图6-2所示。

图6-2 例6-2运行结果

在例6-2中,第8行代码显式声明add()函数模板,指定模板参数类型为int。在调用int类型模板函数时,传入了一个字符'B',则编译器会将字符类型的'B'转换为对应的ASCII码值,然后再与10相加得出结果。实际上就是隐式的数据类型转换。

需要注意的是,对于给定的函数模板,显式实例化声明在一个文件中只能出现一次,并且在这个文件中必须给出函数模板的定义。由于C++编译器的不断完善,模板实例化的显式声明可以省略,在调用时用<>显式指定要实例化的类型即可,如例6-2中如果add(1.2,3.4)函数调用改为add<int>(1.2,3.4)调用,则会得出结果4。

多学一招:显式具体化

函数模板的显式具体化是对函数模板的重新定义,具体格式如下所示:

template< > 函数返回值类型 函数名<实例化类型>(参数列表) 
{ 
 //函数体重新定义 
} 

显式实例化只需要显式声明模板参数的类型而不需要重新定义函数模板的实现,而显式具体化需要重新定义函数模板的实现。例如,定义交换两个数据的函数模板,示例代码如下:

template<typename T> 
void swap(T& t1,T& t2) 
{ 
    T temp = t1; 
    t1 = t2; 
    t2 = temp; 
}

但现在有如下结构体定义,示例代码如下:

struct Student 
{ 
    int id; 
    char name[40]; 
    float score; 
}; 

现在要调换两个学生的id编号,但是又不想交换学生的姓名、成绩等其他信息,那么此时就可以用显式具体化解决这个问题,重新定义函数模板只交换结构体的部分数据成员。显式具体化的代码如下所示:

template<> void swap<Student>(Student& st1, Student& st2) 
{ 
 int temp = st1.id; 
 st1.id = st2.id; 
 st2.id = temp; 
} 

如果函数有多个原型,则编译器在选择函数调用时,非模板函数优先于模板函数,显式具体化模板优先于函数模板,例如下面三种定义:

void swap(int&, int&);       //直接定义 
template<typename T>        //模板定义 
void swap(T& t1, T& t2); 
template<> void swap<int>(int&, int&);   //显式具体化 

对于int a,int b,如果存在swap(a,b)的调用,则优先调用直接定义的函数;如果没有,则优先调用显式具体化,如果两者都没有才会调用函数模板。

6.2.3 函数模板重载

函数模板可以进行实例化,以支持不同类型的参数,不同类型的参数调用会产生一系列重载函数。如例6-1中两次调用add()函数模板,编译器会根据传入参数不同实例化出两个函数,如下所示:

int add(int t1,int t2)      //int类型参数实例化出的函数 
{ 
 return t1 + t2; 
} 
double add(double t1,double t2)     //double类型参数实例化出的函数 
{ 
 return t1+t2; 
}

此外,函数模板本身也可以被重载,即名称相同的函数模板可以具有不同的函数模板定义,当进行函数调用时,编译器根据实参的类型与个数决定调用哪个函数模板实例化函数。

下面通过案例演示函数模板重载的用法,如例6-3所示。

例6-3 templateFunc.cpp

 1  #include<iostream> 
 2  using namespace std; 
 3  int max(const int& a,const int& b)     //非模板函数,求两个int类型数据的较大值 
 4  { 
 5       return a>b ? a:b; 
 6  } 
 7  template< typename T>        //定义求两个任意类型数据的较大值 
 8  T max(const T& t1,const T& t2) 
 9  { 
 10      return t1>t2 ? t1:t2; 
 11 } 
 12 template<typename T>       //定义求三个任意类型数据的最大值 
 13 T max(const T& t1,const T& t2,const T& t3) 
 14 { 
 15      return max(max(t1,t2),t3); 
 16 } 
 17 int main() 
 18 { 
 19      cout<<max(1,2)<<endl;       //调用非模板函数 
 20      cout<<max(1,2,3)<<endl;     //调用三个参数的函数模板 
 21      cout<<max('a','e')<<endl;    //调用两个参数的函数模板 
 22      cout<<max(6,3.2)<<endl;      //调用非模板函数 
 23      return 0; 
 24 } 

例6-3运行结果如图6-3所示。

图6-3 例6-3运行结果

在例6-3中,第3~6行代码定义了一个函数m ax(),用于比较两个int类型数据的大小。第7~11行代码定义了函数模板m ax(),用于比较两个数的大小。第12~16行代码定义了函数模板m ax(),用于比较三个数的大小。第19~22行代码分别传入不同的参数调用函数m ax()。

在调用的过程中,如果参数相同,那么优先调用非模板函数而不会用模板产生实例。例如,第19行代码调用m ax()函数,传入两个int类型参数,很好地匹配了非模板函数。

如果函数模板能够实例化出一个更匹配的函数,则调用时将选择函数模板。例如,第21行代码调用m ax()函数,利用函数模板实例化一个带有两个char类型参数的函数,而不会调用非模板函数m ax(int,int)。

需要注意的是,模板不允许自动类型转化,如果有不同类型参数,只允许使用非模板函数,因为普通函数可以进行自动类型转换,所以第22行代码调用m ax()函数时,调用的是非模板函数,将3.2转换成了int类型再与6进行比较。

脚下留心:使用函数模板要注意的问题

函数模板虽然可以极大地解决代码重用的问题,但读者在使用时仍需注意以下几个方面:

(1)<>中的每一个类型参数在函数模板参数列表中必须至少使用一次。例如,下面的函数模板声明是不正确的。

template<typename T1, typename T2> 
void func(T1 t) 
{ 
} 

函数模板声明了两个参数T1与T2,但在使用时只使用了T1,没有使用T2。

(2)全局作用域中声明的与模板参数同名的对象、函数或类型,在函数模板中将被隐藏。例如:

int num; 
template<typename T> 
void func(T t) 
{ 
 T num; 
 cout<<num<<endl;    //输出的是局部变量num,全局int类型的num被屏蔽 
} 

在函数体内访问的num是T类型的变量num,而不是全局int类型的变量num。

(3)函数模板中声明的对象或类型不能与模板参数同名。例如:

template<typename T> 
void func(T t) 
{ 
 typedef float T;     //错误,定义的类型与模板参数名相同 
} 

(4)模板参数名在同一模板参数列表中只能使用一次,但可在多个函数模板声明或定义之间重复使用。例如:

template<typename T, typename T>  //错误,在同一个模板中重复定义模板参数 
void func1(T t1, T t2){} 
template<typename T> 
void func2(T t1){} 
template<typename T>     //在不同函数模板中可重复使用相同的模板参数名 
void func3(T t1){}

(5)模板的定义和多处声明所使用的模板参数名不是必须相同。例如:

//模板的前向声明 
template<typename T> 
void func1(T t1, T t2); 
//模板的定义 
template<typename U> 
void func1(U t1, U t2) 
{ 
} 

(6)如果函数模板有多个模板参数,则每个模板参数前都必须使用关键字class或typename修饰。例如:

template<typename T, typename U>     //两个关键字可以混用 
void func(T t, U u){} 
template<typename T,U>     //错误,每一个模板参数前都必须有关键字修饰 
void func(T t, U u){} 

6.3 类模板

类也可以像函数一样被不同的类型参数化,如STL中的vector容器就是典型的例子,使用vector不需要关心容器中的数据类型,就可以对数据进行操作。

6.3.1 类模板定义与实例化

函数可以定义函数模板,同样地,对于类来说,也可以定义一个类模板。类模板是针对成员数据类型不同的类的抽象,它不是一个具体实际的类,而是一个类型的类,一个类模板可以生成多种具体的类。类模板的定义格式如下所示:

template<typename 类型占位符> 
class 类名 
{ 
}

类模板中的关键字含义与函数模板相同。需要注意的是,类模板的模板参数不能为空。一旦声明类模板,就可以用类模板的参数名声明类中的成员变量和成员函数,即在类中使用数据类型的地方都可以使用模板参数名来声明。定义类模板示例代码如下所示:

template<typename T> 
class A 
{ 
public: 
 T a; 
 T b; 
 T func(T a, T b); 
};

上述代码中,在类A中声明了两个T类型的成员变量a和b,还声明了一个返回值类型为T并带两个T类型参数的成员函数func()。

定义了类模板就要使用类模板创建对象以及实现类中的成员函数,这个过程其实也是类模板实例化的过程,实例化出的具体类称为模板类。如果用类模板创建类的对象,例如,用上述定义的类模板A创建对象,则在类模板A后面加上一个<>,并在里面表明相应的类型,示例代码如下所示:

A<int> a; 

这样类A中凡是用到模板参数的地方都会被int类型替换。如果类模板有多个模板参数,创建对象时,多个类型之间要用逗号分隔开。例如,定义一个有两个模板参数的类模板B,然后用B创建类对象,示例代码如下所示:

template<typename T1, typename T2> 
class B 
{ 
public: 
 T1 a; 
 T2 b; 
 T1 func(T1 a, T2& b); 
}; 
B<int,string> b; //创建模板类B<int,string>的对象b 

使用类模板时,必须要为模板参数显式指定实参,不存在实参推演过程,也就是说不存在将整型值10推演为int类型再传递给模板参数的过程,必须要在<>中指定int类型,这一点与函数模板不同。

下面通过案例演示类模板的实例化,如例6-4所示。

例6-4 classTemplate.cpp

 1  #include<iostream> 
 2  using namespace std; 
 3 template< typename T>       //类模板的定义
 4  class Array 
 5  { 
 6  private: 
 7       int _size; 
 8       T* _ptr; 
 9  public: 
 10      Array(T arr[], int s); 
 11      void show(); 
 12 }; 
 13 template<typename T>      //类模板外定义其成员函数 
 14 Array<T>::Array(T arr[], int s) 
 15 { 
 16      _ptr = new T[s]; 
 17      _size = s; 
 18      for (int i=0;i<_size; i++) 
 19      { 
 20           _ptr[i]=arr[i]; 
 21      } 
 22 } 
 23 template<typename T>      //类模板外定义其成员函数 
 24 void Array<T>::show() 
 25 { 
 26      for(int i=0;i<_size;i++) 
 27           cout<<*(_ptr + i)<<" "; 
 28      cout<<endl; 
 29 } 
 30 int main() 
 31 { 
 32      char cArr[] = { 'a', 'b', 'c', 'd', 'e' }; 
 33      Array<char> a1(cArr, 5);     //创建类模板的对象 
 34      a1.show(); 
 35      int iArr[10] = { 1, 2, 3, 4, 5, 6 }; 
 36      Array<int> a2(iArr, 10); 
 37      a2.show(); 
 38      return 0; 
 39 } 

例6-4运行结果如图6-4所示。

图6-4 例6-4运行结果

在例6-4中,第3~12行代码定义了一个类模板Array,Array的构造函数有一个数组类型参数。第33行代码在创建类对象a1时,用char类型的数组去初始化,调用show()函数输出数组元素。第36行代码创建对象a2时,用int类型的数组去初始化,调用show()函数输出数组元素。由图6-4可知,两个数组的元素都成功输出。

需要注意的是,类模板在实例化时,带有模板参数的成员函数并不会跟着实例化,这些成员函数只有在被调用时才会被实例化。

6.3.2 类模板的派生

类模板和普通类一样也可以继承和派生,以实现代码复用。类模板的派生一般有三种情况:类模板派生普通类、类模板派生类模板、普通类派生类模板。这三种派生关系可以解决很多实际问题。下面针对这三种派生关系进行讲解。

1. 类模板派生普通类

在C++中,可以从任意一个类模板派生一个普通类。在派生过程中,类模板先实例化出一个模板类,这个模板类作为基类派生出普通类。类模板派生普通类的示例代码如下所示:

template<typename T> 
class Base        //类模板Base 
{ 
private: 
 T x; 
 T y; 
public: 
 Base(); 
 Base(T x, T y); 
 Base getx(); 
 Base gety(); 
 ~ Base(); 
}; 
class Derive:public Base<double>   //普通类Derive公有继承类模板Base 
{ 
private: 
 double num; 
public: 
 Derive(double a, double b, double c):num(c), Base<double>(a, b){} 
}; 

在上述代码中,类模板Base派生出了普通类D erive,其实在这个派生过程中类模板Base先实例化出了一个double类型的模板类,然后由这个模板类派生出普通类Derive,因此在派生过程中需要指定模板参数类型。

2. 类模板派生类模板

类模板也可以派生出一个新的类模板,它和普通类之间的派生几乎完全相同。但是,派生类模板的模板参数受基类模板的模板参数影响。例如,由类模板Base派生出一个类模板D erive,示例代码如下:

template<typename T> 
class Base 
{ 
public: 
 T _a; 
public: 
 Base(T n):_a(n) {} 
 T get() const { return _a; } 
}; 
template<typename T, typename U> 
class Derive:public Base<U> 
{ 
public: 
 U _b; 
public: 
 Derive(T t, U u):Base<T>(t), _b(u) {} 
 U sum() const { return _b + U(Base::get()); } 
}; 

上述代码中,类模板D erive由类模板Base派生,D erive的部分成员变量和成员函数类型由类模板Base的参数U确定,因此Derive仍然是一个模板。类模板派生类模板技术可以用来构建类模板的层次结构。

3. 普通类派生类模板

普通类也可以派生类模板,普通类派生类模板可以把现存类库中的类转换为通用的类模板,但在实际编程中,这种派生方式并不常用,本书只对它作一个简单示例,读者只需要了解即可。普通类派生类模板示例代码如下所示:

class Base 
{ 
 int _a; 
public: 
 Base(int n):_a(n){} 
 int get() const {return _a;} 
}; 
template<typename T> 
class Derive: public Base
{ 
 T _b; 
public: 
 Derive(int n, T t):Base(n), _b(t){} 
 T sum() const {return _b + (T)get();} 
}; 

在上述代码中,类Base是普通类,类模板D erive继承了普通类Base。利用这种技术,程序设计者能够从现存类中创建类模板,由此可以创建基于非类模板库的类模板。

6.3.3 类模板与友元函数

在类模板中声明友元函数有三种情况:非模板友元函数、约束模板友元函数和非约束模板友元函数。接下来,将针对这三种友元函数进行详细讲解。

1. 非模板友元函数

非模板友元函数就是将一个普通函数声明为友元函数。例如,在一个类模板中声明一个友元函数,示例代码如下:

template<typename T> 
class A 
{ 
 T _t; 
public: 
 friend void func(); 
}; 

在类模板A中,将普通函数func()声明为友元函数,则func()函数是类模板A所有实例的友元函数。上述代码中,func()函数为无参函数。除此之外,还可以将带有模板类参数的函数声明为友元函数,示例代码如下:

template<typename T> 
class A 
{ 
 T _t; 
public: 
 friend void show(const A<T>& a); 
};

在上述代码中,show()函数并不是函数模板,只是有一个模板类参数。调用带有模板类参数的友元函数时,友元函数必须显式具体化,指明友元函数要引用的参数的类型,例如:

void show(const A<int>& a); 
void show(const A<double>& a)

上述代码中,模板参数为int类型的show()函数是A<int>类的友元函数,模板参数为double类型的show()函数是A<double>类的友元函数。

下面通过案例演示非模板友元函数的用法,如例6-5所示。

例6-5 friendTeleplate.cpp

 1  #include<iostream> 
 2  using namespace std; 
 3  template<typename T> 
 4  class A 
 5  { 
 6       T _item; 
 7       static int _count;       //静态变量 
 8  public: 
 9       A(const T& t) :_item(t){ _count++; } 
 10      ~A(){ _count--; } 
 11      friend void func();      //无参友元函数func() 
 12      friend void show(const A<T>& a);    //有参友元函数show() 
 13 }; 
 14 template<typename T> 
 15 int A<T>::_count = 0;       //初始化静态变量
 16void func()         //func()函数实现 
 17 { 
 18      cout<<"int count:"<<A<int>::_count<<";"; 
 19      cout<<"double count:"<<A<double>::_count<<";"<<endl; 
 20 } 
 21 //模板参数为int类型 
 22 void show(const A<int>& a){cout<<"int:"<<a._item<<endl;} 
 23 void show(const A<double>& a){cout<<"double:"<<a._item<<endl;} 
 24 int main() 
 25 { 
 26      func();          //调用无参友元函数 
 27      A<int> a(10);        //创建int类型对象 
 28      func(); 
 29      A<double> b(1.2); 
 30      show(a);         //调用有参友元函数 
 31      show(b); 
 32      return 0; 
 33 } 

例6-5运行结果如图6-5所示。

在例6-5中,第3~13行代码定义了类模板A,在类模板A中声明了两个友元函数func()和show()。其中,func()函数为无参友元函数,show()函数有一个模板类对象作为参数。此外,类模板A还声明了静态成员变量_count,用于记录每一种模板类创建的对象个数。

第16~20行代码是func()函数的定义,func()函

图6-5 例6-5运行结果

数的作用是输出A<int>类对象和A<double>类对象的个数。第22~23行代码分别定义了show(const A<int>&a)函数和show(const A<double>&a)函数,用于分别输出A<int>类对象和A<double>类对象的值。

在m ain()函数中,第26行代码调用func()函数,此时还未创建任何模板类对象,由图6-5可知,A<int>类对象和A<double>类对象的个数均为0。第27行代码创建了A<int>模板类对象a,初始化值为10。第28行再次调用func()函数,由图6-5可知,A<int>类对象的个数为1,A<double>类对象的个数为0。第29行代码创建A<double>模板类对象b,初始化值为1.2。第30~31行代码调用show()函数,分别传入对象a和对象b作为参数,由图6-5可知,程序成功输出了对象a和对象b的值。

2. 约束模板友元函数

约束模板友元函数是将一个函数模板声明为类的友元函数。函数模板的实例化类型取决于类模板被实例化时的类型,类模板实例化时会产生与之匹配的具体化友元函数。

在使用约束模板友元函数时,首先需要在类模板定义的前面声明函数模板。例如,有两个函数模板声明,示例代码如下:

template<typename T> 
void func(); 
template<typename T> 
void show(T& t); 

声明函数模板之后,在类模板中将函数模板声明为友元函数。在声明友元函数时,函数模板要实现具体化,即函数模板的模板参数要与类模板的模板参数保持一致,以便类模板实例化时产生与之匹配的具体化友元函数。示例代码如下所示:

template<typename U>     //类模板的定义 
class A 
{ 
…         //其他成员 
 friend void func<U>();     //声明无参友元函数func() 
 friend void show<>(A<U>& a);    //声明有参友元函数show() 
…         //其他成员 
}; 

在上述代码中,将函数模板func()与show()声明为类的友元函数,在声明时,func()与show()的模板参数受类模板A的模板参数约束,与类模板的模板参数相同。当生成A<int>模板类时,会生成与之匹配的func<int>()函数和show<int>()函数作为友元函数。需要注意的是,在上述代码中,func()函数模板没有参数,必须使用<>指定具体化的参数类型。show()函数模板有一个模板类参数,编译器可以根据函数参数推导出模板参数,因此show()函数模板具体化中<>可以为空。

下面通过案例演示约束模板友元函数的用法,如例6-6所示。

例6-6 bindTemplate.cpp

 1  #include<iostream> 
 2  using namespace std; 
 3 template<typename T>     //声明函数模板func() 
 4  void func(); 
 5 template<typename T>     //声明函数模板show() 
 6  void show(T& t); 
 7 template<typename U>     //类模板的定义 
 8  class A 
 9  { 
 10 private: 
 11      U _item; 
 12      static int _count; 
 13 public: 
 14      A(const U& u):_item(u){_count++;} 
 15      ~A(){_count--;} 
 16      friend void func<U>();      //声明友元函数func() 
 17      friend void show<>(A<U>& a);    //声明友元函数show() 
 18 }; 
 19 template<typename T> 
 20 int A<T>::_count = 0; 
 21 template<typename T>     //函数模板func()的定义 
 22 void func() 
 23 { 
 24      cout<<"template size:"<<sizeof(A<T>)<<";"; 
 25      cout<<"template func():"<<A<T>::_count<<endl; 
 26 } 
 27 template<typename T>     //函数模板show()的定义 
 28 void show(T& t){cout<< t._item<<endl;} 
 29 int main() 
 30 { 
 31      func<int>();        //调用int类型的函数模板实例,int类型,其大小为4字节 
 32      A<int> a(10);        //定义A<int>类对象a 
 33      A<int> b(20);        //定义A<int>类对象b 
 34      A<double> c(1.2);       //定义A<double>类对象c 
 35      show(a);         //调用show()函数,输出类对象a的值 
 36      show(b);        //调用show()函数,输出类对象b的值 
 37      show(c);        //调用show()函数,输出类对象c的值 
 38      cout<<"func<int>output:\n"; 
 39      func<int>();         //运行到此,已经创建了两个int类型对象 
 40      cout<<"func<double>()output:\n"; 
 41      func<double>(); 
 42      return 0; 
 43 }

例6-6运行结果如图6-6所示。

在例6-6中,第3~7行代码声明函数模板func()和show()。第16~17行代码分别将函数模板func()与show()声明为类的友元函数。第21~26行代码是函数模板func()的定义,用于输出某一类型模板类的大小及对象个数。第27~28行代码是函数模板show()的定义,用于输出模板类对象的值。

在m ain()函数中,第31行代码调用func<int>(),即输出A<int>类的大小及对象个数。由图6-6可知,A<int>类的大小为4,对象个数为0。第32~34行代码分别定义A<int>类对象a和b,A<double>类对象c。第35~37行代码调用show()函数,分别传入对象a、b、c作为参数,输出各对象的值。由图6-6可知,show()函数三次调用成功输出了各对象的值。第39行代码调用func<int>()函数,由于此时已经创建了两个A<int>类对象a和b,因此输出的对象个数应当为2。由图6-6可知,A<int>类大小为4,对象个数为2。第41行代码调用func<double>()函数,由于此时已经创建了一个A<double>类对象c,因此输出的对象个数应当为1。

由图6-6可知,A<double>类的大小为8,对象个数为1。

图6-6 例6-6运行结果

3. 非约束模板友元函数

非约束模板友元函数是将函数模板声明为类模板的友元函数,但函数模板的模板参数不受类模板影响,即友元函数模板的模板参数与类模板的模板参数是不同的。

声明非约束模板友元函数示例代码如下所示:

template<typename T> 
class A 
{ 
 template<typename U, typename V> 
 friend void show(U& u, V& v); 
}; 

在上述代码中,类模板A将函数模板show()声明为友元函数,但show()的模板参数U、V不受类模板A的模板参数T影响,则函数模板show()就是类模板A的非约束友元函数。函数模板show()的每个模板函数都是类模板A每个模板类的友元函数。

下面通过案例演示非约束模板友元函数的用法,如例6-7所示。

例6-7 freeTemplate.cpp

 1  #include<iostream> 
 2  using namespace std; 
 3 template<typename T>    //定义类模板A 
 4  class A 
 5  { 
 6  private: 
 7       T _item; 
 8  public: 
 9       A(const T& t) :_item(t){} 
 10      template<class U, class V>    //声明非约束模板友元函数 
 11      friend void show(U& u, V& v); 
 12 }; 
 13 template<typename U, typename V>   //函数模板show()的定义 
 14 void show(U& u, V& v){cout<<u._item<<","<<v._item<<endl;} 
 15 int main() 
 16 { 
 17      A<int> a(10);      //定义A<int>类对象a 
 18      A<int> b(20);      //定义A<int>类对象b 
 19      A<double> c(1.2);     //定义A<int>类对象c 
 20      cout<<"a,b: ";   
 21      show(a, b);     //调用show()函数,传入对象a、b作为实参 
 22      cout<<"a,c:"; 
 23      show(a, c);     //调用show()函数,传入对象a、c作为实参 
 24      return 0; 
 25 }

例6-7运行结果如图6-7所示。

图6-7 例6-7运行结果

在例6-7中,第3~12行代码定义类模板A,在类模板中将函数模板show()声明为非约束友元函数。第13~14行代码是函数模板show()的定义。第17~19行代码分别定义A<int>类对象a和b,A<double>类对象c。第21行代码调用show()函数,传入对象a、b作为实参;第23行代码调用show()函数,传入对象a、c作为实参。由图6-7可知,两次show()函数调用都成功输出了各个对象的值。由此可知,非约束模板友元函数的模板参数与类模板的模板参数不相关,它可以接受任何类型的参数。

6.4 模板的参数

模板是C++支持参数化多态的工具,模板的参数有三种类型:类型参数、非类型参数和模板类型参数。本节就针对这三种模板参数进行详细讲解。

1. 类型参数

由class或者typenam e标记的参数,称为类型参数。类型参数是使用模板的主要目的。例如,下列模板声明:

template<typename T> 
T add(T t1,T t2);

上述代码中,T就是一个类型参数,类型参数的名称由用户自行确定,表示的是一个未知类型。模板的类型参数可以作为类型说明符用在模板中的任何地方,与内置类型说明符或类类型说明符的使用方式完全相同。可以为模板定义多个类型参数,也可以为类型参数指定默认值,示例代码如下所示:

template<typename T, typename U = int> 
class A 
{ 
public: 
 void func(T, U); 
}; 

在上述代码中,设置类型参数U的默认值为int类型,类模板的类型参数默认值和普通函数默认参数规则一致。

2. 非类型参数

非类型参数是指内置类型参数。例如,定义如下模板:

template<typename T, int a> 
class A 
{ 
}; 

上述代码中,int a就是非类型的模板参数,非类型模板参数为函数模板或类模板预定义一些常量,在模板实例化时,也要求实参必须是常量,即确切的数据值。需要注意的是,非类型参数只能是整型、字符型或枚举、指针、引用类型。

非类型参数在所有实例中都具有相同的值,而类型参数在不同的实例中具有不同的值。

下面通过案例演示非类型参数的用法,如例6-8所示。

例6-8 typeTemplate.cpp

 1  #include<iostream> 
 2  using namespace std; 
 3  template<typename T, unsigned len>  //非类型参数unsigned len 
 4  class Array 
 5  { 
 6  public: 
 7       T& operator[](unsigned i)  //重载“[]”运算符 
 8       { 
 9            if(i >= len) 
 10                     cout << "数组越界" << endl; 
 11           else 
 12                     return arr[i]; 
 13      } 
 14 private: 
 15      T arr[len]; 
 16 }; 
 17 int main() 
 18 { 
 19      Array<char, 5> arr1;     //定义一个长度为5的char类型数组 
 20      Array<int, 10> arr2;     //定义一个长度为10的int类型数组 
 21      arr1[0]= 'A'; 
 22      cout<<arr1[0]<<endl; 
 23      for(int i = 0; i < 10; i++)   //为int类型数组arr2赋值并输出 
 24           arr2[i] = i + 1; 
 25      for(int i = 0; i < 10; i++) 
 26           cout<<arr2[i]<< " "; 
 27           cout<<endl; 
 28      return 0; 
 29 }

例6-8运行结果如图6-8所示。

图6-8 例6-8运行结果

在例6-8中,第3~16行代码定义了类模板Array,类模板Array的第二个参数为非类型参数unsigned len。第7~10行代码重载了“[]”运算符,用于遍历数组元素。在m ain()函数中,第19~20行代码实例化类模板Array时,分别定义了长度为5和10的两个数组。第二个参数都是具体的数值,解决了常量参数只能固定大小的问题。当需要为同一算法或类定义不同常量时,最适合用非类型参数实现。

使用非类型参数时,有以下几点需要注意。

(1)调用非类型参数的实参必须是常量表达式,即必须能在编译时计算出结果。

(2)任何局部对象、局部变量的地址都不是常量表达式,不能用作非类型的实参,全局指针类型、全局变量也不是常量表达式,也不能用作非类型的实参。

(3)sizeof()表达式结果是一个常量表达式,可以用作非类型的实参。

(4)非类型参数一般不用于函数模板。

3. 模板类型参数

模板类型参数就是模板的参数为另一个模板,声明格式如下所示:

template<typename T, template<typename U, typename Z> class A> 
class Parameter 
{ 
    A<T,T> a; 
};

上述代码中,类模板Param eter的第二个模板参数就是一个类模板。需要注意的是,只有类模板可以作为模板参数,参数声明中必须要有关键字class。

6.5 模板特化

特化就是将泛型的东西具体化,模板特化就是为已有的模板参数进行具体化的指定,使得不受任何约束的模板参数受到特定约束或完全被指定。

通过模板特化可以优化基于某种特定类型的实现,或者克服某种特定类型在实例化模板时出现的不足,如该类型没有提供某种操作。例如,有以下类模板定义:

template<typename T> 
class Special 
{ 
public: 
 Special(T a, T b) 
 { 
      _a = a; 
      _b = b; 
 } 
 T compare() 
 { 
      return _a > _b ? _a : _b; 
 } 
private: 
 T _a; 
 T _b; 
}; 

上述代码中,类模板Special定义了一个成员函数com pare(),用于比较两个成员变量_a和_b的大小。如果实例化为Special<string>类和Special<const char*>类,则目的都是比较两个字符串的大小。但是,由于const char*类型没有提供“>”运算操作,因此,Special<const char*>类对象调用com pare()函数时,比较的是两个字符串的地址大小,这显然是没有意义的。为了解决const char*特殊类型所产生的问题,可以将类模板特化。

模板特化可分为全特化与偏特化,下面分别进行介绍。

1. 全特化

全特化就是将模板中的模板参数全部指定为确定的类型,其标志就是产生出完全确定的东西。对于类模板,包括类的所有成员函数都要进行特化。进行类模板特化时,需要将类的成员函数重新定义为普通成员函数。

在全特化时,首先使用tem plate<>进行全特化声明,然后重新定义需要全特化的类模板,并指定特化类型。下面通过案例演示类模板的全特化,如例6-9所示。

例6-9 specTemplate.cpp

 1  #include<iostream> 
 2  using namespace std; 
 3  //类模板 
 4  template<typename T> 
 5  class Special 
 6  { 
 7  public: 
 8       Special(T a, T b) 
 9       { 
 10           _a = a; 
 11           _b = b; 
 12      } 
 13      T compare() 
 14      { 
 15           cout<< "类模板" << endl; 
 16           return _a > _b ? _a : _b; 
 17      } 
 18 private: 
 19      T _a; 
 20      T _b; 
 21 }; 
 22 //类模板全特化 
 23 template<> 
 24 class Special<const char*>    //指定特化类型为const char* 
 25 { 
 26 public: 
 27      Special(const char* a, const char* b) 
 28      { 
 29           _a = a; 
 30           _b = b; 
 31      } 
 32      const char* compare()   //重新定义成员函数compare() 
 33      { 
 34           cout<< "类模板特化" << endl; 
 35           if (strcmp(_a, _b)>0) 
 36                return _a; 
 37           else 
 38                return _b; 
 39      } 
 40 private: 
 41      const char* _a; 
 42      const char* _b; 
 43 }; 
 44 int main() 
 45 { 
 46      //创建Special<string>类对象s1 
 47      Special<string> s1("hello", "nihao"); 
 48      cout<< s1.compare() << endl; 
 49      //创建Special<const char*>类对象s2 
 50      Special<const char*> s2("hello", "nihao"); 
 51      cout<< s2.compare() << endl; 
 52      return 0; 
 53 }

例6-9运行结果如图6-9所示。

图6-9 例6-9运行结果

在例6-9中,第4~21行代码定义了类模板Special。第23~43行代码是类模板Special针对const char*数据类型的全特化,其中的第32~39行代码重新定义成员函数com pare()为普通成员函数,实现const char*类字符串的比较。第47~48行代码定义Special<string>类对象s1,并调用com pare()函数比较两个字符串大小;第50~51行代码定义Special<const char*>类对象s2,并调用com pare()函数比较两个字符串大小。由图6-9可知,对象s1调用的是类模板实例化出的com pare()函数,对象s2调用的是全特化之后的com pare()函数。

2. 偏特化

偏特化就是模板中的模板参数没有被全部指定,需要编译器在编译时进行确定。例如,定义一个类模板,示例代码如下所示:

template<typename T, typename U> 
class A 
{ 
};

将其中一个模板参数特化为int类型,另一个参数由用户指定,示例代码如下所示:

template<typename T> 
class A<T, int> 
{ 
}; 

关于模板,读者只有多加实践才能真正掌握其要领,体会到它带来的极大方便。C++中的标准模板库就是基于模板完成的,理解、掌握好模板也能为学习标准模板库打下坚实的基础。

6.6 本章小结

本章首先讲解了模板的概念,然后对函数模板与类型模板分别进行了讲解。函数模板中讲解了函数模板的定义、实例化和函数模板的重载;类模板中讲解了类模板的定义、实例化、类模板与友元函数、类模板与派生。最后讲解了模板的特化,包括全特化与偏特化。模板是C++语言中非常重要的内容,它与C++的标准模板库联系紧密,学好模板,对以后学习STL非常重要。

6.7 本章习题

一、填空题

1. 函数模板的实例化分为___、___两种形式。

2. 模板的特化包括___、___两种形式。

3. 约束模板友元函数实例化类型取决于___实例化时的类型。

4. 关键字___用于声明模板。

5. 把普通函数声明为类模板的友元函数,这样的友元函数称为___。

二、判断题

1. 函数模板可以像函数一样进行重载。( )

2. 类模板派生时需要指定模板参数类型,根据模板参数类型创建具体的类作为基类。( )

3. 类模板的派生类对象初始化与普通类一致。( )

4. 函数模板中声明的对象或变量不能与模板参数同名。( )

5. 模板参数名在同一模板参数列表中只能使用一次。( )

三、选择题

1. 关于函数模板,下列描述错误的是( )(多选)。

A.函数模板必须由程序员实例化为可执行的模板函数

B.函数模板的实例化由编译器实现

C.一个类中,只要有一个函数模板,这个类就是类模板

D.类模板的成员函数都是函数模板,类模板实例化后,成员函数也随之实例化

2. 下列模板声明中,正确的是( )。

A.tem plate<typenam e T1,T2>

B.tem plate<class T1,T2>

C.tem plate<T1,T2>

D.tem plate<typenam e T1,typenam e T2>

3. 若定义如下函数模板:

Template<typename T> 
Max(T a,T b,T &c) 
{ 
  c=a+b; 
} 

则下列M ax()函数模板能够调用成功的选项是( )。

A.int x,y;char z;M ax(x,y,z);

B.double x,y,z;M ax(x,y,z);

C.int x,y;float z;M ax(x,y,z);

D.float x;double y,z;M ax(x,y,z);

4. 关于类模板的模板参数,下列说法错误的是( )。

A.可以作为数据成员类型

B.可以作为成员函数的返回类型

C.可以作为成员函数的参数类型

D.以上说法都正确

5. 类模板的使用实际上是将类模板实例化成为一个( )。

A.函数

B.对象

C.类

D.抽象类

四、简答题

1. 简述什么是模板。

2. 简述什么是模板特化。

3. 简述类模板的友元函数有哪几种。

五、编程题

1. 已知一个有若干元素的数组arr,使用函数模板求该数组的最大值。

2. 编写一个类模板对数组元素进行排序、求和。