|
|
|
《面向对象程序设计》自学方法指导 |
|
|
来源: 发布时间:2006-5-29 17:04:30 点击: |
程序在基类中定义了虚拟函数afn,在派生类中定义了函数afn。该函数在两个类中重载。虚函数的重载与普通函数的重载是有区别的。那么.关键字virtual对重载过程有什么影响呢?从程序中看到,当发送指向一个对象的指针的消息时.使用了形如"对象->消息"的记号。如果没有关键字virtual,则系统在编译(早期联编)时根据该指针对象的类型确定与这个消息有关的对象。 假定Derived是Base的派生类,并在两个类中都定义了方法print,则指向派生类的指针与指向基类的指针是相容的。于是有: Base *base; Derived *derived; base=derived; 当消息发送到base,即执行 base->print()时,系统将调用Base中的方法print。即使base被置为derived,在技术上base仍指向类base,因此系统调用类Base中的方法。如果在类Base中的方法print前加上关键字virtual,就可以使系统不根据指针的类型访问print,而是根据指针的引用(滞后联编)来访问print。因此,如果函数print被声明为虚拟函数,则执行 base->print()时,将访问类Derived中的方法print(),虽然对象base是一个指向基类的指针,但它仍引用一个派生类。 在处理虚函数时,编译程序把第一个虚函数处理成它所在类的隐藏数据成员以使虚函数发挥作用。这些隐藏数据成员是对象实际所属类的虚拟数的指针。在上面的例子中,一个Base对象有一个指向它自己的方法afn作为其隐藏成员,编译程序就是通过这个隐藏指针来调用虚函数的。 当一个函数被声明为virtual函数时,就要使用滞后联编。在上面的例子中,afn被声明为virtual函数。afn函数的地址生成一张表,成为虚函数表VFT(virtual function table),而在运行时再指向一个具体的地址。每个对象都有一个内部指针,指向这张表。 利用基类和派生类形成一种从抽象到具体、从一般到特殊的层次关系,是多态性应用的根本思想。基类提供了派生类直接使用的所有成员函数,而派生类必须定义这些函数的实现版本。由于基类定义了接口形式,所以它的任何派生类都使用同一接口。在C++中,统一的接口是通过虚函数来实现的。这种方法更接近于人类的自然思维方法。所有派生类的对象都以同样的方式访问,只要记住一个(而不是多个)接口即可。此外,接口和实现是分离的,这就为建立各种类库提供了方便。我们通过一个具体的例子来看。例:编写程序计算三角形和矩形的面积。 //程序6-1 //声明矩形类 //计算面积 #include<iostream.h> class Rectangle :public Shape //声明基类 { class Shape public: { void DisArea(void) protected: { cout<<"Rectangle area is:" double x,y; <<x*y<<endl;} public: }; void SetData(double i,double j,) int main() { x=i; { y=j;} Shape *pShape;//定义基类指针 virtual DisArea(void) Triangle t; //定义三角类指针 { cout<<"No area computation Rectangle r; //定义矩形类指针 defined。"<<engl;} }; pShape=&t; //声明三角形类 pShape->SetData(10.0,5.0); class Triangle:public Shape pShape->DisArea(); { public: pShape=&r; void DisArea(void) pShape->SetData(10.0,5.0); { cout<<"Triangle area is:" pShape->DisArea(); <<x*0.5*y<<endl;} return 0; }; } 程序执行结果如下: Triangle area is 25; Rectangle area is 50; 该程序定义了一个类Shape(形状),并提供了一个计算和显示面积的接口,即DisArea。基类Shape本身是抽象的(泛化),而由它派生的Triang1e类(三角形)和类Rectangle(矩形)是具体的(特化)。SetData是一个成员函数,用来提供初值(三角形的底、高和矩形的长、宽),它对所有派生类是相同的。DisArea被定义为虚函数,除了表明在基类中调用了该函数外、它不执行任何操作。但是,由于计算方法不同,该函数在派生类Triangle和Rectangle中有各自不同的版本。在main()函数中,其调用过程与前面的例子类似。 在使用虚函数时,要注意以下几点: ①虚函数在基类和派生类中的定义形式要完全一样,即所有虚函数的名字、返回值类型、参数个数及类型都必须相同。如果基类与派生类中虚函数的原型不同,编译程序将忽略关键字virtual,并把该函数作为派生类中一般和重裁函数来对待。 ②为了运用类机制的高度灵活性,除构造函数外,基类中的其他成员函数最好都要声明为虚函数。这样,不用修改原来的代码,就能很容易地重新定义一个函数的功能。 ③派生类中虚函数的定义不-定要使用关键字virtual,只要满足上面(第l点所说的条件即可。但是,为了便于阅读程序,不致引起混乱,最好还是在基类和派生类中都要在虚函数的定义中加上关键字virtual。 ④虚函数必须是其所在类的成员,不能是友元函数。但虚拟函数可以是另一个类的友元。 抽象基类、纯虚函数和空的虚函数 与普通函数-样,基类中的虚拟函数一般必须用一个函数体来定义其操作,但是,如果该虚函数给不出任何有意义的操作,我们可以把它声明为纯虚函数。纯虚函数的定义格式如下: virtual 返回值类型名 函数名(参数表)= 0; 例如,程序6-1中基类Shape中定义的虚函数,由于没有进行任何操作,而且在派生类Triangle和Rectangle中,只是调用的本身的函数,并没有对基类中的DisArea()进行调用,所以我们可以把其设为纯虚函数: class Shape { …… public: //纯虚函数的定义 virtual void DisArea(void) = 0; …… }; 带有一个或多个纯虚函数的类叫做抽象类。抽象类只能作为其他类的基类,因此通常把抽象类也称为抽象基类。C++规定,不能建立抽象类的对象,也不能把抽象类作为函数的参数类型或返回值类型。但可以定义指向抽象类的指针。如果初始化时不需要临时对象,则允许对抽象类的引用。 例如: struct point { //定义纯虚数 virtual void rotateshape(int)=0; …… virtual void drawshape()= 0; }; virtual void hiliteshape()=0; class Shape };//shape类定义完毕 { point center; public: shape s1; //错误,不能定 //定义成员函数 //义抽象类的对象 point getwhere() shape * ps; //正确,可以定义抽象类的指针 { return center; shape fn();// 错误,不能返回抽象类的类型 } void moveshape(point p) shape fn2(shape s2);//错误,不能用抽象类作为参数类型 { center = p; shape & fn3(shape &)//正确,可以 } //为返回值和函数参数引用抽象类 任何由抽象类派生出来的派生类都必须重新声明它所继承的全部纯虚函数,不管这些纯虚函数在该派生类中作为普通的虚函数还是纯虚函数。例如,对于前面定义的抽象基类shape,可以这样定义它的派生类: class circle:public shape { int radius; public: virtual void rotateshape(int){};//空的虚函数 virtual void drawshape(); //虚函数 virtual void hiliteshape()=0;//纯虚函数 }; 上例在派生类circle中重新声明了抽象基类shape中的全部纯虚函数。其中rotateshape的函数体为空,表明对circle(圆)无需执行旋转操作。drawshape仍被声明为虚拟函数,其函数体将在类的外部定义。函数hiliteshape仍被声明为纯虚函数。因此,shape的派生类cir1e仍然是一个抽象类。 大家可能注意到,在派生类circle中,定义了一个空的虚函数rotateshape(int)。空的虚函数是指在派生的中间类中,有的虚函数对于本派生类并没有任何作用,但是在以此中间类为基类的派生类中,它又发挥出了作用,在这种情况下,在中间类中就声明了一个什么操作都不做得虚函数,即空的虚函数。 |
|
|
|