Template 虽然很重要,但它与「运用MFC」有什么关系?有!第8章当我们开始设计Scribble 程序时,需要用到MFC 的collection classes,而这一组类别自从MFC 3.0以来就有了template 版本(因为Visual C++ 编译器从2.0 版开始支持C++ template)。运用之前,我们总该了解一下新的语法、精神、以及应用。
到底什么是template?重要性如何?Kaare Christian 在1994/01/25 的PC-Magazine上有一篇文章,说得很好:
无性生殖并不只是存在于遗传工程上,对程序员而言它也是一个由来已久的动作。过去,我们只不过是以一个简单而基本的工具,也就是一个文字编辑器,重制我们的程序代码。今天,C++ 提供给我们一个更好的繁殖方法template。复制一段既有程序代码的一个最平常的理由就是为了改变数据类型。举个例子,假设你写了一个绘图函数,使用整数x, y 坐标;突然之间你需要相同的程序代码,但坐标值改采用long。你当然可以使用一个文字编辑器把这段码拷贝一份,然后把其中的数据类型改变过来。有了C++,你甚至可以使用多载(overloaded)函数,那么你就可以仍旧使用相同的函数名称。函数的多载的确使我们有比较清爽的程序代码,但它们意味着你还是必须
在你的程序的许多地方维护完全相同的算法。C 语言对此问题的解答是:使用宏。虽然你因此对于相同的算法只需写一次程序代码,但宏有它自己的缺点。第一,它只适用于简单的功能。第二个缺点比较严重:宏不提供资料型别检验,因此牺牲了C++ 的一个主要效益。第三个缺点是:宏并非函数,程序中任何调用宏的地方都会被编译器前置处理器原原本本地插入宏所定义的那一段码,而非只是一个函数调用,因此你每使用一次宏,你的执行文件就会膨胀一点。Templates 提供比较好的解决方案,它把「一般性的算法」和其「对资料型别的实作部份」区分开来。你可以先写算法的程序代码,稍后在使用时再填入实际资料型别。新的C++ 语法使「资料型别」也以参数的姿态出现。有了template,你可以拥有宏「只写一次」的优点,以及多载函数「类型检验」的优点。
C++ 的template 有两种,一种针对function,另一种针对class。
Template Functions
template <class T>
T power(T base, int exponent);
0001 template <class T>
0002 T power(T base, int exponent)
0003 {
0004 T result = base;
0005 if (exponent == 0) return (T)1;
0006 if (exponent < 0) return (T)0;
0007 while (--exponent) result *= base;
这样的函数声明是以一个特殊的template 前缀开始,后面紧跟着一个参数列(本例只一个参数)。容易让人迷惑的是其中的"class" 字眼,它其实并不一定表示C++ 的class,它也可以是一个普通的数据类型。<class T> 只不过是表示:T 是一种类型,而此一类型将在调用此函数时才给予。
下面就是power 函数的template 版本:
#0001 template <class T>
#0002 T power(T base, int exponent)
#0003 {
#0004 T result = base;
#0005 if (exponent == 0) return (T)1;
#0006 if (exponent < 0) return (T)0;
#0007 while (--exponent) result *= base;
#0008 return result;
#0009 }
主义:上面就是power 函数的template 版本:传回值必须确保为类型T,以吻合template 函数的声明。
下面是template 函数的调用方法
#0001 #include <iostream.h>
#0002 void main()
#0003 {
#0004 int i = power(5, 4);
#0005 long l = power(1000L, 3);
#0006 long double d = power((long double)1e5, 2);
#0007
#0008 cout << "i= " << i << endl;
#0009 cout << "l= " << l << endl;
#0010 cout << "d= " << d << endl;
#0011 }
执行结果如下:
i= 625
l= 1000000000
d= 1e+010
在第一次调用中,T 变成int,在第二次调用中,T 变成long。而在第三次调用中,T 又成为了一个long double。但如果调用时候把数据类型混乱掉了,像这样:int i = power(1000L, 4); // 基值是个long,传回值却是个int。错误示范!编译时就会出错。
template 函数的资料型别参数T 究竟可以适应多少种类型?我要说,几乎「任何资料型态」都可以,但函数中对该类型数值的任何运算动作,都必须支持-- 否则编译器就不知道该怎么办了。以power 函数为例,它对于result 和base 两个数值的运算动作有:
1. T result = base;
2. return (T)1;
3. return (T)0;
4. result *= base;
5. return result;
C++ 所有内建数据类型如int 或long 都支持上述运算动作。但如果你为某个C++ 类别产生一个power 函数,那么这个C++ 类别必须包含适当的成员函数以支持上述动作。如果你打算在template 函数中以C++ 类别代替class T,你必须清楚知道哪些运算动作曾被使用于此一函数中,然后在你的C++ 类别中把它们全部实作出来。否则,出现的错误耐人寻味
Template Classes
我们也可以建立template classes,使它们能够神奇地操作任何类型的资料。下面这个例子是让CThree 类别储存三个成员变量,成员函数Min 传回其中的最小值,成员函数Max 则传回其中的最大值。我们把它设计为template class,以便这个类别能适用于各式各样的数据类型:
#0001 template <class T>
#0002 class CThree
#0003 {
#0004 public :
#0005 CThree(T t1, T t2, T t3);
#0006 T Min();
#0007 T Max();
#0008 private:
#0009 T a, b, c;
#0010 };
语法还不至于太稀奇古怪,把T 看成是大家熟悉的int 或float 也就是了。下面是成员函数的定义:
#0001 template <class T>
#0002 T CThree<T>::Min()
#0003 {
#0004 T minab = a < b ? a : b;
#0005 return minab < c ? minab : c;
#0006 }
#0007
#0008 template <class T>
#0009 T CThree<T>::Max()
#0010 {
#0011 T maxab = a < b ? b : a;
#0012 return maxab < c ? c : maxab;
#0013 }
#0014
#0015 template <class T>
#0016 CThree<T>::CThree(T t1, T t2, T t3) :
#0017 a(t1), b(t2), c(t3)
#0018 {
0019 return;
#0020 }
这里就得多注意些了。每一个成员函数前都要加上template <class T>,而且类别名称应
该使用CThree<T>。
以下是template class 的使用方式:
#0001 #include <iostream.h>
#0002 void main()
#0003 {
#0004 CThree<int> obj1(2, 5, 4);
#0005 cout << obj1.Min() << endl;
#0006 cout << obj1.Max() << endl;
#0007
#0008 CThree<float> obj2(8.52, -6.75, 4.54);
#0009 cout << obj2.Min() << endl;
#0010 cout << obj2.Max() << endl;
#0011
#0012 CThree<long> obj3(646600L, 437847L, 364873L);
#0013 cout << obj3.Min() << endl;
#0014 cout << obj3.Max() << endl;
#0015 }
执行结果如下:
2 5
-6.75
8.52
364873
646600
稍早我曾说过,只有当template 函数对于资料型别T 支持所有必要的运算动作时,T 才得被视为有效。此一限制对于template classes 亦属实。为了针对某些类别产生一个CThree,该类别必须提供copy 构造式以及operator<,因为它们是Min 和Max 成员函数中对T 的运算动作。
但是如果你用的是别人template classes,你又如何知道什么样的运算动作是必须的呢?唔,该template classes 的说明文件中应该有所说明。如果没有,只有源代码才能揭露秘密。C++ 内建资料型别如int 和float 等不需要在意这份要求,因为所有内建的资料类型都支持所有的标准运算动作。
Templates 的编译与联结
对程序员而言C++ templates 可说是十分容易设计与使用,但对于编译器和联结器而言却是一大挑战。编译器遇到一个template 时,不能够立刻为它产生机器码,它必须等待,直到template 被指定某种类型。从程序员的观点来看,这意味着template function 或template class 的完整定义将出现在template 被使用的每一个角落,否则,编译器就没有足够的信息可以帮助产生目的码。当多个源文件使用同一个template 时,事情更趋复杂。随着编译器的不同,掌握这种复杂度的技术也不同。有一个常用的技术,Borland 称之为Smart,应该算是最容易的:每一个使用Template 的程序代码的目的档中都存在有template码,联结器负责复制和删除。假设我们有一个程序,包含两个源文件A.CPP 和B.CPP,以及一个THREE.H(其内定义了一个template 类别,名为CThree)。A.CPP 和B.CPP 都包含THREE.H。如果A.CPP以int 和double 使用这个template 类别,编译器将在A.OBJ 中产生int 和double 两种版本的template 类别可执行码。如果B.CPP 以int 和float 使用这个template 类别,编译器将在B.OBJ 中产生int 和float 两种版本的template 类别可执行码。即使虽然A.OBJ 中已经有一个int 版了,编译器没有办法知道。然后,在联结过程中,所有重复的部份将被删除。请看下图。
附冒泡排序的模板
template<class Stype>
void bubble(Stype *item,int count)
{
register int i,j;
Stype t;
for (i=1;i<count;i++)
{
for (j=count-1;j>=i;j--)
{
if (item[j-1]>item[j])//当前一项比后一项大时,交换2个数
{
t=item[j-1];
item[j-1]=item[j];
item[j]=t;
}
}
}
}
void main()
{
char str[]="htegshqqhsadqshdh";
int len=strlen(str);
bubble(str,len);
cout<<str<<endl;
int num[]={8,4,6,3,1,2};
bubble(num,6);
for (int i=0;i<6;i++)
{
cout<<num[i]<<endl;
}
}