模板是C++支持参数化多态的工具,是泛型编程的基础。模板可以实现类型参数化,即把类型定义为参数,真正实现了代码的可重用性,减少了编程及维护的工作量,并且降低了编程难度。模板是类或函数在编译时定义所需处理和返回的数据类型。一个模板是类或函数的描述,即模板分为函数模板和类模板,本章将针对函数模板和类模板的相关知识进行详细讲解。
一 模板的概念
在C++程序中,声明变量、函数、对象等实体时,程序设计者需要指定数据类型,让编译器在程序运行之前进行类型检查并分配内存,以提高程序运行的安全性和效率。但是这种强类型的编程方式往往会导致程序设计者为逻辑结构相同而具体数据类型不同的对象编写模式一致的代码。例如,定义一个求和函数add(int,int),add()函数可以计算两个int类型数据的和,但是对于double类型的数据就无能为力了,此时,程序设计者还需要定义一个函数add(float,float),计算两个double类型的数据之和,但是这样不利于程序的扩充和维护。
为此,C++标准提供了模板机制,用于定义数据类型不同但逻辑结构相同的数据对象的通用行为。在模板中,运算对象的类型不是实际的数据类型,而是一种参数化的类型。带参数类型的函数称为函数模板,带参数类型的类称为类模板。例如,定义函数add(),计算两个数之和,可以将类型参数化,如add(T,T),其中,T就是参数化的类型,在调用add()函数时,可以传入任意类型的数据,函数可以根据传入的数据推导出T的值是哪种数据类型,从而进行相应的计算。这样程序设计者就可以专注于逻辑代码的编写,而不用关心实际具体的数据类型。模板就像生产模具,例如,中秋生产月饼,生产月饼的模具就是模板,在做模具时,只关心做出什么样式的月饼,而不用关心月饼具体的原料是什么(如面粉、糯米粉、玉米粉等)。
程序运行时,模板的参数由实际参数的数据类型决定,编译器会根据实际参数的数据类型生成相应的一段可运行代码,这个过程称为模板实例化。函数模板生成的实例称为模板函数,类模板生成的实例称为模板类。
二 函数模板
函数模板是函数的抽象,它与普通函数相似,唯一的区别就是函数参数的类型是不确定的,函数参数的类型只有在调用过程中才被确定。本节将针对函数模板的用法进行详细讲解。
函数模板的定义
如果定义一个实现两个数相加的函数add(),要实现int、float、double等多种类型的数据相加,则要定义很多个函数,这样的程序就会显得非常臃肿。但使用模板就无须关心数据类型,只定义一个函数模板就可以。定义函数模板的语法格式如下所示:
template<typename 类型占位符>
返回值类型 函数名(参数列表)
{
//函数体;
}
上述语法格式中,template是声明模板的关键字,<>中的参数称为模板参数;typename关键字用于标识模板参数,可以用class关键字代替,class和typename并没有区别。模板参数不能为空,一个函数模板中可以有多个模板参数,模板参数和普通函数参数相似。template下面是定义的函数模板,函数模板定义方式与普通函数定义方式相同,只是参数列表中的数据类型要使用<>中的参数名表示。下面通过案例演示函数模板的用法
例6-1:实例代码:
#include <iostream>
#include <iomanip>
#include <string.h>
#include <stdio.h>
using namespace std;
class mynum{
friend ostream& operator<<(ostream& o,const mynum &m);
friend istream& operator>>(istream& i,mynum &m);
public:
int num;
mynum(int n):num(n){
}
mynum& operator+(mynum &m){
this->num += m.num;
return *this;
}
void show(){
cout << "num = " << num << endl;
}
};
ostream& operator<<(ostream& o,const mynum &m){
o << m.num;
return o;
}
istream& operator>>(istream& i,mynum &m){
i >> m.num;
return i;
}
template <typename T>
T add(T t1,T t2)
{
return t1 + t2;
}
int main()
{
cout << add(1,2) << endl;
cout << add(1.2,3.4) << endl;
mynum m1(5),m2(10);
m1.show();
cout << m1 + m2 << endl;
m1.show();
cout << add<mynum>(m1 , m2) << endl;
cout << "--end--" << endl;
return 0;
}
执行结果:
3
4.6
num = 5
15
num = 15
25
--end--
示例中定义了函数模板add(),用于实现两个数据相加。调用分别传入两个int类型数据,两个double类型数据,两个自定义的类mynum,在munum中重载了加+运算符。使得两个mynum对象可以相加。结果无误。符合预期。
函数模板实例化
函数模板并不是一个函数,它相当于一个模子,定义一次即可使用不同类型的参数来调用该函数模板,这样做可以减少代码的书写,提高代码的复用性和效率。需要注意的是,函数模板不会减少可执行程序的大小,因为编译器会根据调用时的参数类型进行相应的实例化。所谓实例化,就是用类型参数替换模板中的模板参数,生成具体类型的函数。实例化可分为隐式实例化与显式实例化,下面分别介绍这两种实例化方式。
1.隐式实例化
隐式实例化是根据函数调用时传入的参数的数据类型确定模板参数T的类型,模板参数的类型是隐式确定的,如例6-1中函数模板add(1,2)的调用过程。
在例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 实例代码:
#include <iostream>
#include <iomanip>
#include <string.h>
#include <stdio.h>
using namespace std;
template <typename T>
T add(T t1,T t2)
{
return t1 + t2;
}
template int add<int>(int t1,int t2);
int main()
{
cout << add<int>(10,'B') << endl;
cout << add(1.2,3.4) << endl;
cout << "--end--" << endl;
return 0;
}
执行结果:
76
4.6
--end--
在例6-2中,template int add<int>(int t1,int t2);显式声明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-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:注意mynum的两个+运算符的重载。
#include <iostream>
#include <iomanip>
#include <string.h>
#include <stdio.h>
using namespace std;
class mynum{
//friend ostream& operator<<(ostream& o,const mynum &m);
//friend istream& operator>>(istream& i,mynum &m);
public:
int num;
mynum(int n):num(n){
cout << "mynum init num = " << num << endl;
}
~mynum(){
cout << "mynum delete num = " << num << endl;
}
mynum& operator+(mynum &m){
this->num += m.num;
return *this;
}
int operator+(const mynum &m1) const{
int ret;
ret = this->num + m1.num;
return ret;
}
void show(){
cout << "num = " << num << endl;
}
};
ostream& operator<<(ostream& o,const mynum &m){
o << m.num;
return o;
}
istream& operator>>(istream& i,mynum &m){
i >> m.num;
return i;
}
int add(const int &a,const int &b){
cout << "int add" << endl;
return a + b;
}
template <typename T>
T add(const T &t1,const T &t2)
{
cout << "T add" << endl;
return t1 + t2;
}
template <typename F>
F add(const F &t1,const F &t2,const F &t3)
{
cout << "F add" << endl;
return t1 + t2 + t3;
}
int main()
{
cout << add(10,20) << endl;
cout << add(30,40.5) << endl;
cout << add(1.2,3.4) << endl;
cout << add(string("hello "),string("world! ")) << endl;
cout << add(string("Cat "),string("like "),string("cake ")) << endl;
cout << add(mynum(1),mynum(2)) << endl;
cout << "--end--" << endl;
return 0;
}
执行结果:
int add
30
int add
70
T add
4.6
T add
hello world!
F add
Cat like cake
mynum init num = 2
mynum init num = 1
T add
mynum init num = 3
3
mynum delete num = 3
mynum delete num = 1
mynum delete num = 2
--end--
需要注意的是,模板不允许自动类型转化,如果有不同类型参数,只允许使用非模板函数,因为普通函数可以进行自动类型转换,所以cout << add(30,40.5) << endl;行代码调用add()函数时,调用的是非模板函数,将40.5转换成了int类型再与30进行相加。
使用函数模板要注意的问题
函数模板虽然可以极大地解决代码重用的问题,但读者在使用时仍需注意以下几个方面:
(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){}