浅谈C++模板机制
一、 什么是模板?
1. 模板(Template)可以看做成对于某一类问题一种通用的解决方案,而实现的具体细节则需要根据实际问题对模板做出调整和优化。
2. 如我们在使用Word进行文档处理时,模板决定了文档的基本结构和文档的设置,如果你想要某种风格的文档结构,你可以对模板进行修改。模板提供了更加通用、灵活的解决方案。
3. 在C++中,模板是泛型编程的基础,是创建类和函数的蓝图或公式。
二、 从函数模板谈起
1. 从一个实例出发
假设我们想设计一个函数根据输入参数的类型来返回这个参数的绝对值,如果按照C语言的做法,我们会设计如下几个函数:
int fabsInt(int arg);
double fabsDouble(double arg);
float fabsFloat(float arg);
这样设计出的三个函数虽然函数定义不同,但是完成的功能却是相同的,这样的设计比较麻烦。C++提供了函数模板使得这一问题的解决方案更加通用、灵活。
源代码:
template<typename T>
T fabs(T arg)
{
return arg>0?arg:(-arg);
}
测试:
inta;
doubleb;
floatc;
cout<<"a=";
cin>>a;
cout<<"b=";
cin>>b;
cout<<"c=";
cin>>c;
cout<<"|"<<a<<"|"<<"="<<fabs(a)<<endl;
//执行int fabs(int arg)
cout<<"|"<<b<<"|"<<"="<<fabs(b)<<endl;
//执行double fabs(double arg)
cout<<"|"<<c<<"|"<<"="<<fabs(c)<<endl;
//执行float fabs(float arg)
这样编译器在编译的时候就可以根据传送的参数类型实例化某个函数执行。
1. 定义函数模板的注意事项:
(1)函数模板是一个独立于类型的函数,可以看做一种通用的功能;
(2)模板定义必须以template开始,之后接模板的形参列表且此列表不能为空;
(3)可用typename或class定义模板的类型,两者没有区别。
2. 模板形参的注意事项:
(1)模板形参遵循名字屏蔽规则,如下例:
typedef int T; //T是全局作用域
template<class T> // 此时的T是局部作用域,它屏蔽了全局的T
void fun(constT& val)
{
T temp=val;
//此时temp的类型是具有局部作用域的T,并非int类型
//Todo something
}
(2) 模板的类型不能重复定义,如下例:
template<classT, class T> / /T重复定义
//编译器报错:error C2991: redefinition of template parameter 'T'
void fun(constT& val)
{
T temp=val;
//Todo something
}
3. 实例化
(1)模板实参的推断
在使用函数模板时,编译器通常会自动推断出模板实参,如以上求绝对值的例子。
(2)实例化时形参、实参类型必须匹配
template<typenameT>
T compare(constT& a,const T& b) //此模板函数返回a, b的最大值
{
return a>b?a:b;
}
void main()
{
int a=1;
double b=3;
cout<<compare(a,b); //类型不匹配
//编译器报错:
//error C2782: 'T__cdecl compare(const T &,const T &)' : template parameter 'T' isambiguous
}
原因分析:此模板只有一个类型T,在编译时,编译器最先遇到的实参是a,类型是整型,编译器就将该模板形参解释为整型,此后出现的形参b不能被解释成整型,所以编译器报错。
解决方案一:强制类型转换
cout<<compare(a,int(b));
//将变量b强制转换为整型,但转换后精度损失
或:
cout<<compare(double(a),b);
//此时整型变量a会提升为double型,精度不会损失
解决方案二:修改函数模板
但这样做,可能会违背函数模板设计的初衷,所以我们在使用函数模板时,最好做到形参、实参类型完全匹配。
(3)实例化与函数指针的确定
在C/C++语言中,函数名代表函数的入口地址,那么我们可以将这个地址放在一个指针变量中,这个指针变量就是指向函数的指针,如下代码:
int add(int a, int b); //返回a, b之和
int (*ptrfun)(int a, int b); //ptrfun是指向函数的指针
ptrfun=add; //赋值
ptrfun(1,3); //调用
或者以另一种形式赋值:
int (*ptrfun)(int a, int b)=add;
这里我们写了一个关于加法的函数模板:
template<typename T>
T add(const T &a, const T &b)
{
return a+b;
}
int (*pfun)(const int &a ,const int &b)=add;
//此时编译器在会以整型来实例化函数模板
如果编译器不能从函数指针类型(如上例中的函数指针所指函数有整型参数)推断出函数模板的类型,那么编译器就会报错。如下例:
template<typename T>
T add(const T &a, const T &b)
{
return a+b;
}
//定义重载函数func,注意此这两个func函数的参数分别是指向不同函数的指针
void func(int (*)(const int&, const int&));
//指向两个整型数相加函数
void func(string (*)(const string&, conststring&));
//指向两个字符串连接函数
func(add); //编译器报错
//error C2668: 'func' : ambiguous call to overloadedfunction
原因分析:当执行func(add)时,要进行参数传递,相当于
func=add,此时的func有两种类型,编译器无法推断出模板函数add的类型,所以编译器报错。
4. 函数模板的显式实参的探讨
(1)假如我们想设计一个加法函数模板,函数模板的参数类型可以不同,但返回类型应该是参数类型的最高类型,如
template<typename T1,typename T2>
? add(const T1 &a, const T2 &b);
现在的问题是函数模板add的返回类型是什么?
假如 int a=1; double b=2.11; add(a, b);
我们希望add返回double类型
解决方案:重新定义模板
template<typenameT1,typename T2,typename T3>
T1 add(const T2&a,const T3 &b)
{
return a+b;
}
但此时会出现问题,那就是编译器无法推断出T1的类型,解决方法很简单,我们写下如下代码:
cout<<add<double>(1,2.111)<<endl;
按照以上的提示,输入“<”后第一个类型就是我们希望函数模板返回的类型T1,其他两个是参数类型,可以省略不写,编译器会根据后面实参的类型来推断形参的类型。
(2)解决以上指向函数的指针二义性的方案
void func(int(*)(const int&, const int&));
void func(string(*)(const string&, const string&));
func(add); //编译器报错
解决方案:显式指定实参,如下代码所示
template<typenameT>
T add(const T&a, const T &b)
{
return a+b;
}
void func(int(*ptrfun)(const int& a, const int& b),const int& a, const int&b) //后面两个参数的声明必须写
{
ptrfun=add<int>;
cout<<ptrfun(a,b)<<endl;
}
void func(string(*ptrfun)(const string& a,const string& b),const string& a,conststring& b)
{
ptrfun=add<string>;
cout<<ptrfun(a,b)<<endl;
}
void main()
{
func(add,1,2);
func(add,"hello","world");
}
5. 函数模板与重载函数的探讨
代码实例如下:
int add(int a, intb)
{
cout<<"调用 int add(int a, int b)"<<endl;
return a+b;
}
double add(doublea, double b)
{
cout<<"调用 void fun(double a, double b)"<<endl;
return a+b;
}
template<typename T>
T add(T a,T b)
{
cout<<"调用模板函数"<<endl;
return a+b;
}
在上面的代码中,我们写了三个重载的函数,其中有一个是函数模板,这三个函数都是完成返回两个加数的和。但这样的代码在调用时很容易出错,
cout<<add(1,2);
//绝对匹配,调用int add(int a, int b)
cout<<add(1.0,2.0);
//绝对匹配,调用double add(double a, double b)
cout<<add(1.0,2);//编译器报错
//error C2782: 'T__cdecl add(T,T)' : template parameter 'T' is ambiguous
原因分析:编译器在编译第一、二行代码的时候,能够找到与实参绝对匹配的函数,编译通过,但是编译器在编译第三行代码的时候,它会做出如下检查:
第一步,编译器会找寻有没有与实参绝对匹配的函数,发现没有;
第二步,编译器会找寻能够实例化的函数模板,因为源代码的模板只有一个类型T,而实参提供了两种类型,所以,编译器找不到能够实例化的函数模板。
解决方案:强制类型转换
cout<<add(1.0,static_cast<double>(2));
或cout<<add(static_cast<int>(1.0),2);
在此,当代码中出现重载、函数模板时,我们总结下编译器选择的次序:
(1)选择与实参完全匹配的函数;
(2)选择能够根据实参类型推断出形参的函数模板并实例化;
(3)选择能够根据实参进行隐式转换的函数。
其实第三步一些编译器不支持,如VC6.0
三、 有关类模板的探讨
有了以上对函数模板的认识,我们对类模板的理解就会比较容易了。就像函数模板一样,我们可以把类模板理解为具有一定类型的类(不要与抽象类混淆),而这种类要根据类型来实例化,以完成特定的功能。
从一个有关链表的例子讲解
链表的操作也许我们并不陌生,在C语言中,我们就学习过了,但那时的链表要想实现通用的效果,我们往往在代码中写下如下的语句:typedef int ElemType; 这样我们要想创建一个double类型的链表,只需要修改int为double即可。现在我们用C++的类模板来写一个双向循环链表程序,代码如下所示:
#include <iostream>
#include <cstdlib>
using namespace std;
template<class T>
class DLNode //结点类
{
public:
T data; //数据域
DLNode<T>*lLink; //前驱指针域,指向当前结点的直接前驱
DLNode<T>*rLink; //后继指针域,指向当前结点的直接后继
DLNode(DLNode<T>* left=0,DLNode<T>* right=0):lLink(left),rLink(right)
{
}
DLNode(const T& val,DLNode<T>* left=0,DLNode<T>* right=0)
{
data=val;
lLink=left;
rLink=right;
}
};
const int Max=100;
template<class T>
class DList //双向循环链表类
{
private:
DLNode<T>* head; //头指针
public:
DList()
{
head=0;
}
DList(int N) //N表示要构造的链表长度
{
if(N<=0 || N>Max)
{
cerr<<"参数不合法!"<<endl;
exit(1);
}
head=new DLNode<T>; //生成头结点
DLNode<T>* p;
DLNode<T>* current=head;
int i;
T data;
for(i=0;i<N;i++)
{
cout<<"请输入第"<<i<<"个数据:";
cin>>data;
if(!cin.good())
{
cerr<<"输入不合法!"<<endl;
exit(1);
}
p=new DLNode<T>(data); //生成一个新的结点
current->rLink=p;
p->lLink=current;
current=p;
}
current->rLink=head;
head->lLink=current;
}
bool IsEmpty() const
{
return head==0 || head->rLink==head || head->lLink==head;
}
void Print() const //打印链表
{
if(IsEmpty()) return;
for(DLNode<T>* p=head->rLink;p!=head;p=p->rLink)
{
cout<<p->data<<" ";
}
cout<<endl;
}
int Length() const
{
DLNode<T>*p;
int n=0;
if(IsEmpty()) return 0;
for(p=head->rLink;p!=head;p=p->rLink)
{
n++;
}
return n;
}
DLNode<T>* Search(const T& val) //查找val,函数返回指向val结点的指针
{
DLNode<T>* p;
if(IsEmpty()) return 0;
p=head->rLink;
while(p->data!=val && p!=head) p=p->rLink;
if(p==head) return 0;
else return p;
}
DLNode<T>* Search(int pos) //按元素的位置进行查找
{
if(IsEmpty()) return 0;
if(pos<0)
{
cerr<<"参数错误!"<<endl;
return 0;
}
int i=0;
DLNode<T>* p=head->rLink;
while(i<pos && p!=head)
{
i++;
p=p->rLink;
}
return p;
}
void Insert(int pos,const T &val) //在指定的位置pos后插入值为val的结点
{
if(IsEmpty()) return;
DLNode<T>* p=Search(pos); //寻找插入位置的指针
if(!p) return; //找不到则返回
DLNode<T>* q=new DLNode<T>(val); //q指向要插入的结点
if(!q) return; //存储空间不足则返回
q->rLink=p->rLink; //进行后插
q->lLink=p;
p->rLink->lLink=q;
p->rLink=q;
}
void Removw(int pos,T& val) //移除位置pos的结点,val带回此结点的元素值
{
if(IsEmpty()) return;
DLNode<T>* p=Search(pos); //寻找删除位置的指针
if(!p) return; //找不到则返回
p->lLink->rLink=p->rLink;
p->rLink->lLink=p->lLink;
val=p->data;
delete p;
}
};
测试:
DList<int>L(3); //定义链表
注意:将类模板实例化时,必须提供类型
cout<<L.Length(); //输出链表的长度
int a;
cin>>a;
DLNode<int>*p=L.Search(a); //查找a
cout<<p->data<<endl;
L.Insert(2,18); //在链表的第2个位置后插入18
L.Print();
L.Removw(3,a); //删除链表第3个位置处的结点
L.Print();
在上面的实例代码中,我们将类模板的声明和定义放在同一个cpp文件中。但有时代码量很大时,我们将类模板的声明放在头文件,而实现则放在cpp文件中,如果是这样做,我们的代码就要写成下面的形式:
template<class T> //此行必须要写
bool DList<T>::IsEmpty() const //类型T不能丢失
{
returnhead==0 || head->rLink==head || head->lLink==head;
}