一、虚函数
首先要明白C++为什么要引进虚函数这个机制, 虚函数就是在基类中被关键字virtual说明,并在派生类中重新定义的函数。虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。 从以上的定义来看,需函数简单的说就是为了让基类指针能够指向派生类中与基类同名的函数而引进的,举个简单的例子,1:你定义了一个“图形类”这样的基类,然后再类中定义了一个求图形周长的函数(不是虚函数);2:现在再定义这个“图形类”的一个派生类“三角形类”,中也含有一个求三角形周长的函数(不是虚函数);3:再定义一个这个“图形类”的一个派生类“矩形类”,中也含有一个求矩形周长的函数(不是虚函数);4:现在回到主函数,你定义了这个“图形类”的一个指针(即基类的指针),根据C++的规定,基类的对象指针可以指向它的公有派生类的对象,但是当其指向公有的派生类对象时,它只能访问派生类中从基类继承来的成员,而不能访问公有派生类中定义的成员。 所以,你定义的这个指针是不能够指向“三角形”和“矩形”类中定义的那个周长函数,但是,如果你在基类中将这个周长函数定义为虚函数,则这样的代码是允许的,而且能够到达预期目的。 为什么呢?你想想你在基类中只定义一个基类指针,可以通过改变基类指针所指的对象(即派生类对象),就可以通过指针访问这个虚函数(虚函数的函数原型是一样的)而完成不同图形周长的计算和输出,显然,这是很值得提倡的,因为你只需“p->perimeter;”一个代码就能完成很多图形的工作。 不知道,你能否明白,总的来说就是虚函数就是为了让基类指针(或引用)能访问派生类中定义的成员。
虚函数的作用是实现动态联编,也就是在程序的运行阶段动态地选择合适的成员函数,在定义了虚函数后,可以在基类的派生类中对虚函数重新定义,在派生类中重新定义的函数应与虚函数具有相同的形参个数和形参类型。以实现统一的接口,不同定义过程。如果在派生类中没有对虚函数重新定义,则它继承其基类的虚函数。
比如:基类定义一个多边形,其种虚函数是求面积;
这个基类有多个派生类,派生类中对从基类继承来的虚函数求面积重定义,
如,派生类三角形,定义了求面积的方法是“半底高”;
派生类正方形,定义了求面积的方法是“边平方”;
……等等
以上的基类中的虚函数纯粹就是为了派生类的重定义而存在……
我们知道,在同一类中是不能定义两个名字相同、参数个数和类型都相同的函数的,否则就是“重复定义”。但是在类的继承层次结构中,在不同的层次中可以出现名字相同、参数个数和类型都相同而功能不同的函数。例如在例12.1(具体代码请查看:C++多态性的一个典型例子)程序中,在Circle类中定义了 area函数,在Circle类的派生类Cylinder中也定义了一个area函数。这两个函数不仅名字相同,而且参数个数相同(均为0),但功能不同,函数体是不同的。前者的作用是求圆面积,后者的作用是求圆柱体的表面积。这是合法的,因为它们不在同一个类中。 编译系统按照同名覆盖的原则决定调用的对象。在例12.1程序中用cy1.area( ) 调用的是派生类Cylinder中的成员函数area。如果想调用cy1 中的直接基类Circle的area函数,应当表示为 cy1.Circle::area()。用这种方法来区分两个同名的函数。但是这样做 很不方便。
人们提出这样的设想,能否用同一个调用形式,既能调用派生类又能调用基类的同名函数。在程序中不是通过不同的对象名去调用不同派生层次中的同名函数,而是通过指针调用它们。例如,用同一个语句“pt->display( );”可以调用不同派生层次中的display函数,只需在调用前给指针变量 pt 赋以不同的值(使之指向不同的类对象)即可。
打个比方,你要去某一地方办事,如果乘坐公交车,必须事先确定目的地,然后乘坐能够到达目的地的公交车线路。如果改为乘出租车,就简单多了,不必查行车路线,因为出租车什么地方都能去,只要在上车后临时告诉司机要到哪里即可。如果想访问多个目的地,只要在到达一个目的地后再告诉司机下一个目的地即可,显然,“打的”要比乘公交车 方便。无论到什么地方去都可以乘同—辆出租车。这就是通过同一种形式能达到不同目的的例子。
C++中的虚函数就是用来解决这个问题的。虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。
请分析例12.2。这个例子开始时没有使用虚函数,然后再讨论使用虚函数的情况。
[例12.2] 基类与派生类中有同名函数。在下面的程序中Student是基类,Graduate是派生类,它们都有display这个同名的函数。运行结果如下:
- #include <iostream>
- #include <string>
- using namespace std;
- //声明基类Student
- class Student
- {
- public:
- Student(int, string,float); //声明构造函数
- void display( );//声明输出函数
- protected: //受保护成员,派生类可以访问
- int num;
- string name;
- float score;
- };
- //Student类成员函数的实现
- Student::Student(int n, string nam,float s)//定义构造函数
- {
- num=n;
- name=nam;
- score=s;
- }
- void Student::display( )//定义输出函数
- {
- cout<<"num:"<<num<<"\nname:"<<name<<"\nscore:"<<score<<"\n\n";
- }
- //声明公用派生类Graduate
- class Graduate:public Student
- {
- public:
- Graduate(int, string, float, float);//声明构造函数
- void display( );//声明输出函数
- private:float pay;
- };
- // Graduate类成员函数的实现
- void Graduate::display( )//定义输出函数
- {
- cout<<"num:"<<num<<"\nname:"<<name<<"\nscore:"<<score<<"\npay="<<pay<<endl;
- }
- Graduate::Graduate(int n, string nam,float s,float p):Student(n,nam,s),pay(p){}
- //主函数
- int main()
- {
- Student stud1(1001,"Li",87.5);//定义Student类对象stud1
- Graduate grad1(2001,"Wang",98.5,563.5);//定义Graduate类对象grad1
- Student *pt=&stud1;//定义指向基类对象的指针变量pt
- pt->display( );
- pt=&grad1;
- pt->display( );
- return 0;
- }
num:1001(stud1的数据)
name:Li
score:87.5
num:2001 (grad1中基类部分的数据)
name:wang
score:98.5
假如想输出grad1的全部数据成员,当然也可以采用这样的方法:通过对象名调用display函数,如grad1.display(),或者定义一个指向Graduate类对象的指针变量ptr,然后使ptr指向gradl,再用ptr->display()调用。这当然是可以的,但是如果该基类有多个派生类,每个派生类又产生新的派生类,形成了同一基类的类族。每个派生类都有同名函数display,在程序中要调用同一类族中不同类的同名函数,就要定义多个指向各派生类的指针变量。这两种办法都不方便,它要求在调用不同派生类的同名函数时采用不同的调用方式,正如同前面所说的那样,到不同的目的地要乘坐指定的不同的公交车,一一 对应,不能搞错。如果能够用同一种方式去调用同一类族中不同类的所有的同名函数,那就好了。
用虚函数就能顺利地解决这个问题。下面对程序作一点修改,在Student类中声明display函数时,在最左面加一个关键字virtual,即
virtual void display( );
这样就把Student类的display函数声明为虚函数。程序其他部分都不改动。再编译和运行程序,请注意分析运行结果:
num:1001(stud1的数据)
name:Li
score:87.5
num:2001 (grad1中基类部分的数据)
name:wang
score:98.5
pay=1200 (这一项以前是没有的)
看!这就是虚函数的奇妙作用。现在用同一个指针变量(指向基类对象的指针变量),不但输出了学生stud1的全部数据,而且还输出了研究生grad1的全部数据,说明已调用了grad1的display函数。用同一种调用形式“pt->display()”,而且pt是同一个基类指针,可以调用同一类族中不同类的虚函数。这就是多态性,对同一消息,不同对象有 不同的响应方式。
说明:本来基类指针是用来指向基类对象的,如果用它指向派生类对象,则进行指针类型转换,将派生类对象的指针先转换为基类的指针,所以基类指针指向的是派生类对象中的基类部分。在程序修改前,是无法通过基类指针去调用派生类对象中的成员函数的。虚函数突破了这一限制,在派生类的基类部分中,派生类的虚函数取代了基类原来的虚函数,因此在使基类指针指向派生类对象后,调用虚函数时就调用了派生类的虚函数。 要注意的是,只有用virtual声明了虚函数后才具有以上作用。如果不声明为虚函数,企图通过基类指针调用派生类的非虚函数是不行的。
虚函数的以上功能是很有实用意义的。在面向对象的程序设计中,经常会用到类的继承,目的是保留基类的特性,以减少新类开发的时间。但是,从基类继承来的某些成员函数不完全适应派生类的需要,例如在例12.2中,基类的display函数只输出基类的数据,而派生类的display函数需要输出派生类的数据。过去我们曾经使派生类的输出函数与基类的输出函数不同名(如display和display1),但如果派生的层次多,就要起许多不同的函数名,很不方便。如果采用同名函数,又会发生同名覆盖。
利用虚函数就很好地解决了这个问题。可以看到:当把基类的某个成员函数声明为虚函数后,允许在其派生类中对该函数重新定义,赋予它新的功能,并且可以通过指向基类的指针指向同一类族中不同类的对象,从而调用其中的同名函数。由虚函数实现的动态多态性就是:同一类族中不同类的对象,对同一函数调用作出不同的响应。
虚函数的使用方法是:
- 在基类用virtual声明成员函数为虚函数。
这样就可以在派生类中重新定义此函数,为它赋予新的功能,并能方便地被调用。在类外定义虚函数时,不必再加virtual。- 在派生类中重新定义此函数,要求函数名、函数类型、函数参数个数和类型全部与基类的虚函数相同,并根据派生类的需要重新定义函数体。
C++规定,当一个成员函数被声明为虚函数后,其派生类中的同名函数都自动成为虚函数。因此在派生类重新声明该虚函数时,可以加virtual,也可以不加,但习惯上一般在每一层声明该函数时都加virtual,使程序更加清晰。如果在派生类中没有对基类的虚函数重新定义,则派生类简单地继承其直接基类的虚函数。- 定义一个指向基类对象的指针变量,并使它指向同一类族中需要调用该函数的对象。
- 通过该指针变量调用此虚函数,此时调用的就是指针变量指向的对象的同名函数。
通过虚函数与指向基类对象的指针变量的配合使用,就能方便地调用同一类族中不同类的同名函数,只要先用基类指针指向即可。如果指针不断地指向同一类族中不同类的对象,就能不断地调用这些对象中的同名函数。这就如同前面说的,不断地告诉出租车司机要去的目的地,然后司机把你送到你要去的地方。
需要说明;有时在基类中定义的非虚函数会在派生类中被重新定义(如例12.1中的area函数),如果用基类指针调用该成员函数,则系统会调用对象中基类部分的成员函数;如果用派生类指针调用该成员函数,则系统会调用派生类对象中的成员函数,这并不是多态性行为(使用的是不同类型的指针),没有用到虚函数的功能。
以前介绍的函数重载处理的是同一层次上的同名函数问题,而虚函数处理的是不同派生层次上的同名函数问题,前者是横向重载,后者可以理解为纵向重载。但与重载不同的是:同一类族的虚函数的首部是相同的,而函数重载时函数的首部是不同的(参数个数或类型不同)。
二、虚基类
多继承时很容易产生命名冲突,即使我们很小心地将所有类中的成员变量和成员函数都命名为不同的名字,命名冲突依然有可能发生,比如非常经典的菱形继承层次。如下图所示:
类A派生出类B和类C,类D继承自类B和类C,这个时候类A中的成员变量和成员函数继承到类D中变成了两份,一份来自 A-->B-->D 这一路,另一份来自 A-->C-->D 这一条路。
在一个派生类中保留间接基类的多份同名成员,虽然可以在不同的成员变量中分别存放不同的数据,但大多数情况下这是多余的:因为保留多份成员变量不仅占用较多的存储空间,还容易产生命名冲突,而且很少有这样的需求。
为了解决这个问题,C++提供了虚基类,使得在派生类中只保留间接基类的一份成员。
声明虚基类只需要在继承方式前面加上 virtual 关键字,请看下面的例子:运行结果:
- #include <iostream>
- using namespace std;
- class A{
- protected:
- int a;
- public:
- A(int a):a(a){}
- };
- class B: virtual public A{ //声明虚基类
- protected:
- int b;
- public:
- B(int a, int b):A(a),b(b){}
- };
- class C: virtual public A{ //声明虚基类
- protected:
- int c;
- public:
- C(int a, int c):A(a),c(c){}
- };
- class D: virtual public B, virtual public C{ //声明虚基类
- private:
- int d;
- public:
- D(int a, int b, int c, int d):A(a),B(a,b),C(a,c),d(d){}
- void display();
- };
- void D::display(){
- cout<<"a="<<a<<endl;
- cout<<"b="<<b<<endl;
- cout<<"c="<<c<<endl;
- cout<<"d="<<d<<endl;
- }
- int main(){
- (new D(1, 2, 3, 4)) -> display();
- return 0;
- }
a=1
b=2
c=3
d=4
本例中我们使用了虚基类,在派生类D中只有一份成员变量 a 的拷贝,所以在 display() 函数中可以直接访问 a,而不用加类名和域解析符。
请注意派生类D的构造函数,与以往的用法有所不同。以往,在派生类的构造函数中只需负责对其直接基类初始化,再由其直接基类负责对间接基类初始化。现在,由于虚基类在派生类中只有一份成员变量,所以对这份成员变量的初始化必须由派生类直接给出。如果不由最后的派生类直接对虚基类初始化,而由虚基类的直接派生类(如类B和类C)对虚基类初始化,就有可能由于在类B和类C的构造函数中对虚基类给出不同的初始化参数而产生矛盾。所以规定:在最后的派生类中不仅要负责对其直接基类进行初始化,还要负责对虚基类初始化。
有的读者会提出:类D的构造函数通过初始化表调了虚基类的构造函数A,而类B和类C的构造函数也通过初始化表调用了虚基类的构造函数A,这样虚基类的构造函数岂非被调用了3次?大家不必过虑,C++编译系统只执行最后的派生类对虚基类的构造函数的调用,而忽略虚基类的其他派生类(如类B和类C)对虚基类的构造函数的调用,这就保证了虚基类的数据成员不会被多次初始化。
最后请注意:为了保证虚基类在派生类中只继承一次,应当在该基类的所有直接派生类中声明为虚基类,否则仍然会出现对基类的多次继承。
可以看到:使用多重继承时要十分小心,经常会出现二义性问题。上面的例子是简单的,如果派生的层次再多一些,多重继承更复杂一些,程序员就很容易陷人迷 魂阵,程序的编写、调试和维护工作都会变得更加困难。因此很多程序员不提倡在程序中使用多重继承,只有在比较简单和不易出现二义性的情况或实在必要时才使用多重继承,能用单一继承解决的问题就不要使用多重继承。也正由于这个原因,C++之后的很多面向对象的编程语言(如Java、Smalltalk、C#、PHP等)并不支持多重继承。
三、类模板
多个类有着共同操作,但是数据类型不同。如下的3个类,getMax的功能是相同的,即求两个数中的最大值,仅仅是数据类型不同。
- class Compare_int
- {
- private:
- int x,y;
- public:
- Compare(int a,int b)
- {
- x=a;
- y=b;
- }
- int getMax()
- {
- return (x>y)? x:y;
- }
- };
- class Compare_float
- {
- private:
- float x,y;
- public:
- Compare(float a,float b)
- {
- x=a;
- y=b;
- }
- float getMax()
- {
- return (x>y)? x:y;
- }
- };
- class Compare_char
- {
- private:
- char x,y;
- public:
- Compare(char a,char b)
- {
- x=a;
- y=b;
- }
- char getMax()
- {
- return (x>y)? x:y;
- }
- };
我们用一个类模板减少重复性的工作。
- template<class dataType>
- class Compare
- {
- private:
- dataType x,y;
- public:
- Compare(dataType a,dataType b)
- {
- x=a;
- y=b;
- }
- dataType getMax()
- {
- return (x>y)? x:y;
- }
- };
template是声明各模板的关键字,表示声明一个模板,模板参数可以是一个,也可以是多个。
声明类模板要增加一行: template<class 类型参数名> 如template<class dataType>其中的类型参数名为虚拟的类型参数名,以后会被实际的类型名替代。如例子中的
dataType将会被int,float,char等替代。
如果说类是对象的抽象,对象是类的实例。那么类模板是类的抽象,类是类模板的实例。
实例化时必须用实际的类型名去替代虚拟的类型,如Compare<int> cmp1(3,7);
完整的代码:
- #include "stdafx.h"
- #include <iostream>
- using namespace std;
- template<class dataType>
- class Compare
- {
- private:
- dataType x,y;
- public:
- Compare(dataType a,dataType b)
- {
- x=a;
- y=b;
- }
- dataType getMax()
- {
- return (x>y)? x:y;
- }
- };
- int main(int argc, char* argv[])
- {
- //类模板的实例化(To 类)and类的实例化(To 对象)
- Compare<int> cmp1(3,7);
- cout<<cmp1.getMax()<<" is the Maximum of two Integer numbers"<<endl;
- Compare<float> cmp2(12.3,23.4);
- cout<<cmp2.getMax()<<" is the Maximun of two Float numbers"<<endl;
- Compare<char> cmp3('a','b');
- cout<<cmp3.getMax()<<" is the Maximun of two Char numbers"<<endl;
- return 0;
- }
一、类模板定义及实例化
1. 定义一个类模板:

1 template<class 模板参数表> 2 3 class 类名{ 4 5 // 类定义...... 6 7 };
其中,template 是声明类模板的关键字,表示声明一个模板,模板参数可以是一个,也可以是多个,可以是类型参数 ,也可以是非类型参数。类型参数由关键字class或typename及其后面的标识符构成。非类型参数由一个普通参数构成,代表模板定义中的一个常量。
例:

1 template<class type,int width> 2 3 //type为类型参数,width为非类型参数 4 5 class Graphics;
注意:
(1)如果在全局域中声明了与模板参数同名的变量,则该变量被隐藏掉。
(2)模板参数名不能被当作类模板定义中类成员的名字。
(3)同一个模板参数名在模板参数表中只能出现一次。
(4)在不同的类模板或声明中,模板参数名可以被重复使用。

1 typedef string type; 2 3 template<class type,int width> 4 5 class Graphics 6 7 { 8 9 type node;//node不是string类型 10 11 typedef double type;//错误:成员名不能与模板参数type同名 12 13 }; 14 15 template<class type,class type>//错误:重复使用名为type的参数 16 17 class Rect; 18 19 template<class type> //参数名”type”在不同模板间可以重复使用 20 21 class Round;
(5)在类模板的前向声明和定义中,模板参数的名字可以不同。

1 // 所有三个 Image 声明都引用同一个类模板的声明 2 3 template <class T> class Image; 4 5 template <class U> class Image; 6 7 // 模板的真正定义 8 9 template <class Type> 10 11 class Image { //模板定义中只能引用名字”Type”,不能引用名字”T”和”U” };
(6)类模板参数可以有缺省实参,给参数提供缺省实参的顺序是先右后左。

1 template <class type, int size = 1024> 2 3 class Image; 4 5 template <class type=double, int size > 6 7 class Image;
(7)类模板名可以被用作一个类型指示符。当一个类模板名被用作另一个模板定义中的类型指示符时,必须指定完整的实参表

1 template<class type> 2 3 class Graphics 4 5 { 6 7 Graphics *next;//在类模板自己的定义中不需指定完整模板参数表 8 9 }; 10 11 template <calss type> 12 13 void show(Graphics<type> &g) 14 15 { 16 17 Graphics<type> *pg=&g;//必须指定完整的模板参数表 18 19 }
2.类模板实例化
定义:从通用的类模板定义中生成类的过程称为模板实例化。
例:Graphics<int> gi;
类模板什么时候会被实例化呢?
①当使用了类模板实例的名字,并且上下文环境要求存在类的定义时。
②对象类型是一个类模板实例,当对象被定义时。此点被称作类的实例化点。
③一个指针或引用指向一个类模板实例,当检查这个指针或引用所指的对象时。
例:

1 template<class Type> 2 3 class Graphics{}; 4 5 void f1(Graphics<char>);// 仅是一个函数声明,不需实例化 6 7 class Rect 8 9 { 10 11 Graphics<double>& rsd;// 声明一个类模板引用,不需实例化 12 13 Graphics<int> si;// si是一个Graphics类型的对象,需要实例化类模板 14 15 } 16 17 int main(){ 18 19 Graphcis<char>* sc;// 仅声明一个类模板指针,不需实例化 20 21 f1(*sc);//需要实例化,因为传递给函数f1的是一个Graphics<int>对象。 22 23 int iobj=sizeof(Graphics<string>);//需要实例化,因为sizeof会计算Graphics<string>对象的大小,为了计算大小,编译器必须根据类模板定义产生该类型。 24 25 }
3.非类型参数的模板实参
要点:
①绑定给非类型参数的表达式必须是一个常量表达式。
②从模板实参到非类型模板参数的类型之间允许进行一些转换。包括左值转换、限定修饰转换、提升、整值转换。
③可以被用于非类型模板参数的模板实参的种类有一些限制。
例:

1 Template<int* ptr> class Graphics{…….}; 2 3 Template<class Type,int size> class Rect{……..}; 4 5 const int size=1024; 6 7 Graphics<&size> bp1;//错误:从const int*->int*是错误的。 8 9 Graphics<0> bp2;//错误不能通过隐式转换把0转换成指针值 10 11 const double db=3.1415; 12 13 Rect<double,db> fa1;//错误:不能将const double转换成int. 14 15 unsigned int fasize=255; 16 17 Rect<String, fasize> fa2;//错误:非类型参数的实参必须是常量表达式,将unsigned改为const就正确。 18 19 Int arr[10]; 20 21 Graphics<arr> gp;//正确
二、类模板的成员函数
要点:
①类模板的成员函数可以在类模板的定义中定义(inline函数),也可以在类模板定义之外定义(此时成员函数定义前面必须加上template及模板参数)。
②类模板成员函数本身也是一个模板,类模板被实例化时它并不自动被实例化,只有当它被调用或取地址,才被实例化。

1 template<class type> 2 3 Class Graphics{ 4 5 Graphics(){…}//成员函数定义在类模板的定义中 6 7 void out(); 8 9 }; 10 11 template<class type>//成员函数定义在类模板定义之外 12 13 void Graphics<type>::out(){…}
三、类模板的友元声明
类模板中可以有三种友元声明:
1.非模板友元类或友元函数

1 class Graphics{void out();}; 2 3 Template<class T> 4 5 Class Rect{ 6 7 friend class Graphics;//类Graphics、函数 8 9 friend void create();// create、 out是类模板 10 11 friend void Graphics::out();// Rect所有实例的友元 12 13 };
2、绑定的友元类模板或函数模板。
3、非绑定的友元类模板或函数模板。
第二种声明表示类模板的实例和它的友元之间是一种一对一的映射关系。
如图:
第三种声明表示类模板的实例和它的友元之间是一种一对多的映射关系。
如图:
例:绑定的友元模板

1 template<class type> 2 3 void create(Graphics<type>); 4 5 template<class type> 6 7 class Graphics{ 8 9 friend void create<type>(Graphics<type>); 10 11 };
例:非绑定的友元模板

1 template<class type> 2 3 class Graphics{ 4 5 template<class T> 6 7 friend void create(Graphics<T>); 8 9 };
注意:当把非模板类或函数声明为类模板友元时,它们不必在全局域中被声明或定义,但将一个类的成员声明为类模板友元,该类必须已经被定义,另外在声明绑定的友元类模板或函数模板时,该模板也必须先声明。
例:

1 template <class T> 2 3 class A { 4 5 private: 6 7 friend class B<T>; //错误:类B必须先声明 8 9 }; 10 11 template <class T> 12 13 class B{};
四、类模板的静态数据成员、嵌套类型
1.类模板的静态数据成员
要点:
①静态数据成员的模板定义必须出现在类模板定义之外。
②类模板静态数据成员本身就是一个模板,它的定义不会引起内存被分配,只有对其实例化才会分配内存。
③当程序使用静态数据成员时,它被实例化,每个静态成员实例都与一个类模板实例相对应,静态成员的实例引用要通过一个类模板实例。
例:

1 template<class type> 2 3 class Graphics{ 4 5 static Graphics *next; 6 7 static const type item; 8 9 }; 10 11 template<class type> 12 13 Graphics<type> * Graphics<type>::next=0; 14 15 template<class type> 16 17 type Graphics<type>::item=NULL; 18 19 //静态成员定义分为两部分:前一部分是类型,比如Graphics<type>*,后一部分是名称和值,比如Graphics<type>::next=0;
2.类模板的嵌套类型
要点:
①在类模板中允许再嵌入模板,因此类模板的嵌套类也是一个模板,它可以使用外围类模板的模板参数。
②当外围类模板被实例化时,它不会自动被实例化,只有当上下文需要它的完整类类型时,它才会被实例化。
③公有嵌套类型可以被用在类定义之外,这时它的名字前必须加上类模板实例的名字。
例:

1 template<class type> 2 3 class Graphics{ 4 5 public: 6 7 template<class T> 8 9 class Rect{void out(type a,T b);}; 10 11 }; 12 13 Graphics<int>::Rect<double> node; 14 15 //引用公有嵌套类型必须加上类模板实例名字
五、成员模板
定义:成员定义前加上template及模板参数表。
要点:
①在一个类模板中定义一个成员模板,意味着该类模板的一个实例包含了可能无限多个嵌套类和无限多个成员函数.
②只有当成员模板被使用时,它才被实例化.
③成员模板可以定义在其外围类或类模板定义之外.
例:

1 template<class type> 2 3 class Graphics<type>{ 4 5 public:template<class T> 6 7 class Rect{void out(type a,T b);};}; 8 9 template<class Gtype> template<class TT> 10 11 void Graphics<Gtype>::Rect<TT>::out(Gtype a,TT b){}//成员模板被定义在类模板定义之外(要根上完整模板实参) 12 13 Graphics<int>的实例可能包括下列嵌套类型: 14 15 Graphics<int>::Rect<double> 16 17 Graphics<int>::Rect<string>
注意:类模板参数不一定与类模板定义中指定的名字相同。
六、类模板的编译模式
1.包含编译模式
这种编译模式下,类模板的成员函数和静态成员的定义必须被包含在“要将它们实例化”的所有文件中,如果一个成员函数被定义在类模板定义之外,那么这些定义应该被放在含有该类模板定义的头文件中。
2.分离编译模式
这种模式下,类模板定义和其inline成员函数定义被放在头文件中,而非inline成员函数和静态数据成员被放在程序文本文件中。
例:

1 //------Graphics.h--------- 2 3 export template<class type> 4 5 Class Graphics 6 7 {void Setup(const type &);}; 8 9 //-------Graphics.c------------ 10 11 #include “Graphics.h” 12 13 Template <class type> 14 15 Void Graphics<type>::Setup(const type &){…} 16 17 //------user.c----- 18 19 #include “Graphics.h” 20 21 Void main() 22 23 {Graphics<int> *pg=new Graphics<int>; 24 25 Int ival=1; 26 27 //Graphics<int>::Setup(const int &)的实例(下有注解) 28 29 Pg->Setup(ival); 30 31 }
Setup的成员定义在User.c中不可见,但在这个文件中仍可调用模板实例Graphics<int>::Setup(const int &)。为实现这一点,须将类模声明为可导出的:当它的成员函数实例或静态数据成员实例被使用时,编译器只要求模板的定义,它的声明方式是在关键字template前加关键字export
3.显式实例声明
当使用包含编译模式时,类模板成员的定义被包含在使用其实例的所有程序文本文件中,何时何地编译器实例化类模板成员的定义,我们并不能精确地知晓,为解决这个问题,标准C++提供了显式实例声明:关键字template后面跟着关键字class以及类模板实例的名字。
例:

1 #include “Graphics.h” 2 3 Template class Graphics<int>;//显式实例声明
显式实例化类模板时,它的所有成员也被显式实例化。
七、类模板的特化及部分特化
1.类模板的特化
先看下面的例子:

1 Template<class type> 2 3 Class Graphics{ 4 5 Public:void out(type figure){…}}; 6 7 Class Rect{…};
如果模板实参是Rect类型,我们不希望使用类模板Graphics的通用成员函数定义,来实例化成员函数out(),我们希望专门定义Graphics<Rect>::out()实例,让它使用Rect里面的成员函数。
为此,我们可以通过一个显示特化定义,为类模板实例的一个成员提供一个特化定义。
格式:template<> 成员函数特化定义
下面为类模板实例Graphics<Rect>的成员函数out()定义了显式特化:
Template<> void Graphics<Rect>::out(Rect figure){…}
注意:
①只有当通用类模板被声明后,它的显式特化才可以被定义。
②若定义了一个类模板特化,则必须定义与这个特化相关的所有成员函数或静态数据成员,此时类模板特化的成员定义不能以符号template<>作为打头。(template<>被省略)
③类模板不能够在某些文件中根据通用模板定义被实例化,而在其他文件中却针对同一组模板实参被特化。
2.类模板部分特化
如果模板有一个以上的模板参数,则有些人就可能希望为一个特定的模板实参或者一组模板实参特化类模板,而不是为所有的模板参数特化该类模板。即,希望提供这样一个模板:它仍然是一个通用的模板,只不过某些模板参数已经被实际的类型或值取代。通过使用类模板部分特化,可以实现这一点。
例:

1 template<int hi,int wid> 2 3 Class Graphics{…}; 4 5 Template<int hi>//类模板的部分特化 6 7 Class Graphics<hi,90>{…};
格式:template<模板参数表>
注意:
①部分特化的模板参数表只列出模板实参仍然未知的那些参数。
②类模板部分特化是被隐式实例化的。编译器选择“针对该实例而言最为特化的模板定义”进行实例化,当没有特化可被使用时,才使用通用模板定义。
例:Graphics<24,90> figure;
它即能从通用类模板定义被实例化,也能从部分特化的定义被实例化,但编译器选择的是部分特化来实例化模板。
③类模板部分特化必须有它自己对成员函数、静态数据成员和嵌套类的定义。
八、名字空间和类模板
类模板定义也可以被放在名字空间中。例如:

1 Namespace cplusplus_primer{ 2 3 Template<class type> 4 5 Class Graphics{…}; 6 7 Template<class type> 8 9 Type create() 10 11 {…} 12 13 }
当类模板名字Graphics被用在名字空间之外时,它必须被名字空间名cplusplus_primer限定修,或者通过一个using声明或指示符被引入。例如:

1 Void main() 2 3 { 4 5 using cplusplus_primer::Graphics; 6 7 Graphics<int> *pg=new Graphics<int>; 8 9 }
注意:在名字空间中声明类模板也会影响该类模板及其成员的特化和部分特化声明的方式,类模板或类模板成员的特化声明必须被声明在定义通用模板的名字空间中(可以在名字空间之外定义模板特化)。
一个关于队列的例子,下面将其代码整理如下:

1 #include "iostream.h" 2 3 template <class Type> class QueueItem; 4 5 template <class Type> 6 7 class Queue { 8 9 public: 10 11 friend ostream& operator<<(ostream &os,const Queue<Type> &q); 12 13 Queue() : front( 0 ), back ( 0 ) { } 14 15 ~Queue(){} 16 17 void add( const Type & ); 18 19 bool is_empty() const 20 21 { 22 23 return front == 0; 24 25 } 26 27 Type remove(); 28 29 private: 30 31 QueueItem<Type> *front; 32 33 QueueItem<Type> *back; 34 35 }; 36 37 template <class Type> 38 39 class QueueItem 40 41 { 42 43 public: 44 45 QueueItem(Type val){item=val;next=0;} 46 47 friend class Queue<Type>; 48 49 friend ostream& operator<<(ostream &os,const Queue<Type> &q); 50 51 friend ostream& operator<<(ostream &os,const QueueItem<Type> &qi); 52 53 54 55 private: 56 57 Type item; 58 59 QueueItem *next; 60 61 }; 62 63 template <class Type> 64 65 void Queue<Type>::add(const Type &val) 66 67 { 68 69 QueueItem<Type> *pt =new QueueItem<Type>(val); 70 71 if ( is_empty() ) 72 73 front = back = pt; 74 75 else 76 77 { 78 79 back->next = pt; 80 81 back = pt; 82 83 } 84 85 } 86 87 template <class Type> 88 89 Type Queue<Type>::remove() 90 91 { 92 93 if ( is_empty() ) 94 95 { 96 97 cerr << "remove() on empty queue \n"; 98 99 exit(-1); 100 101 } 102 103 QueueItem<Type> *pt = front; 104 105 front = front->next; 106 107 Type retval = pt->item; 108 109 delete pt; 110 111 return retval; 112 113 } 114 115 template <class Type> 116 117 ostream& operator<<(ostream &os, const Queue<Type> &q) //输出队列成员 118 119 { 120 121 os << "< "; 122 123 QueueItem<Type> *p; 124 125 for ( p = q.front; p; p = p->next ) 126 127 os << *p << “ ;//用到了Queue和QueueItem的私有成员,因此需将此运算符重 128 129 //载函数声明为Queue和QueueItem的友元,书上没有将此函数声明为QueueItem 130 131 os << “ >”;//的友元。 132 133 return os; 134 135 } 136 137 template <class Type> 138 139 ostream& operator<< ( ostream &os, const QueueItem<Type> &qi ) 140 141 { 142 143 os << qi.item;//用到了QueueItem的私有成员,因此需将此运算符重载函数声明 144 145 //为QueueItem的友元 146 147 return os; 148 149 } 150 151 void main() 152 153 { 154 155 Queue<int> qi; 156 157 cout << qi << endl; 158 159 int ival; 160 161 for ( ival = 0; ival < 10; ++ival ) 162 163 qi.add( ival ); 164 165 cout << qi << endl; 166 167 int err_cnt = 0; 168 169 for ( ival = 0; ival < 10; ++ival ) { 170 171 int qval = qi.remove(); 172 173 if ( ival != qval ) err_cnt++; 174 175 } 176 177 cout << qi << endl; 178 179 if ( !err_cnt ) 180 181 cout << "!! queue executed ok\n"; 182 183 else cout << “?? queue errors: " << err_cnt << endl; 184 185 }
运行结果