模板概念
我们在设计程序时,常常会遇到大量的相似函数。如比较多个数求其最大值,如果对于两个数的比较,我们可以将函数名称命名为Max, 各种数据类型的参数可以构成重载函数,保证函数名称不变在编程中具有很多优势,能让程序调用者确切知道函数的功能,调用简单。如果没有函数重载的概念,则每一个函数都要规定一个不能重复的函数,程序调用者在使用函时会感到困惑。但重载函数在使用中是有条件的,重载函数间必须在函数参数个数或参数类型上有所不同。而且同名的重载函数要分别定义,当遇到有大量的重载函数时不复不定义大量的重载函数,比如本例中仅两个参数计算最大值就可能有很重载函数,如果三个参数,多个参数,重载函数的数目可想而知。
有没有更进一步的简化办法呢?C++提出了模板的概念,所谓模板就是虚拟的类型,或者称参数化类型。模板与类型的关系就跟类型和变量的关系一样。
将模板应用在函数的参数和返回类型中称为模板函数,将模板应用到类的定义中称为模板类。
函数模板
函数模板是用模板做函数参数和返回,设计出的通用的函数。
其定义形式为:
template<class T1,class T2,,,>
函数返回类型 函数名(函数参数表)
{
//函数模板定义
}
其中template表示定义的是模板,<>里是模板类型参数列表,其中每一个模板参数用关键字class来定义,模板参数的名称可以由程序定义,模板参数的名称就标识一个模板参数,该参数并不表示某一具体类型,而是表示了一个虚拟的类型,在函数调时可以用一个给定的类型来替换之,模板参数列表可以定义一个或多个模板参数。函数模板的返回值类型可以是普通类型,也可以是模板参数表中指定的类型。函数参数表给出的类型可以是普通类型,也可以是模板参数表中指定的类型。在模板参数表中指明的类型参数不必都用于函数参数表中。例如:
template<class T>
Tmax(T a,T b)
{
return a>b? a:b;
}
在上例中,如果用一个普通类类型来实例化该函数模板,那么T应该重载”>”运算符。
例:
#include"stdafx.h"
#include<iostream>
usingnamespace std;
template<class T> T Max(T a, T b)
{
return a>b? a:b;
}
intmain(int argc, char* argv[])
{
int i1=1,i2=9;
char c1='a', c2='b';
//调用Max(int , int)
int iRet=Max(i1,i2);
//调用Max(char , char)
char cRet=Max(c1,c2);
cout<<iRet<<endl;
cout<<cRet<<endl;
return 0;
}
函数的模板参数
函数模板参数列表中也可以定义也可以出现具体类型参数,例子:
template<class T,inti>
Tmax(T a, T b)
{
T c = a>b? a:b;
if (c>i)
return c;
else
return i;
}
上例中参数i是具体类型的参数。
模板函数的调用
一般来讲,模板函数可以直接调用,比如例子中代码int iRet=Max(i1,i2);编译器会自动根据函数实参的类型去替换模板类型,并生成整型版本的函数加以调用。
也可以明确指定其实例化所用的类型参数。比如在上面的例子中加上:
intiRet=Max<int>(c1,c2);
当函数模板参数中有具体类型的参数,函数的调用必须显式指定模板参数实例化所用的类型,如上例中函数的调用必须使用下面的语句调用:
inta = max<int, 5>(12,18);
具体类型的参数在实例化时要用该类型的值来替换。
函数模板之间也可以重载
和普通函数一样,函数模板之间也可以重载。例:
template<classT>
TFunc(T t)
{
return t;
}
template<classT>
intFunc(int i,T t)
{
return i*t;
}
而且函数模板也可以与普通函数之间构成重载关系。如上例中可以再写一个普通函数:
intFunc(int i)
{
return i;
}
当函数调用时,具体函数可以调用,普通函数也可以调用,实际会调用哪一个呢?实际会调用具体函数。
模板函重载规则
函数模板表示了一组相同的函数,这些函数之间(包括重载的函数模板),以及这些函数与其他同名的普通函数之间是重载的关系。这些重载函数之间的匹配规则如下:
1) 如果发现某一函数的参数正好与调用函数所使用的参数匹配,则调用该函数;否则按照2执行;
2) 如果从相应的模板所生成的某个函数,其参数正好与调用函数的参数匹配,调用时使用从模板实例化生成的函数,否则执行3;
3) 对相应参数进行隐式转换后,调用普通的函数;
4) 调用模板实例化生成的函数时,从不进行隐式的类型转换;
5) 若明确指定了函数模板的类型参数,则调用相应的模板实例化后生成的函数,如果需要,编译器会进行隐式类型转换
例:
#include<iostream>
usingnamespace std;
intMax(int i1,int i2)
{
cout<<"NormalMax"<<endl;
return i1>i2? i1:i2;
}
template<classT> Max(T t1, T t2)
{
cout<<"TemplateMax,sizeof(t1):"<<sizeof(t1)<<endl;
return t1>t2? t1:t2;
}
intmain(int argc, char* argv[])
{
int i1=1,i2=9;
char c1='a',c2='b';
//调用普通函数int Max(int i1,int i2)
int iRet=Max(i1,i2);
//调用模板实例化生成的char Max(char a,char b)
char cRet=Max(c1,c2);
//调用模板实例化生成的char Max(char a,char b),
//最后将返回值隐式转换成int型
int iRet2=Max(c1,c2);
//调用模板实例化生成的char Max(char a,char b),
char cRet2=Max<char>(i1,c1);
return 0;
}
类模板
在定义一个类时,也可以包含模板参数。类模板的主要用途是实现包容类,如集合、链表等。虽然模板的功能很大程度上可以通过宏实现,但是模板带有类型信息,编译器可以进行类型检查,而且具有更好的通用型。
类模板的定义形式为:
template<class T1,class T2...class Tn>
class类模板名
{
//类模板定义
}
其中template是C++关键字,表示是对模板进行定义。tempalte后的<>里是模板的参数,参数可以有一个或多个,每个参数用class关键字修饰,并用逗号格开。class关键字也可以用基本数据类型代替。在类模板实例化时,T可以是任意类型。接下来的关键字class 说明模板是类模板,后面紧接的是类模板名。
下面例子定义了一个堆栈模板类,并使用该模板类实现了字符堆栈和整型堆栈:
#include"stdafx.h"
#include<iostream>
usingnamespace std;
template<class T> class TStack
{
private:
T* pFirst;
T* pLast;
int iSize;
public:
TStack(int i)
{
iSize=i;
pFirst=pLast=new T[iSize];
}
~TStack()
{
delete []pFirst;
}
void Push(T t)
{
*pLast++=t;
}
T Pop()
{
--pLast;
return *pLast;
}
int GetSize() const
{
return pLast-pFirst;
}
};
typedefTStack<char> CStackChar;
typedefTStack<int> CStackInt;
intmain(int argc, char* argv[])
{
CStackChar charStack(3);
CStackInt intStack(5);
charStack.Push('a');
charStack.Push('b');
charStack.Push('c');
cout<<"CharStack:"<<charStack.Pop()
<<","<<charStack.Pop()
<<","<<charStack.Pop()<<endl;
intStack.Push(1);
intStack.Push(2);
cout<<"IntStack:"<<intStack.Pop()
<<","<<intStack.Pop();
return 0;
}
由于类模板中T是一个类型参数,不是实际的类型,因此,不能由它直接生成实例对象。为类模板中的类型参数指定具体类型的过程叫做类模板的实例化,类模板实例化的结果是类,而不是对象。实例化类模板的一般形式为:
类模板名<具体类型表>
比如上面的:
typedefTStack<char> CStackChar;
typedefTStack<int> CStackInt;
用类模板实例化了两个类CStackChar,CStackInt.
类模板的成员函数也可以在类模板的外面定义。如
template<classT>
voidTStack<T>::Push(T t)
{
*pLast++=a;
}
类模板成员函数也可以重载,如:
template<classT> T TStack<T>::Pop(int i)
{
return *(pLast-i);
}
类模板的继承性
类模板可以从普通类中派生。
如:
classCBase
{
//...
};
template<classT> class TDerived:public CBase
{
public:
T t;
//...
};
类模板的基类也可以是类模板。
比如:
template<classT> class CBase
{
public:
T t;
};
template<classT1,class T2>
classTDerived:public CBase<T2>
{
public:
T1 t;
};
注意,派生的类模板的基类是类模板时,派生类模板的参数表中应包含基类模板的参数。
如果派生类没有类型参数,或者它的类型参数与基类的参数相同时,派生类模板的参数只需包含基类模板的参数。如:
template<classT> TDerived:public CBase<T>
{
public:
T d;
//...
};
无法从类模板中派生出普通类。
因为从类模板中派生的类总是含有类型参数,不能做为普通类使用。
类模板之间也可以多重继承:
例子:
template<classT> class TBase
{
public:
T tb;
};
classCBase
{
//...
};
template<classT> class TDerived:public TBase<T>,public CBase
{
public:
T td;
//...
};
类模板实例化后即是普通的类,所以类模板实例化后也可以当函数的参数类型使用:
voidFunc(TStack<int> &intStack)
{
intStack.Push(1);
//...
}
类模板实例化后生成的类可以做其他类模板的参数
前面说过,可以用任何具体类型作为参数来实例化类模板,类模板实例化以后就可以当普通类来使用,所以也可以用类模板实例化后生成的类来做为其他类模板的参数:
template<classT>
classTVector
{
//...
}
TStack<TVector<int>> vStack(5);
注意两个”> >”中间应该空一格,否则当右移处理了0。
标准的数据类型也可以做模板参数
模板参数除了类型参数外,也可以是标准的数据类型,但是在类模板实例化类时,标准数据类型的模板参数必须用实际的值代替。
template<classT,int i> class TBase
{
public:
T tb;
int m_i;
TBase();
};
template<classT,int i>
TBase<T,i>::TBase():m_i(i)
{
}
intmain(int argc, char* argv[])
{
TBase<int,9> obj;
return 0;
}
类模板的模板参数也可以没有类型参数
如:
template<inti> class A
{
int ar[i];
};