C++最重要的特性之一就是代码重用,为了实现代码重用,代码必须具有通用性。通用代码应不受数据类型的影响,并且可以自动适应数据类型的变化。这种程序设计类型称为参数化程序设计。模板是C++支持参数化程序设计的工具,通过它可以实现参数化多态性。所谓参数化多态性,就是将程序所处理的对象的类型参数化,使得一段程序可以用于处理多种不同类型的对象。
通过函数重载,可以看出重载函数通常是对于不同的数据类型完成类似的操作。很多情况下,一个算法是可以处理多种数据类型的。但是用函数实现算法时,即使设计为重载函数也只是使用相同的函数名,函数体仍然要分别定义。
下面是两个求绝对值的函数:
int abs(int x)
{
return x < 0 ? -x : x;
}
double abs(double x)
{
return x < 0 ? -x : x;
}
这两个函数只有参数类型和返回类型不同,功能完全一样。类似这样的情况,我们需要写一段通用的代码是用于多种不同的数据类型,这样会使代码的可重用性大大提高,从而提高软件的开发效率。使用函数模板就是为了达到这一目的。程序员只对函数模板编写一次,然后基于调用函数时提供的参数类型,C++编译器将自动产生相应的函数来正确地处理该类型的数据。
template <模板参数表>
类型名 函数名(参数表)
{
函数体定义
}
所有函数模板的定义都是用关键字template开始的,该关键字之后是用尖括号<>括起来的“模板参数表”。模板参数表由用逗号隔开的模板参数构成,可以包括以下内容:
①class(或typedef)标识符,指明可以接收一个类型参数。这些类型参数代表的是类型,可以是内部类型或者自定义类型。
②“类型说明符”标识符,指明可以接收一个由“类型说明符”所规定类型的常量作为参数。
③template<参数表>class标识符,指明可以接收一个类模板名作为参数。
类型参数可以用来指定函数模板本身的形参类型、返回值类型,以及声明函数中的局部变量。函数模板中函数体的定义方式与定义普通函数类似。
【例1】求绝对值的函数模板
template<class T>
T abs(T x)
{
return x < 0 ? -x : x;
}
int main()
{
int n = -5;
cout << abs(n) << endl;
double m = -6.8;
cout << abs(m) << endl;
return 0;
}
①在上述主函数中调用abs()时,编译器从实参的类型推导出函数模板的类型参数。
②当类型参数的含义确定后,编译器将以函数模板为样板,生成一个函数,这一过程称为函数模板的实例化。
例如,对于调用表达式abs(n),由于实参n是int类型,所以推导出函数模板中类型参数T为int,接着,编译器以函数模板为样板,生成如下函数,该函数为函数模板abs的一个实例:
int abs(int x)
{
return x < 0 ? -x : x;
}
同样,对于调用表达式abs(m),由于实参m是double型,所以推导出函数模板中类型参数T为double,接着,编译器以函数模板为样板,生成如下函数:
double abs(double x)
{
return x < 0 ? -x : x;
}
③因此,当主函数第一次调用abs时,执行的实际上是由函数模板生成的函数int abs(int x);
,主函数第二次调用abs时,执行的实际上是由函数模板生成的函数double abs(double x);
。
【例2】函数模板示例
template<class T>
void outputA(const T* arr, int n)
{
for (int i = 0;i < n; i++)
{
cout << arr[i] << " ";
}
cout << endl;
}
int main()
{
const int A_n = 5;
const int B_n = 6;
const int C_n = 7;
int arr[A_n] = { 1,2,3,4,5 };
cout << "输出数组arr的内容:" << " ";
outputA(arr, A_n);
double brr[B_n] = { 1.1,2.2,3.3,4.4,5.5,6.6 };
cout << "输出数组brr的内容:" << " ";
outputA(brr, B_n);
char crr[C_n] = "Hi yyn";
cout << "输出数组crr的内容:" << " ";
outputA(crr, C_n);
return 0;
}
函数模板中声明了类型参数T,表示一种抽象的类型。当编译器检测到程序中调用函数模板outputA时,便用outputA的第一个实参的类型替换掉整个模板定义中的T,并建立用来输出指定类型数组的一个完整的函数,然后再编译这个新建的函数。
主函数中声明了3中不同类型的数组,int型数组arr,double型数组brr和char型数组crr,长度分别为5,6,7。然后调用函数模板生成相应的函数,最后在屏幕上输出每个数组。编译过程中针对3种数据类型生成的函数如下:
outputA(a,A_n);//适用于int类型的outputA模板函数
outputA(b,B_n);//适用于double类型的outputA模板函数
outputA(c,C_n);//适用于char类型的outputA模板函数
由上例可以看出,模板函数与重载密切相关。从函数模板产生的相关函数都是同名的,编译器用重载的方法调用相应的函数。另外函数模板本身也可以用多种方法重载。
①函数模板本身在编译时不会生成任何目标代码,只有由模板生成的实例会生成目标代码。
②被多个源文件引用的函数模板,应当连同函数体一同放在头文件中,而不能像普通函数那样只将声明放在头文件中。
③函数指针也只能指向函数模板的实例,而不能指向函数模板本身。
使用类模板使用户可以为类定义一种模式,使得类中的某些数据成员、某些成员函数的参数、返回值或局部变量能取任意类型(包括系统预定义的和用户自定义的)。
类是对一组对象的公共性质的抽象,而类模板则是对不同类的公共性质的抽象,因此,类模板是属于更高层次的抽象。由于类模板需要一种或多种类型参数,所以类模板也常常称为参数化类。
template<模板参数表>
class 类名
{
类成员声明;
};
其中类成员的声明方法和普通类的定义几乎相同,只是它的各个成员(数据成员和函数成员)中通常要用到模板的类型参数T。其中“模板参数表”的形式与函数模板中的“模板参数表”相同。
如果需要在类模板以外定义其成员函数,则要采用以下的形式:
template<模板参数表>
类型名 类名<模板参数标识符列表>::函数名(参数表)
一个类模板声明,其自身并不是一个类,它说明了类的一个家族,只有被其他代码引用时,类模板才根据引用的需要生成具体的类。类模板的实例化过程在程序中时隐藏的。
使用一个类模板建立对象时,应以如下形式声明:
模板名<模板参数表>对象名1,...,对象名n;
【例】类模板应用举例
在本例中,声明一个实现任意类型数据存取的类模板S,然后通过具体数据类型参数对类模板进行实例化,生成类,然后类在被实例化生成对象s1,s2,s3和d。
struct student//结构体student
{
int id;//学号
float avg;//平均分
};
template<class T>//类模板:实现对任意类型数据进行存取
class S
{
private:
T item;//用于存放任意类型的数据
bool Isvalue;//标记item是否被存入
public:
S();//默认构造函数
T& getE();//提取数据函数
void putE(const T& x);//存入数据函数
};
template<class T>//默认构造函数的实现
S<T>::S():Isvalue(false){}
template<class T>//提取数据函数的实现
T&S<T>::getE()
{
if (!Isvalue)//如果提取的是没有初始化的数据,则程序终止
{
cout << "数据不存在" << endl;
exit(1);//使程序完全退出,返回到操作系统
//参数可用来表示程序终止的原因,可以被操作系统接收
}
else
return item;//返回item中存放的数据
}
template<class T>//存入函数的实现
void S<T>::putE(const T& x)
{
Isvalue = true;//将Isvalue设置为true,表示item中已存入数值
item = x;//将x的值存入item
}
int main()
{
S<int>s1, s2;//定义两个S类对象s1和s2,其中数据成员item为int型
s1.putE(3);//向对象s1中存入数据(初始化对象s1为3)
s2.putE(-7);//向对象s2中存入数据(初始化对象s1为-7)
cout << s1.getE() << " " << s2.getE() << endl;//输出对象s1和s2的数据成员
student g = { 1000,23 };//定义student类型结构体变量的同时赋予初值
S<student>s3;//定义S类对象s3,其中数据成员item为student类型
s3.putE(g);//向对象s3中存入数据(初始化对象s3)
cout << "这个学生的id是" << s3.getE().id << endl;//输出对象s3的数据成员
S<double>d;//定义S类对象d,其中数据成员item为double类型
cout << "检索对象d";
cout << d.getE() << endl;//输出对象d的数据成员
//由于对象d未经初始化,在执行函数d.getE()过程中导致程序终止
return 0;
}