1、第9章 模板,主要内容,1,2,3,4,5,9.1 函数模板,9.2 类模板,9.3 模板编译模式,9.4 模板和代码复用,9.5 小结,9.1 函数模板,C+是一种强类型的语言,这对实现某些简单的函数似乎是个障碍。例如下面的max()函数,虽然算法很简单,但C+要求我们为所有要比较的类型都提供一个实现: int max( int a, int b ) return a b ? a : b ; double max( double a, double b ) return a b ? a : b ; 这要求对不同的数据类型重复书写几乎相同的代码。虽然“Copy&Paste”的编辑功能可以减少我
2、们的工作量,但是会造成代码的重复和冗余。而且这种方法还有一个缺点:如果算法本身有所改变,那么对处理所有数据类型的算法代码都要一一进行修改。,9.1 函数模板,一种替代方法是用预处理器宏,例如: #define max(a, b) (a)(b) ?(a):(b) 这个宏定义虽然对简单的max() 调用能够正常工作,但宏调用只是简单的参数替换,和函数调用机制不同,在表达式尤其是有副作用的表达式作参数的情况下,会产生不正确的结果。 函数模板提供了一种描述通用算法的机制,它将算法处理的数据类型参数化,从而可以保留函数定义和函数调用的语义,又不必绕过C+的强类型检查。,9.1.1 函数模板的定义,函数模
3、板提供了一个用来自动生成各种类型函数实例的算法。程序员对于函数接口(参数和返回类型)中的全部或者部分类型进行参数化,而函数体保持不变。如果一个函数的实现对一组实例都是相同的,并且每个实例都处理唯一的一种数据类型,例如max(),那么该函数就可以定义为函数模板。,max()函数模板的定义,下面是max()函数模板的定义:,模板参数,模板参数:模板类型参数,它代表了一种类型;模板非类型参数,它代表一个常量表达式。,模板实例化何时发生,函数模板指定了如何根据一组实际类型或值构造出独立的函数。这个构造过程被称为模板实例化。模板实例化是在函数模板被调用或取地址时隐式发生的。例如下面对上节min1和min
4、2的使用: int ia = 10, 8, 6, 13, 25, 9; double da5 = 10.2, 7.1, 23.0, 3.2, 16.4; int main() /隐式实例化:Type = intint minia = min1(ia, 6);/隐式实例化:Type = doubledouble minda = min1(da, 5); /隐式实例化:Type = int, size = 6int mi = min( ia );/隐式实例化:Type = double, size = 5double md = min( da ); ,编译器对函数模板进行实例化,函数模板本身并不是
5、完整的函数定义,因为其中的类型信息不完整。函数模板被调用时编译器对其进行实例化,生成真正完整的函数定义,再实施对生成函数的调用。根据模板调用是的实参不同,一个函数模板可以实例化得到多个不同的函数定义。,实例化过程,模板实参推演,为了判断用作模板实参的实际类型和值,编译器需要检查函数调用中提供的函数实参的类型。用函数实参的类型来决定模板实参的类型和值的过程被称为模板实参推演。 double minda = min1(da, 5); /da的类型是double: Type=double int mi = min( ia ); /ia的类型是int,大小是6:Type =int,size = 6,显
6、式指定模板实参,在使用函数模板时,必须能够通过上下文为一个模板实参确定一个唯一的类型或值,否则会产生编译错误。 template T1 func(T2 arg1, T3 arg2) /*/ template void goo(T a, T b) /*/ void main() int x, y;double d;x = func( y, d ); /Error:goo(x, d);/Error: 二义性:T是int还是double? 解决这种问题的方法是显式指定模板实参: x = func(y,d);/OK,显式指定 x = func(y, d); /OK,能够推演出T2 T3 goo(d,
7、y); /OK, 对y进行类型转换,9.1.3 函数模板的重载,函数模板可以像一般函数一样重载。例如: template Type max( Type a, Type b )return a b ? a : b ; template Type max( Type a, Type b, Type c )Type t = a b ? a : b ;return t c ? t : c ; ,通常是参数的个数不同,函数模板的重载通常是参数的个数不同,而不是参数类型。例如,下面的两个声明将指同一个函数模板: template Type max( T1 a, T1 b )return a b ? a :
8、 b ; template Type max( T2 a, T2 b )return a b ? a : b ; ,非模板函数重载同名的函数模板,经常使用的还有另一种重载方式:用一个非模板函数重载一个同名的函数模板。例如,用上面的max()函数模板比较两个C风格字符串: char* s1 = “abc”; char* s2 = “def”; max( s1, s2 ); 为此,我们可以写一个专门处理字符串比较的非模板函数,对max()模板进行重载。 char* max( char* a, char* b )return ( strcmp(a, b) 0 ) ? a : b ; 这样,当参数的类
9、型为char*时,会调用这个非模板函数进行比较。,普通函数/模板函数:重载解析步骤,对于一个函数调用,考虑普通函数和模板函数的函数重载解析步骤如下: 寻找一个参数完全匹配的非模板函数,若找到,则调用该函数; 寻找一个函数模板,将其实例化为一个完全匹配的函数,若找到,则调用; 只考虑重载函数集中的普通函数,按照第4章的重载解析过程选择可调用的函数,若找到,则调用。 在上面三步中如果都没有找到匹配的函数,则出现无可调用函数的错误。如果某一步出现了多个匹配的函数,则引起二义性错误。,9.2 类模板,假设我们想定义一个类来支持队列的机制。队列是一种用于对象集合的数据结构,对象被加入到队列的尾部,而从队
10、列的顶部被删除,称为先进先出(FIFO)。我们设计的队列支持下面的操作: 在队列尾加入一个元素; 从队列首删除一个元素; 判断队列是否为空; 判断队列是否已满。,问题,这个队列类可能如下定义: class Queue public:Queue();Queue();Type 现在的问题是,对于Type,我们应该使用什么类型?如果我们选择int来代替Type,那么这个Queue类就只能处理int型的对象。如果想用队列处理另一种类型的数据,例如double、string或者用户自定义的对象,应该怎么办?,一种解决方法,一种解决方法是拷贝代码,拷贝整个Queue 类的实现,并修改它,使之能够对doub
11、le类型工作,然后对下一个类型再拷贝,再修改。而且由于类名字不能重载,必须为每个队列类指定不同的名字:IntQueue、DoubleQueue、StringQueue,等等。每当需要一个新类时,我们重复这个拷贝、修改和重命名的过程。这种过程不仅是无休止的,而且会引起复杂的维护问题。,C+中用类模板来定义通用类型,可以定义队列类模板,利用模板机制为每个具体类型的队列自动生成相应的类定义。 Queue类模板可以如下定义: template class Queue public:Queue();Queue();Type /string队列,9.2.1 类模板的定义,类模板的定义和声明都以关键字tem
12、plate开头,template之后是尖括号括起来的模板参数表。模板参数可以是模板类型参数,也可以是模板非类型参数。 模板类型参数由关键字class或typename后加一个标识符构成。class和typename这两个关键字在模板参数表中的意义是相同的。 一个类模板可以有多个类型参数,每个类型参数前都要有class或typename关键字。 模板非类型参数由一个普通的参数声明构成。模板非类型参数代表了一个潜在的值,这个值是模板定义中的一个常量。在模板被实例化时,非类型参数会被一个编译时刻已知的常量值代替。,类模板Queue的定义,/queue.h template class QueueIt
13、em public: QueueItem(const T,9.2.2 类模板的实例化,类模板定义本身并不定义任何类,而只是指定了怎样根据一个或多个实际的类型或值来构造单独的类。从通用的类模板定义生成类的过程被称为模板实例化。例如: Queue qi; / Type用int代替 Queue qs; /Type用string代替 这两条语句分别将Queue类模板实例化为两个类:一个是将类模板中的参数Type用int代替,另一个是将Type用string代替。qi和qs都是类类型的对象,但是分属于Queue和Queue两个不同的类型。 同一个类模板用不同的类型实例化后得到的类之间并没有任何特殊的关系
14、,类模板的每个实例都是一个独立的类型。 类模板在实例化时必须显式指定模板实参。声明一个类模板实例的指针和引用不会引起类模板的实例化。,9.2.3 类模板的成员函数,和非模板类一样,类模板的成员函数也可以在类模板的定义中定义,这时,该成员函数是inline成员函数,例如Queue 模板类中的is_empyt()成员函数。 成员函数也可以定义在类模板定义之外,这时要使用特殊的语法,以指明它是一个类模板的成员。成员函数定义的前面必须加上关键字 template 以及模板参数。,成员函数类外定义,Queue类模板的析构函数和add()的类外定义如下: template Queue:Queue() wh
15、ile ( !is_empty() )remove(); template void Queue:add(const Type ,9.2.4 类模板的非类型参数,模板的非类型参数代表一个常量值,大多数情况下都是整型。如果使用模板非类型参数表示堆栈的大小,Stack 类模板也可以如下定义: template /cap 在Stack 定义中用作常量 class Stack public: Stack(); /构造函数不带int 参数,由模板参数cap 提供堆栈大小 Stack() delete ele; void push(Type e) ele+top = e; Type pop() retur
16、n eletop-; bool empty() return top = bottom; ; bool full() return top = size-1; private: Type* ele; int top; int size; const static int bottom = -1; ;,9.2.4 类模板的非类型参数,/成员函数的类外定义,两个模板参数 template Stack:Stack() assert( cap 0 ); size = cap; ele = new Typesize; top = bottom; /- /使用Stack 类模板的代码 int main()
17、 Stack is; /实例化时指定两个参数:Type=int, cap=10 for(int i=0; i10; i+) is.push(i); while(!is.empty() coutis.pop()“t“; ,9.2.5 类模板的静态数据成员,类模板可以声明静态数据成员,静态数据成员的定义必须出现在类模板定义之外。 类模板的每个实例都有自己的一组静态数据成员。每个静态数据成员实例都与一个类模板实例相对应。因此,一个静态数据成员的实例总是通过一个特定的类模板实例被引用。,9.2.6 类模板中的友元,三种友元声明可出现在类模板中: 非模板友元类或友元函数。不使用模板参数的友元类或友元函数
18、是类模板的所有实例的友元。 绑定的友元类模板或函数模板。使用模板类的模板参数的友元类和友元函数与实例化后的模板类实例之间是一一对应的关系。 非绑定的友元类模板或函数模板。这时友元类模板或函数模板有自己的模板参数,因此和模板类实例之间形成一对多的映射关系,即对任一个模板类实例,友元类模板或函数模板的所有实例都是它的友元。C+ 标准化之前的编译器不支持这种友元声明。,例如,template class MTC public: friend void foo (); /foo()是所有MTC模板的实例的友元 friend void goo(vect, v); /每个实例化的MTC类都有一个相应的go
19、o()友元实例 template friend void hoo( MTC ); /对MTC的每个实例,hoo()的每个实例都是它的友元 ;,9.3 模板编译模式,函数模板本身并不定义任何函数,它只是一组函数实例定义的规范描述。同样,类模板也只是一组类类型的定义的规范描述,类模板定义本身并没有定义任何类类型。只有编译器看到模板真正被使用时才实例化该模板。函数模板和类模板都可以被实例化多次,在实例化模板时,要求有模板的完整定义。那么模板定义的代码应该如何组织呢? C+ 模板编译模式指定了定义和使用模板的程序代码组织方式。 C+支持两种模板编译模式:包含模式和分离模式。,9.3.1 包含编译模式,
20、在包含编译模式下,模板被实例化的文件中要包含模板的完整定义。对函数模板而言,要在它被实例化的每个文件中都包含其定义;对类模板而言,类模板的定义和所有成员函数以及静态数据成员的定义也必须完全被包含在每个要将它们实例化的文件中。所以,一般将函数模板的定义,类模板的完整定义(包括在类模板外定义的成员函数)都放在头文件中,在使用模板的文件中包含相应的头文件。 注意,为了避免因为模板定义被多次包含引起的编译错误,在模板定义的头文件中应该使用包含守卫:#ifndef/#define/#endif。,templatemin.h,/模板定义头文件 templatemin.h #ifndef TEMPLATE_
21、MIN_H #define TEMPLATE_MIN_H template Type min(Type array, int size) Type min_val = array0;for (int ix = 1; ix size; +ix )if ( arrayix min_val ) min_val = arrayix;return min_val; #endif /TEMPLATE_MIN_H,test.cpp,/使用模板定义的源文件test.cpp include #include “templatefunc.h“ using namespace std; int main()int
22、a=2,5,3,4,1,7;coutmin(a, 6)endl;double da = 1.2,3,5,7.2;cout min(da, 4)endl; ,包含编译模式两个缺点,这种组织方式有两个缺点: 函数体中描述的是实现细节,如果将函数定义和成员函数的定义都放在头文件中,就不能隐藏这些细节。 在多个文件中包含相同的函数模板定义和类模板定义会增加不必要的编译时间。,9.3.2 分离编译模式,分离编译模式允许我们将类模板的接口与它的实现分离。在分离编译模式下,函数模板的声明放在头文件中,定义放在源文件中;类模板的声明放在头文件中,而成员函数和静态数据成员的定义被放在源文件中。在这种模式下,模板
23、代码的组织方式,与非模板代码的组织方式相同。只不过,我们要用export关键字将模板声明为可导出的。,分离编译模式例子,/smodel.h /只提供函数模板的声明 template Type max(Type x, Type y); /smodel.cpp /提供函数模板的定义 export template Type max(Type x, Type y) /* */ /客户代码中只需要包含头文件就可使用模板了 #include “smodel.h” min(a, b);,可导出的类模板,可导出的类模板在声明或定义时在template关键字前加上export: /queue.h export
24、 template class Queue; /queue.cpp #include “queue.h” /模板类成员函数的定义 在模板的实现文件(queue.cpp)中,成员函数的定义也自动被声明为可导出的。在其他文件使用这些成员函数的实例之前,这些成员的定义可以不出现。,类模板的部分成员声明为可导出,也可以将类模板的部分成员声明为可导出的,这时关键字export不是在类模板上指定,而是在导出的成员函数定义上指定。那么那些没有指定export的成员定义也必须和类定义一起放在头文件中。,可导出的类模板,/queue.h template class Queue public:Type temp
25、late Type& Queue:remove() /queue.cpp #include “queue.h” /add()是可导出的成员函数 export template void Queue:add(const Type& val),大部分C+编译器不支持,分离编译模式虽然使我们能够将函数模板的接口同其实现分离 , 但是,分离编译模式需要更复杂的程序设计环境,所以它们不能在所有的C+ 编译器实现中提供。目前大部分C+ 编译器都不支持分离编译模式,如GNU C+和Microsoft Visual C+,都采用了忽略export关键字的策略。,编写模板代码时采用的步骤,实际编程时,模板语法容
26、易引起编译错误。为了使代码调试更容易,在编写模板代码时通常采用以下步骤: 编写对某个类型适用的普通函数或普通类,编译、运行、测试,确定代码可以正确工作之后进入下一步; 使用模板语法改写代码,用包含编译模式调试模板代码。注意模板定义头文件中要使用包含守卫; 如果编译器支持export,再将代码改写为分离编译模式进行调试。,9.4 模板和代码复用,模板机制支持通用型程序设计,也是一种复用代码的机制,与继承和组合相比,模板的使用限制更多,灵活性不足,因而适用范围有限。 与继承类似,模板描述的也是共性。只不过继承描述类型在概念上的相似性,而模板要求代码的相似度。模板对代码的相似度要求非常严苛:除了算法
27、处理的数据类型不同之外,其余代码应该完全相同。 继承描述一组具有公共接口的类之间的层次关系。类层次中各个类之间的差异往往体现在对消息的不同处理方式上。类模板在实例化之后形成真正的类型定义。实例化同一个类模板得到的各种类型之间除了代码相似之外,没有任何内在的逻辑关系,这些类可以分别实例化各自的对象,同样,这些对象也是互不相干。类模板的不同实例之间的差异体现在代码中的某些数据类型和常量值不同。 继承利用虚函数的运行时绑定展现包含多态性,是动态机制。模板则通过类型参数化实现了参数多态性,模板实例化发生在编译期间,是静态机制。,9.5 小结,模板是重用代码的一种机制,利用模板可以进行与类型无关的通用型程序设计。 函数模板可以实现通用算法。函数模板的实例化只在函数被调用的时候发生。 类模板可以定义通用的数据结构。类模板的实例化只在创建对象时发生,声明类模板实例对象的指针或引用不能引起类模板的实例化。 目前的大多数C+编译器只支持模板的包含编译模式,组织代码时需要注意。,Thank You!,