第九章 继承与多态_第1页
第九章 继承与多态_第2页
第九章 继承与多态_第3页
第九章 继承与多态_第4页
第九章 继承与多态_第5页
已阅读5页,还剩41页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

第九章继承与多态

主要内容:

理解基类和派生类的概念掌握继承的概念和用法理解虚函数、纯虚函数和抽象基类的概念和用法理解多态的概念和作用,掌握多态的实现方法理解虚析构函数的概念和作用,掌握虚析构函数的用法

9.1基类和派生类面向对象程序设计的其它重要特征还包括:继承和多态。

继承能够从一个类派生出另一个类,前者称之为基类或父类,后者称之为派生类或子类。派生类能够继承基类的功能,也能改变或增加它的功能。代码复用是C++最重要的性能之一,它是通过类继承机制来实现的。通过类继承,我们可以复用基类的代码,并可以在继承类中增加新代码或者覆盖基类的成员函数,为基类成员函数赋予新的意义,实现最大限度的代码复用。

多态是指在基类中定义的属性或行为,在派生类中可以具有不同的类型或表现出不同的行为.

(1)它允许指向基类对象的指针指向派生类的对象。

(2)基类和派生类中可有名字和参数完全相同的函数,但他们的功能并不相同。

(3)当程序运行时,如果用基类指针调用成员函数,能够根据该指针所指向的对象的类型自行确定是调用基类的成员函数还是调用派生类的成员函数。如果没有多态的功能,我们就不得不用条件语句,确定是调用哪一个类的成员函数。

9.1.1继承的定义及工作方式

我们可以用一个简单的例子解释什么叫继承。假定我们要处理2维空间中的点,定义了一个称之为twoD的2维空间点类:

程序段9-1classtwoD

{

protected:

doublex,y;//x和y坐标

public:

twoD(doublei,doublej):x(i),y(j){}//内联构造函数

voidsetX(double

NewX){x=NewX;}//内联成员函数

voidsetY(double

NewY){y=NewY;}//内联成员函数

doublegetX()const{returnx;}

doublegetY()const{returny;}

};假定后来又要处理3维空间点的情形,一个直接的方法是再定义一个3维空间点类threeD:程序段9-2classthreeD

{

protected:

doublex,y,z;//x、y和z坐标

public:

threeD(doublei,doublej,doublek):x(i),y(j),z(k){}

voidsetX(double

NewX){x=NewX;}

voidsetY(double

NewY){y=NewY;}

voidsetZ(double

NewZ){z=NewZ;}

doublegetX()const{returnx;}

doublegetY()const{returny;}

doublegetZ()const{returnz;}

};如果使用继承,我们可以将threeD类的定义改写为://使用继承定义threeD类

classthreeD:publictwoD

{

private:

doublez;

public:

threeD(doublei,doublej,doublek):twoD(i,j){z=k;}

voidsetZ(double

NewZ){z=NewZ;}

doublegetZ(){returnz;}

};

上例中,twoD称为基类,threeD称为派生类。应该注意到:派生类threeD中,setX()、setY()、getX(、getY()函数没有再定义,因为这些函数是可从基类twoD继承,就好象在threeD类中定义了这些函数一样。

注意:twoD的构造函数用threeD的构造函数的初始化表中,说明基类的数据成员先初始化。基类的构造函数和析构函数不能被派生类继承。每一个类都有自己的构造函数和析构函数,如果用户没有显式定义,则编译器会隐式定义缺省的构造函数和析构函数下面是一个可以编译和运行的完整的程序:例9-1//文件名:Points.cpp

//功能:演示继承的思想

#include<iostream.h>

ClasstwoD;//超前声名ClassthreeD;voidmain()

{

twoDObj2(1,1);

threeDObj3(1,2,3);

cout<<"Coordinatesfor3dobjectis:"<<endl;

cout<<Obj3.getX()<<","<<Obj3.getY()<<","<<

Obj3.getZ()<<endl;

}classtwoD{

protected:

doublex,y;

public:

twoD(doublei,doublej):x(i),y(j){}

voidsetX(double

NewX){x=NewX;}

voidsetY(double

NewY){y=NewY;}

doublegetX()const{returnx;}

doublegetY()const{returny;}

};classthreeD:public

twoD{

private:

doublez;

public:

threeD(doublei,doublej,doublek):twoD(i,j){z=k;}

voidsetZ(double

NewZ){z=NewZ;}

doublegetZ(){returnz;}

};运行结果如下:

Coordinatesfor3dobjectis:

1,2,3继承的一般形式如下:

class派生类名:访问权限基类名

{

}访问权限是访问控制说明符,它可以是public、private或protected。基类的构造函数和析构函数不能被派生类继承.

派生类是与基类有一定联系的,基类是描述一个事物的一般特征,而派生类有比基类更丰富的属性和行为。比如正文中的例子,基类是一个二维点的模型,派生类从一个二维点模型中派生,添加了第三维向量和相应函数,生成了一个三维点模型。如果需要,派生类可以从多个基类继承,也就是多重继承,这将在后面章节中讲解。通过继承,派生类自动得到了除基类私有成员以外的其它所有数据成员和成员函数,在派生类中可以直接访问,从而实现了代码的复用。

派生类对象生成时,要调用构造函数进行初始化。编译器的调用过程是:

(1)先调用基类的构造函数,对派生类中的基类数据进行初始化,

(2)接下来调用成员对象的构造函数,如果派生类拥有成员对象的话;(3)然后再调用派生类自己的构造函数,对派生类的数据进行初始化工作。需要注意的是:析构函数次序与构造函数调用次序正好相反。基类数据的初始化要通过基类的构造函数,而且,它要在派生类数据之前初始化,所以基类构造函数在派生类构造函数的初始化表中调用:

派生类名(参数表1):基类名(参数表2)其中“参数表1”是派生类构造函数的参数,“参数表2”是基类构造函数的参数。通常情况下,参数表2中的参数是参数表1的一部分。也就是说,用户应该提供给派生类所有需要的参数,包括派生类和基类的。如果派生类构造函数没有显式调用基类的构造函数,编译器也会先调用基类的缺省参数的构造函数,对基类数据进行初始化。如果派生类自己也没有显式定义构造函数,那么编译器会为派生类定义一个缺省的构造函数,在生成派生类对象时,仍然先调用基类的构造函数。所以,派生类没有定义构造函数的话,必须保证基类有缺省参数的构造函数。下面的情况是错误的:程序段9-3

classA

{

public:

A(int

n):a(n){}//A就不存在缺省构造函数

inta;//因为已经定义了构造函数

};

classB:publicA

{

public://编译器会为类B生成缺省的构造函数

intb;

};

当程序中定义了一个类B的对象:Bb;编译器会报告“noappropriatedefaultconstructoravailable”这样的错误,所以必须为B编写构造函数函数.以变完成初始化,例如:

B(int

i,intj):A(i){b=j;}

如果类中有另一个类的对象作为它的数据成员,该对象称为内嵌的对象。内嵌的对象应该先构造,所以,对它的初始化也应在初始化表中。程序段9-4

classB

{

public:

B(intI,int

j):a(i),b(j){}

Aa;

intb;

}

调用内嵌对象的构造函数与调用基类构造函数不同的是:调用基类构造函数用的是基类构造函数名,而这里使用的是内嵌的对象名。

9.1.2访问控制一、类内访问说明符

我们定义twoD和threeD类的时候,变量成员和成员函数前面有访问说明符:public、private或proteced,它们控制变量成员和成员函数在类内和类外如何访问。所谓类内访问是指用类的成员函数进行访问,而类外访问是指用对象或指向对象的指针进行访问。

当一个类的成员定义为public,就能够在类外访问,包括它的派生类。

当一个成员定义为private,它仅能在类内访问,不能被它的派生类访问。

当一个成员定义为proteced,它仅能在类内访问,但是能被它的派生类访问。

当一个成员没有指定访问说明符时,默认为private。

二、继承访问说明符

在定义派生类时,访问说明符也能出现在基类的前面,它控制基类的变量成员和成员函数在派生类中的访问方法。当访问说明符为public时,称为公有继承。同样地,当问说明符为protected时,称为保护继承,而当问说明符为private时,称为私有继承。

公有继承时,基类的公有成员,变为派生类的公有成员,基类的保护成员,变为派生类的保护成员。

保护继承时,基类的公有和保护成员,均变为派生类的保护成员。

私有继承时,基类的公有和保护成员,均变为派生类的私有成员。

虽然C++语法上,能够定义公有继承、保护继承和私有继承。但是,我们通常使用公有继承,保护继承与私有继承的用处不大。

这里要明确几点:

1.基类的私有成员不能被派生类访问。

2.公有继承时,派生类原样继承基类的公有成员和保护成员,它们的属性在派生类中没有改变。

3.保护继承时,基类的公有成员和保护成员,变成派生类的保护成员,它只能在派生类内被访问,但能够被该派生类的派生类访问。

4.私有继承时,基类的公有成员和保护成员,变成派生类的私有成员。

在一般情况下,我们都是用public继承,这样使得基类的接口也是派生类的接口。当我们想隐藏这个类的基类部分的功能时,我们可以用private继承。当私有继承时,基类的所有公有和保护成员都变成派生类的私有成员,如果希望它们中任何一个成为public,只要用派生类的public选项声明它们的名字即可。

例9-3

classA{

private:intx;

protected:

doublew;

public:

A(intv,doublez):x(v),w(z){}

~A(){}

doublef();

doubleg();

};classB:publicA{

private:

doubleu;

public:

B(intx,doublez,doubley):A(x,z),u(y){}

~B(){}

doubleg();//重新定义A的成员函数g

doubleh();//增加一个新的接口函数h

};例9-4

#include“iostream.h”

classA{

inta;//没有指定访问说明符,默认为private

public:

A(int

i,int

j):a(i),b(j){}

//类内访问,可以访问公有变

//量和私有变量

intb;

int

geta();

};voidmain(){

Aa(1,2);

cout<<a.a;//错误,不能访问类的私有成员

cout<<a.b;//正确

cout<<a.geta();

}例9-5

#include"iostream.h"

classA{

public:

voida(){cout<<"a"<<endl;}

voidb(){cout<<"b"<<endl;}

};

classB:privateA{

public:A::a;

};voidmain()

{

Bb;

b.a();//正确,函数a是类b的公有成员

b.b();//错误,函数b是类b的私有成员

}9.1.3函数的隐藏与覆盖

我们已经知道了函数的重载,

重载的函数名字相同,但它们的参数个数和类型不同。函数的隐藏和覆盖,与函数的重载不同,它们只是在继承的情况下才存在。

如果在派生类中定义了一个与基类同名的函数,也就是说为基类的成员函数提供了一个新的定义,有两种情况:

(1)在派生类中的定义与在基类中的定义有完全相同的信号(即参数个数与类型均相同)和返回类型,

(2)在派生类中,改变了成员函数参数表与返回类型。

在派生类重定义了基类的成员函数,通常会自动隐藏基类的同名成员函数。

例9-6

//在派生类中,隐藏重载的函数名

#include<iostream.h>

classBase{

public:

intf()const{

cout<<"Base::f()\n";

return1;

}

intf(char*)const{return1;}

voidg(){}

};classDerived1:publicBase{

public:

voidg()const{}//重定义了基类Base的成员函数g

};voidmain(){

Derived1d1;//在派生类Derived1中,

intx=d1.f();//基类Base重载的

d1.f(s);//函数f可以被使用}#include<iostream.h>

classBase{

public:

intf()const{

cout<<"Base::f()\n";

return1;

}

intf(char*)const{return1;}

voidg(){}

};classDerived2:publicBase{

public://函数f被重定义

intf()const{

cout<<"Derived2::f()\n";

return2;}

};voidmain(){

Derived2d2;

x=d2.f();

//!d2.f(s);//基类带char*参数的成员函数f被隐藏

}重定义了基类Base两个重载的成员函数f中的一个,导致第二个重载的函数也不能用。#include<iostream.h>

classBase{

public:

intf()const{

cout<<"Base::f()\n";

return1;

}

intf(char*)const{return1;}

voidg(){}

};classDerived3:publicBase{

public://改变函数f的返回类型

voidf()const{cout<<"Derived3::f()\n";}

};voidmain(){

Derived3d3;

//!x=d3.f();//基类带返回int类型的成员函数f被隐藏

}改变了返回类型,基类Base的两个重载的成员函数f都不能用#include<iostream.h>

classBase{

public:

intf()const{

cout<<"Base::f()\n";

return1;

}

intf(char*)const{return1;}

voidg(){}

};classDerived4:publicBase{

public://改变函数f的参数表

int

f(int)const{

cout<<"Derived4::f()\n";

return4;

}

};voidmain(){

Derived4d4;

//!x=d4.f();//基类的成员函数f被隐藏

x=d4.f(1);

}改变参数表也隐藏基类Base的两个重载的成员函数f。关于函数覆盖应该注意以下两点:

(1)在派生类中覆盖了基类的成员函数后,基类的成员函数被隐藏。但是,基类的成员函数如果需要的话,仍然可以显式调用。例如,派生类B中成员函数g覆盖了基类成员函数g,但基类成员函数g仍然可以通过域运算符显式调用:

doubleB::g()

{

doubleres1=A::g();//调用A中的函数g

}

(2)在类外,基类的被覆盖的成员函数,也可以通过域运算符访问。例如:

Derived3d3;

intx=d3.Base::f();//调用基类BASE中的函数f9.1.4多继承

C++支持多继承,即一个派生类继承多个基类:当继承基类时,派生类对象就包含了每一个基类对象的成员。假定以类X和类Y为基类派生出类Z,类Z就会同时包含类X的和类Y的数据成员,如图派生类构造函数应该在成员初始化表中调用所有基类的构造函数:Z::Z(intn,doubled):X(n),Y(d)

{

}多继承类成员的引用比单继承复杂,例如:classX{

public://…

voidH(intpart);

};

classY{

public://…

voidH(intpart);

};classZ:publicX,publicY{

public://…

voidH(intpart);

};

voidZ::H(intpart){

X::H(part);//必须指明调用哪个基类的函数

Y::H(part);

}如果类X和类Y都是从相同的基类A派生的,那么从类层次上看,就成了一个菱形的结构,如图一旦出现菱形的情况,不仅增加了存储空间,更严重的是产生了二义性。我们建议尽量不使用多重继承。

9.1.6基类和派生类之间的转换

假定我们定义了两个对象,一个是基类对象,另一个是派生类对象:

twoDtwo(15.0,15.0);

threeDthree(20.0,20.0,30.0);

派生类对象three同时也是基类对象two,派生类对象允许给基类对象赋值:

two=three;

对象two比对象three少一个数据成员z,这个赋值的效果是对象two收到对象three的x和y坐标值。

反过来,用基类对象对派生类对象赋值是不行的。例如:

three=two;

由于对象two没有z数据成员,使得对象three的数据成员z的值是不定的。所以,C++编译器不允许基类对象对派生类对象赋值。

派生类对象的地址也可以赋给指向基类对象的指针,例如:

threeDthree(20.0,20.0,30.0);

twoD*pTwo;

下面的赋值是合法的:

pTwo=&three;

由于指针pTwo是指向twoD对象,在这个赋值过程中,派生类类型被隐式转换成twoD。所以,pTwo只能调用twoD的成员函数,需要注意的是:我们不能把一个指向派生类对象的指针指向基类的对象。

引用与指针类似。例如,如果一个函数A定义有twoD引用参数,用函数B调该函数A时,实参可以是从twoD派生的类的对象。在函数A内,可以访问twoD类的成员。

通过指针调用成员函数时,到底是调用基类的成员函数,还是调用派生类的成员函数,是由指针的类型决定的,而不是由对象决定的。

比如,上面的对象three,编译器只把指针pTwo当成一个twoD类型指针处理,也就是说,不知道它现在指向的是一个threeD类型的对象。当调用getSum()成员函数时:

pTwo->getSum();

编译器会调用基类版本:

“twoD::getSum()”,而不是调用:

“threeD::getSum()”。

如果想解决这个问题,需要利用下一节虚函数的知识。

9.2虚函数与动态联编9.2.1静态联编与动态联编

在C/C++语言中,函数调用在程序运行之前就已经和函数体(函数的首地址)联系起来。编译器把函数体翻译成机器代码,并记录了函数的首地址。在对函数调用的源程序段进行编译的时候,编译器知道这个函数名的首地址在那里,然后将这个首地址替换函数名,一并翻译成机器码。这种编译方法称为早期或静态联编。

当基类指针指向派生类对象时,我们能不能通过该指针来调用派生类的成员函数呢?从这种编译方法来看,是不可能的。因为编译器只会寻找基类的成员函数。

利用C++的多态特性,基类指针是调用基类的成员函数,还是调用派生类的成员函数,可以不由指针的类型决定的,而是由指针指向的对象的类型决定的。多态也称为动态联编或迟后联编。因为到底调用哪一个函数,在编译时不能确定,而要推迟到运行时确定。也就是说,要等到程序运行时,确定了指针所指向的对象的类型时,才能够确定。在C++中,动态联编是通过虚函数来实现的。

9.2.2虚函数的定义与使用

虚函数的定义很简单,只要在成员函数原型前加一个关键字virtual即可。如果一个基类的成员函数定义为虚函数,那么,它在所有派生类中也保持为虚函数,即使在派生类中省略了virtual关键字。

需要注意的是:要达到动态联编的效果,基类和派生类的对应函数不仅名字相同,而且返回类型、参数个数和类型也必须相同。

例如:classvehicle

{

virtualvoidmessage(void){cout<<"Vehiclemessage\n";}

};classcar:publicvehicle

{

int

passenger_load;

public:

(virtual)voidmessage(void){cout<<"Carmessage\n";}

};carmycar;

vehicle*v=&mycar;

v->message()//调用类car的成员函数message()

另外,如果car::message是私有的,通过动态联编也能访问。9.2.3多态的实现

virtual将一个成员函数说明为虚函数,对于编译器来讲,它的作用是告诉编译器,这个类含有虚函数,对于这个函数不使用静态联编,而是使用动态联编机制。编译器就会按照动态联编的方案进行一系列的工作。

对于每个包含虚函数的类,编译器都为其创建一个表(称之为VTABLE表)。在VTABLE表中放置的是每个类自己的虚函数地址,在每个包含虚函数的类中放置了一个指针(VPTR),指向VTABLE表。通过基类指针调用虚函数时,编译器会在函数调用的地方插入一段特定的代码。这段代码的作用就是得到VPTR,找到VTABLE,并在VTABLE表中找到相应的虚函数地址,然后进行调用。

有虚函数的对象的内部组织

9.3抽象类有的时候我们的基类只是起到接口的作用,没有必要生成该类的对象,该类本身也不需要对函数进行实现,这样的基类称之为抽象类。在C++中,有一种虚函数称之为纯虚函数,含有纯虚函数的类,就是抽象类。C++编译器不允许用抽象类创造对象,它只能被其它类继承。要定义抽象类,就必须定义纯虚函数,它实际上是起到一个接口的作用。由抽象类派生的类,必须对抽象类定义的纯虚函数进行实现。仅定义了函数的信号,而没有函数实现的虚函数称之为纯虚函数。定义纯虚函数的方法是在虚函数参数表右边的括号后加一个“=0”的后缀。例如:

classvehicle//抽象类

{

virtualvoidmessage(void)=0;//纯虚函数

};

9.4虚析构函数在C++中,构造函数不能定义为虚函数,析构函数可以定义为虚函数。因为构造函数的功能是构造并初始化这个对象,它的一项功能就是设置VPTR,所以构造函数不能定义成虚函数,动态联编机制还没有建立起来,是不可能进行虚函数调用的。

我们常常将析构函数定义为虚函数。

例9-8

#include"iost

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论