模板(template)是C++实现代码重要机制的重要工具,是泛型技术(即与数据类型无关的通用程序设计技术)的基础。模板表示的是概念级的通用程序设计方法,它把算法和数据类型区分开来,能够设计出独立于具体数据类型的模板程序,模板程序能以数据类型为参数生成针对于该类型的实际程序代码。模板分为函数模板和类模板两类,ANSI标准C++库就是使用模板技术实现的。
某些程序除了所处理的数据类型之外,程序代码和程序功能相同,但为了实现他们,却不得不编写多个与具体数据类型紧密结合的程序,例如
int Min(int a, int b){
return a<b?a:b;
}
float Min(float a, float b){
return a<b?a:b;
}
double Min(double a, double b){
return a<b?a:b;
}
char Min(char a, char b){
return a<b?a:b;
}
如何简化以上编程呢?C语言中,可以通过宏的方式实现以上想法:
#define Min(a,b) ((a) < (b))?(a):(b)
C++中,也可以利用宏来进行类似程序设计,但宏避开了C++类型检查机制,在某些情况下可能引发错误,是不安全的。更好的方法就是模板来实现这样的程序设计。
C++中的模板与制作冰糕的模具很相似,是生产函数或类的模具。模板接收数据类型参数,并根据此类型创建相应的函数或类。
对于上面的所有Min()而已,只需要下面的函数模板就能够生成所有的Min()函数。
template <typename T>
T Min(T a, T b){
return a<b?a:b;
}
template和typenae是用来定义模板的关键字。min模板不涉及任何具体的数据类型,而是用T代表任意数据类型,称为类型参数。Min模板代表了求两个数据最小的通用算法,它与具体数据类型无关,但能够生成计算各种具体数据类型的最小值的函数。编译器的做法是用具体的类型替换模板中的类型参数T,生成具体类型的函数Min()。比如用int替换掉模板中的所有T就能生成求两个int类型数据的函数min()。
#include <iostream>
using namespace std;
/*
int Min(int a, int b){
return a<b?a:b;
}
float Min(float a, float b){
return a<b?a:b;
}
double Min(double a, double b){
return a<b?a:b;
}
char Min(char a, char b){
return a<b?a:b;
}
*/
template <typename T>
T Min(T a, T b){
return a<b?a:b;
}
int main(void){
int i = 4, j = 8;
char i2 = '4', j2 = '3';
cout << Min(i, j) << endl;
cout << Min(i2, j2) << endl;
return 0;
}
从函数模板Min可以看成,C++模板提供了对逻辑结构相同的数据对象通用行为的定义方法,它把通用算法的实现和具体的数据类型区分开来,模板操作的是参数化的数据类型(类型参数)而非实际数据类型。一个带有类型参数的函数称为函数模板,带有类型参数的类型称为模板类。
在调用模板时,必须为它的类型参数提供实际数据类型,C++将用该数据类型替换模板中的全部类型参数,由编译器生成与具体的数据类型相关的运行的程序代码,这个过程称为模板的实例化。由函数模板实例化生成的函数称为模板函数,由类模板实例化生成的类称为模板类。
函数模板提供了一种通用的函数行为,该函数行为可以用多种不同的数据类型进行调,编译器会根据调用类型自动将它实例化为具体数据类型的函数代码,也就是说函数模板代表了一个函数家族。与普通函数相比,函数模板中某些函数元素的数据类型是未确定的,这些元素的类型将在使用时被参数化;与重载函数相比,函数模板不需要程序员重复编写函数代码,它可以自动生成许多功能相同单参数和返回值类型不同的函数。
template <typename T1, typename T2, ...>
返回类型 函数名(参数表){
... ...
}
template是模板定义的关键字写在<>中的T1 T2…是模板参数中的typename表示其后的参数可以是任意类型的。
#include <iostream>
using namespace std;
/*
int Min(int a, int b){
return a<b?a:b;
}
float Min(float a, float b){
return a<b?a:b;
}
double Min(double a, double b){
return a<b?a:b;
}
char Min(char a, char b){
return a<b?a:b;
}
*/
template <typename T>
T Min(T a, T b){
return a<b?a:b;
}
int main(void){
int i = 4, j = 8;
char i2 = '4', j2 = '3';
cout << Min(i, j) << endl;
cout << Min(i2, j2) << endl;
return 0;
}
当编译器遇到关键字template和跟随其后的参数定义时,它只是简单地知道这个函数模板在后面的程序代码中可能会用到。除此之外,编译器并不会做额外的工作。在这个阶段函数模板本身并不能使编译器产生任何代码,因为编译器此时并不知道函数模板要处理的具体数据类型,根本无法生成任何函数代码。
当编译器此时并不知道函数模板要处理的具体数据类型,根本无法生成任何函数代码。
当编译器遇到程序中对函数模板的调用时,它才会根据调用语句中实参的具体类型,确定模板参数的数据类型,并用此类型替换函数模板的模板参数,生成能够处理该类型的函数代码,即模板函数。
对于7.2.1代码可以执行一下命令观察实验效果。
nm a.out | grep a.out
模板参数的匹配问题
C++在实例化函数模板的过程中,只是简单地将模板参数替换为实参的类型,并以此生成模板函数,不会进行参数类型的任何转换。这种方式与普通函数的参数处理有着极大的区别,以前在普通函数的调用过程中,会进行参数的自动类型转换。
#include <iostream>
using namespace std;
double Max(double a, double b) {
return a>b?a:b;
}
int main(void) {
double a = 2, b = 3.14;
float c = 4.6, d = 5.7;
cout << "2 3.14 : " << Max(a,b) << endl;
cout << "2 4.6 : " << Max(a,c) << endl;
cout << "2 100 : " << Max(a,100) << endl;
return 0;
}
以上程序能够正确执行,现在使用函数模板来实现通用的功能,如下所示
#include <iostream>
using namespace std;
template <typename T>
T Max(T a, T b){
return a>b?a:b;
}
int main(void) {
double a = 2, b = 3.14;
float c = 4.6, d = 5.7;
cout << "2 3.14 : " << Max(a,b) << endl;
cout << "2 4.6 : " << Max(a,c) << endl;
cout << "2 100 : " << Max(a,100) << endl;
return 0;
}
编译以上程序,产生模板参数不匹配的错误。产生这个错误的原因是模板实例化过程中不会进行任何的参数类型转换。编译器在翻译Max(a,c)时,由于实参类型为double和float,而Max函数模板只有一个形参类型T,总不能让T同时取double和float两种类型吧?要知道模板实例化过程中,C++不会进行任何形式的隐式类型转换,于是产生了上述编译错误。
这种问题的解决方式有:
在模板调用时进行参数类型的强制转换
cout << "2 4.6 : " << Max(a,double(c)) << endl;
cout << "2 100 : " << Max(a,double(100) << endl;
显示指定函数模板实例化的类型参数
cout << "2 4.6 : " << Max<double>(a,c) << endl;
cout << "2 100 : " << Max<double>(a,100) << endl;
指定多个模板参数
在模板函数的调用过程中,为了避免出现一个模板参数与多个调用实参的类型冲突问题,可以为函数模板指定多个不同的类型参数。
#include <iostream>
using namespace std;
template <typename T1, typename T2>
T1 Max(T1 a, T2 b){
return a>b?a:b;
}
int main(void) {
double a = 2, b = 3.14;
float c = 4.6, d = 5.7;
cout << "2 3.14 : " << Max(a,b) << endl;
cout << "2 4.6 : " << Max(a,c) << endl;
cout << "2 100 : " << Max(a,100) << endl;
return 0;
}
模板函数的形参表
不要误以为函数模板中的参数只能是类型形参,它也可以包括普通类型的参数。
#include <iostream>
using namespace std;
template <typename T>
void display(T &arr, unsigned int n){
for(int i = 0; i < n; i++){
cout << arr[i] << "\t" ;
}
cout << endl;
}
int main(void) {
int a[] = {1,2,3,4,5,6,7,8};
char b[] = {'a','b','c','d','e','f'};
display(a, sizeof(a)/sizeof(a[0]));
display(b, sizeof(b)/sizeof(b[0]));
return 0;
}
//输出结果
myubuntu@ubuntu:~/lv19/cplusplus/dy08$ ./a.out
1 2 3 4 5 6 7 8
a b c d e f
在某些情况下,函数模板并不能生成处理特定数据类型的模板函数,上面例子中的Max函数模板可以计算int 或者 char 类型数据的最大值,但对于字符串类型却是无能为力的。解决这类问题的方法就是对函数模板进行特化。所谓特化,就是针对模板不能处理的特殊数据类型,编写与模板同名的特殊函数专门处理这些数据类型。语法格式如下所示。
template<>
返回类型 函数名<特化的数据类型>(参数表){
...
}
例如:
#include <iostream>
#include <cstring>
using namespace std;
template <typename T> //函数模板
T Min(T a, T b){
return a>b?a:b;
}
template <> //特化版 函数模板
const char* Min(const char* a, const char* b){
cout << "Min(const char*, const char*)" << endl;
return (strcmp(a,b) > 0)?b:a;
}
int main(void) {
cout << Min('a', 'b') << endl;
cout << Min("aaa", "aaa") << endl;
return 0;
}
//输出结果
myubuntu@ubuntu:~/lv19/cplusplus/dy08$ ./a.out
b
Min(const char*, const char*)
aaa
函数模板用于设计程序代码相同而所处理的数据类型不同的通用函数。与此类似,C++也支持用类模板来设计结构和成员函数完全相同,但所处理的数据类型不同的通用类。比如,对于堆栈类而言,可能存在整数栈、双精度栈、字符栈等多种不同数据类型的栈,每个栈除了所处理的数据类型不同之外,类的结构和成员函数完全相同,可为了在非模板的类设计中实现这些栈,不得不重复编写各个栈类的相同代码,例如初始化栈、入栈、出栈等操作。为了解决该问题,C++中用类模板设计这样的类簇最方便,一个类模板就能够实例化生成所需要的栈类。
类模板也称为类属类,它可以接收类型为参数,设计出与具体类型无关的通用类。在设计类模板时,可以使其中的某些数据成员,成员函数的参数或返回值与具体类型无关。
类模板与函数模板的定义形式类似,如下所示:
template <typename T1, typename T2, ...>
class 类名{
...
}
实例:设计一个栈的类模板Stack,在模板中使用类型参数T表示栈中存放的数据,用非类型参数MAXSIZE代表栈的大小。
vi Stack.cpp
#include <iostream>
using namespace std;
template <typename T, int MAXSIZE>
class Stack{
private:
T elements[MAXSIZE]; //类型不确定 所以写 T
int top; //栈顶
public:
Stack():top(0){
}
void push(T e);
T pop();
bool empty(){
return top == 0;
}
bool full(){
return top==MAXSIZE;
}
};
template <typename T, int MAXSIZE>
void Stack<T, MAXSIZE>::push(T e){
if(full()) {
cout << "Stack is full, can not push" << endl;
}
elements[top++] = e;
}
template <typename T, int MAXSIZE>
T Stack<T, MAXSIZE>::pop() {
if(top == 0) {
cout << "stack is empty, can not pop" << endl;
}
top--;
return elements[top];
}
int main(void){
return 0;
}
类模板的实例化包括模板实例化和成员函数实例化。当用类模板定义对象时,将引起类模板的实例化。在实例化模板时,如果模板参数就是类型参数,则必须为它指定具体的类型;如果模板参数是非类型参数,则必须为它指定一个常量值。如对前面的Stack类模板而言,下面是他的一条实例化语句:
Stack<int, 10> istack;
编译器实例化Stack的方法是:将Stack模板声明中的所有的类型参数T替换为int,将所有的非类型参数MAXSIZE替换为10,这样就用Stack模板生成了一个int类型的模板类。为了区别于普通类,暂且将该类记作Stack<int, 10>,即在类模板后面的一对<>中写上模板参数。该类的代码如下:
class Stack{
private:
int elements[10]; // MAXSIZE 替换为 10 , T类型 替换为 int
int top; //栈顶
public:
Stack():top(0){
}
void push(int e);
int pop();
bool empty(){
return top == 0;
}
bool full(){
return top==MAXSIZE;
}
};
最后C++用这个模板类定义一个对象istack.
注意:在上面的实例化过程中,并不会实例化模板的成员函数,也就是说,在用类模板定义对象时并不会生成类成员函数的代码。类模板成员函数的实例化发生在该成员函数被调用时,这就以为着只有那些被调用的成员才会被实例化。或者说,只有当成员函数被调用了,编译器才会为它生成真正的代码。
#include <iostream>
using namespace std;
template <typename T, int MAXSIZE>
class Stack{
private:
T elements[MAXSIZE]; //类型不确定 所以写 T
int top; //栈顶
public:
Stack():top(0){
}
void push(T e);
T pop();
bool empty(){
return top == 0;
}
bool full(){
return top==MAXSIZE;
}
};
template <typename T, int MAXSIZE>
void Stack<T, MAXSIZE>::push(T e){
if(full()) {
cout << "Stack is full, can not push" << endl;
}
elements[top++] = e;
}
template <typename T, int MAXSIZE>
T Stack<T, MAXSIZE>::pop() {
if(top == 0) {
cout << "stack is empty, can not pop" << endl;
}
top--;
return elements[top];
}
int main(void) {
Stack<int, 10> istack;
istack.push(11); //此处调用该成员函数 编译器才会生成成员函数模板
//cout << istack.pop() << endl;
return 0;
}
可以将pop函数的类外定义删除掉,然后再编译运行程序,可以发现的是程序依然能够正确的执行。
与普通类的对象一样,类模板的对象或引用也可以作为函数的参数,只不过这类函数通常是模板函数,且其调用实参常常是该类模板的模板类对象。
#include <iostream>
using namespace std;
template <typename T, int MAXSIZE>
class Stack{
private:
T elements[MAXSIZE]; //类型不确定 所以写 T
int top; //栈顶
public:
Stack():top(0){
}
void push(T e);
T pop();
bool empty(){
return top == 0;
}
bool full(){
return top==MAXSIZE;
}
};
template <typename T, int MAXSIZE>
void Stack<T, MAXSIZE>::push(T e){
if(full()) {
cout << "Stack is full, can not push" << endl;
}
elements[top++] = e;
}
template <typename T, int MAXSIZE>
T Stack<T, MAXSIZE>::pop() {
if(top == 0) {
cout << "stack is empty, can not pop" << endl;
}
top--;
return elements[top];
}
template <typename T>
void display(Stack<T, 10> &s){
while(!s.empty()){
cout << s.pop() << endl;
}
}
int main(void) {
Stack<int, 10> istack;
istack.push(11);
istack.push(12);
istack.push(123);
istack.push(886);
display(istack);
return 0;
}
//输出结果
myubuntu@ubuntu:~/lv19/cplusplus/dy08$ ./a.out
886
123
12
11
类模板代表了一种通用程序设计的方法,它表示了无限类集合,可以实例化生成基于任何类型的模板类。在通常情况下,由类模板生成的模板类都能够正常的工作,但也有类模板生成的模板类代码对某些数据类型不适用的情况。
如:设计一个通用数组类,它能够直接存取数组元素,并能够输出数组中的最小值。
#include <iostream>
using namespace std;
template <typename T>
class Arr{
private:
T *arr;
int size;
public:
Arr(int size=10){
this->size = size;
arr = new T;
}
~Arr(){
delete arr;
}
T &operator[](int i);
T Min();
};
template <typename T>
T& Arr<T>::operator[](int i){
if(i < 0 || i > size-1){
cout << "数组下标越界" << endl;
}
return arr[i];
}
template <typename T>
T Arr<T>::Min() {
T temp;
temp = arr[0];
for(int i=1; i < size; i++){
if(temp > arr[i])
temp = arr[i];
}
return temp;
}
int main(void) {
Arr<int> a(5);
for(int i=0; i < 5; i++){
a[i] = 68+i;
}
cout << a.Min() << endl;
Arr<char *> b(5);
b[0] = (char *)"ffs";
b[1] = (char *)"ees";
b[2] = (char *)"dds";
b[3] = (char *)"ccs";
b[4] = (char *)"bbs";
cout << b.Min() << endl;
return 0;
}
//输出结果
myubuntu@ubuntu:~/lv19/cplusplus/dy08$ ./a.out
68
ffs
*** Error in `./a.out': free(): invalid next size (fast): 0x0924d018 ***
Aborted (core dumped)
显然Arr类模板并不完全适用于生成char *类型的模板类,因为Arr类模板的Min成员函数并使用于字符指针类型的计算大小。
解决上述问题的方法就是类模板特化,即用与该模板相同的名字为某种数据类型专门重新一个模板类。特化类模板时,可以随意增减和改写模板原有的成员,成员函数的改写也不受任何限制,可以与原来的成员函数变得完全不同。
类模板有两种特化方式,一种是特化整个类模板,另一种是特化个别成员函数。前者是为某种类型单独建立一个类,后者则只针对特化的数据类型提供个别成员函数的实现代码,特化后的成员函数不再是一个模板函数,而是针对特点类型的普通函数。
与函数模板特化方式相同,类模板成员函数的特化也以templat<>开头。形式如下:
template<>
返回类型 类模板名<特化的数据类型>:: 特化成员函数名(参数表){
...
}
Arr类模板对char * 类型来说,除了Min成员函数不适用外,其余成员都可以,则针对char * 类型重新编写Min成员函数,即特例Arr类模板的Min成员函数就能够解决字符串求大小的问题。特化的Min函数如下:
template<>
char * Arr<char *>::Min(){
char *temp;
temp = arr[0];
for(int i=1; i < size; i++){
if(strcmp(temp, arr[i]) > 0)
temp = arr[i];
}
return temp;
}
为了某种数据类型特化整个类模板也要以template<> 开头,形式如下所示:
template<> class 类模板名<特化的数据类型>{
...
}