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

下载本文档

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

文档简介

1、第4章 继承性与多态性4.1 继承性与派生4.1.1 继承的概念所谓继承(inheritance)就是利用已有的数据类型定义出新的数据类型。在继承关系中,被继承的类称为基类(base class)或父类,而通过继承关系定义出来的新类则被称为派生类(derived class)或子类。派生类既可以对基类的性质进行扩展又可以对基类进行限制,从而得到更加灵活、适用的可重用模块,大大缩短了程序的开发时间。一个派生类既可以从一个基类派生也可以从多个基类派生。从一个基类派生称为单继承;从多个基类派生称为多重继承。1单继承单继承的定义形式如下:class 派生类名:访问方式 基类名派生类中的新成员;其中,派

2、生类名是新定义的类名。基类名必须是程序中已有的一个类。在单继承中,每个类可以有多个派生类,但是每个派生类只能有一个基类。例如:class A.;class B:public A.;2多重继承所谓多重继承是指派生类从多个基类中派生而来的。定义多重继承类的方式如下:class 派生类名:访问方式1 基类名1,访问方式2 基类名2派生类中的新成员;例如:class A.;class B.;class C:public A,public B.;从定义格式上来看,多重继承与单继承的区别主要是多重继承的基类多于一个。3访问方式不管在单继承还是在多重继承的定义格式中,访问方式,即继承方式,可以为public

3、、private或protected,如果省略,则默认为private方式。访问方式为public方式时,这种继承称为公有继承;访问方式为private方式时,这种继承称为私有继承;访问方式为protected方式时,这种继承称为保护继承。4.1.2 派生类的生成过程在给出了派生类的定义和相应成员函数的实现代码后,整个派生类的定义就算完成了,这是就可以利用该类定义相应的对象处理实际问题了。由于派生类是在基类的基础上经过继承而产生的,所以搞清派生类中到底有哪些成员对于更好的使用派生类是很重要的。事实上,派生新类经历了三个步骤:1吸收基类成员派生类继承吸收了基类的全部数据成员以及除了构造函数、析构

4、函数之外的全部函数成员。也就是说,基类中的构造函数和析构函数不能继承到派生类中的。2改造基类成员对继承到派生类中基类成员的改造包括两个方面:一是基类成员的访问方式问题,这由派生类定义时的访问方式来控制;二是对基类数据成员或成员函数的覆盖,也就是在派生类中定义了与基类中同名的数据成员或函数成员,由于作用域不同,于是发生同名覆盖,基类中的成员就被替换成派生类中的同名成员。3添加新成员在派生类中,除了从基类中继承过来的成员外,还可以根据需要在派生类中添加新的数据成员和成员函数,以此实现必要的新功能。可以看出,在派生类中可以添加新成员的机制是继承和派生机制的核心,保证了派生类在功能上比基类有所发展。4

5、.1.3 继承方式对基类成员的访问控制前面已经分析,派生类继承和吸收了基类的全部数据成员和除了构造函数、析构函数之外的全部函数成员,但这些成员在派生类中的访问属性是可以调整的,这是由派生类定义格式中的继承方式来决定的,也就是继承方式控制了基类中具有不同访问属性的成员在派生类中的访问属性。由于继承方式可以有public、private和protected三种,不同的继承方式会导致原来具有不同访问属性的基类成员在派生类中的访问属性也有所不同。这种访问包括两个方面:一是派生类中新增成员对从基类继承来的成员的访问;二是派生类的外部通过派生类的对象从基类继承来的成员的访问。1公有继承当类的继承方式为公有

6、继承时,基类中public和protected成员的访问属性在派生类中不变,而基类private成员不可访问。也就是说,基类的public和protected成员在公有继承方式下分别继承为派生类的public和protected成员,派生类中的其他成员可以直接访问它们,在派生类的外部只能通过派生类的对象访问从基类继承来的public成员。而无论是派生类的成员还是派生类的对象都无法访问从基类继承来的private成员。2私有继承当类的继承方式为私有继承时,基类中的public和protected成员都以private成员出现在派生类中,而基类private成员不可访问。也就是说,基类的publi

7、c和protected成员在私有继承方式下被继承为派生类的private成员,派生类中的其他成员可以直接访问它们,但在派生类的外部无法通过派生类的对象访问它们。而无论是派生类的成员还是派生类的对象都无法访问从基类继承来的private成员。可以看出,经过私有继承后,所有基类的成员都成为派生类的私有成员,如果进一步派生的话,基类的成员就无法在新的派生类中被访问。因此,经过私有继承后,基类的成员再也无法在以后的派生类中发挥作用,实际是相当于中止了基类功能的继续派生。3保护方式当类的继承方式为保护继承时,基类中的public和protected成员都以protected成员出现在派生类中,而基类pr

8、ivate成员不可访问。也就是说,基类的public和protected成员在保护继承方式下被继承为派生类的protected成员,派生类中的其他成员可以直接访问它们,但在派生类的外部无法通过派生类的对象访问它们。而无论是派生类的成员还是派生类的对象都无法访问从基类继承来的private成员。继承访问方式基类成员特性派生类成员特性公有publicpublicprotectedprivatepublicprotected不可访问私有privatepublicprotectedprivateprivateprivate不可访问保护protectedpublicprotectedprivatepro

9、tectedprotected不可访问示例:#include iostream.hclass Apublic:void f1();protected:int j1;private:int i1;class Bpublic:void f2();void f1();protected:int j2;int j1;private:int i2;class B:public Apublic:void f2();protected:int j2;private:int i2;class C:public Bpublic:void f3();class C:public Bpublic:void f3()

10、; void f1(); void f2();protected: int j1,j2;回答如下问题: B的成员函数f2()能否访问A中的f1()、i1、j1等成员? B的对象b1能否访问A中的f1()、i1、j1等成员? C的成员函数f3()能否访问B中的f2()、j2等成员?能否访问A中的f1()、i1、j1等成员? C的对象能否访问B中的f2()、i2、j2等成员?能否访问A中的f1()、i1、j1等成员? 从上述过程可得出对公有访问继承什么样的结论?解答: B的成员函数f2()能访问A中的f1()、j1,不能访问i1。 B的对象b1能访问A中的f1(),不能访问i1、j1。 C的成员函

11、数f3()能访问B中的f2()、j2,不能访问i2。能访问A中的f1()、j1,不能访问i1。 C的对象能否访问B中的f2()和A中的f1(),其他均不可访问。 在公有继承时,派生类的成员函数可以访问基类中的公有成员和保护成员;派生类的对象仅可访问基类中的公有成员。示例:#include iostream.hclass Apublic:void f(int i)coutiendl;void g()coutgendl;class B:Apublic:void h()couthn;A:f;void main()B d1;d1.f(6);/d1.g();d1.h();运行结果:6h示例:#inclu

12、de iostream.h#include string.hclass Apublic:A(const char *nm)strcpy(name,nm);/private:protected:char name80;class B:public Apublic:B(const char *nm):A(nm)void PrintName()const;void B:PrintName()constcoutname:nameendl;void main()B b1(Wang Li);b1.PrintName();4.1.4 派生类的构造函数和析构函数继承和派生的机制可以使派生类继承基类的成员,从而

13、实现了原有代码的重用,但是,由于基类的构造函数和析构函数不能继承,那么在派生类中,如果对派生类新增的成员进行初始化,就必须在派生类中根据需要加入新的构造函数,如果对从基类继承下来的成员进行初始化,还必须由基类的构造函数来完成,所以需要在派生类中的构造函数,一方面负责调用基类的构造函数对基类成员进行初始化,另一方面还要负责对基类的构造函数所需要的参数进行必要的设置。1单继承方式下派生类构造函数的定义在单继承方式下,派生类的构造函数的定义格式如下:派生类名:派生类构造函数名(形参表):基类构造函数名(参数表),子对象名(参数表)./派生类构造函数的函数体;在此定义格式中,派生类构造函数名后面括号内

14、的参数表中包括参数的类型和参数名,而基类构造函数名后面括号内的参数表中只有参数名而没有参数类型,并且这些参数必须是来源于派生类构造函数名后面括号内的参数。示例:#include iostream.hclass Aprivate:int a;public:A() a=0;coutAs default constructor called.endl; A(int i) a=i; coutAs constructor called.endl; A() coutAs destructor called.endl; void Print() const couta,; int Geta() return

15、 a; ;class B:public Aprivate:int b;A aa;public:B() b=0;coutBs default constructor called.endl; B(int i,int j,int k);B() coutBs destructor called.endl; void Print();B:B(int i,int j,int k):A(i),aa(j)b=k;coutBs constructor called.endl;void B:Print()A:Print();coutb,aa.Geta()endl;void main()B bb2;bb0=B(1

16、,2,5);bb1=B(3,4,7);for(int i=0;i2;i+)bbi.Print();运行结果:As default constructor called.As default constructor called.Bs default constructor called.As default constructor called.As default constructor called.Bs default constructor called.As constructor called.As constructor called.Bs constructor called.

17、Bs destructor called.As destructor called.As destructor called.As constructor called.As constructor called.Bs constructor called.Bs destructor called.As destructor called.As destructor called.1,5,23,7,4Bs destructor called.As destructor called.As destructor called.Bs destructor called.As destructor

18、called.As destructor called.说明: 该程序中先定义了类A,接着定义类B,它是类A的派生类。继承方式为公有继承。 派生类B的构造函数格式如下:B:B(int i,int j,int k):A(i),aa(j)b=k;coutBs constructor called.endl;其中,B是派生类构造函数名,它的参数表中有3个参数:参数i用来初始化基类的数据成员;参数j用来初始化类B的子对象aa;参数k用来初始化类B中数据成员b。冒号后面的是成员初始化列表,如果该表中有多项,它们用逗号分隔。该成员初始化列表的顺序如下:先是基类构造函数;再是派生类中子对象类的构造函数;最后

19、是派生类的构造函数。故亦可写成如下形式:B:B(int i,int j,int k):A(i),aa(j),b(k)/b=k;coutBs constructor called.endl; 运行结果分析:先创建两个对象元素的对象数组。调用两次类B的缺省构造函数,每调用类B的缺省构造函数时,先调用两次类A的缺省构造函数和一次类B的构造函数。于是出现了输出结果的前6行。接着,程序中出现两个赋值语句,即给两个已定义的对象(对象数组元素)赋值。系统要建立一个匿名对象,通过构造函数对它初始化,并将其值赋给已定义的左值对象,再调用析构函数将匿名对象删除。因此输出结果中出现两个调用派生类B的构造函数和析构函

20、数的12行信息。输出两个类B对象的数据成员值,占有2行信息。最后,程序结束前由系统自动调用派生类B的析构函数,显示输出了删除两个对象的6行信息。2多重继承方式下的派生类构造函数的定义在多重继承方式下,派生类的构造函数必须同时负责所有基类构造函数的调用,对于派生类构造函数的参数个数必须同时满足多个基类初始化的需要。所以,在多重继承方式下,派生类的构造函数的定义格式如下:派生类名:派生类构造函数名(参数表):基类名1(参数表1)基类名2(参数表2)子对象名(参数表)/派生类构造函数的函数体其中,第1个参数表中的参数包含了其后的各个参数表中的参数。3派生类构造函数的执行次序派生类构造函数执行的一般次

21、序为: 调用基类构造函数,调用顺序按照它们被继承时说明的顺序(从左到右),而不是按派生类构造函数在初始化表中的次序; 调用子对象的构造函数(如果在派生类中存在子对象的话),调用顺序按照它们在类中说明的顺序; 执行派生类构造函数的函数体。当派生类的对象被删除时,派生类的析构函数被执行。由于基类的析构函数不能被继承,因此在执行派生类的析构函数时,基类的析构函数也将被调用。而执行顺序是先执行派生类的析构函数,再执行基类的析构函数,其顺序与执行构造函数是的顺序正好相反。示例:#include iostream.hclass B1private:int b1;public:B1(int i)b1=i;c

22、outconstructor B1.iendl;void print() coutb1endl; ;class B2private:int b2;public:B2(int i)b2=i;coutconstructor B2.iendl;void print() coutb2endl; ;class B3private:int b3;public:B3(int i)b3=i;coutconstructor B3.iendl;int getb3() return b3; ;class A:public B2,public B1private:int a;B3 bb;public:A(int i,

23、int j,int k,int l):B1(i),B2(j),bb(k),a(l)/a=l;coutconstructor A.lendl;void print()B1:print();B2:print();couta,bb.getb3()endl;int main(void)A aa(1,2,3,4);aa.print();return 0;运行结果:constructor B2.2constructor B1.1constructor B3.3constructor A.4124,3Press any key to continue说明: 派生类A的构造函数定义如下:A(int i,int

24、 j,int k,int l):B1(i),B2(j),bb(k)a=l;coutconstructor A.lendl;该函数的总参数表中参数有4个,分别是基类B1,基类B2,子对象bb以及派生类A构造函数的参数,亦可写成如下形式:A(int i,int j,int k,int l):B1(i),B2(j),bb(k),a(l)coutconstructor A.lendl; 派生类构造函数的执行顺序:在构造函数的成员初始化表中,两个基类顺序是B1在前,B2在后。而定义派生类A时的两个基类顺序是B2在前,B1在后。输出结果中,可以看出,先执行B2的构造函数,后执行B1的构造函数。因此,执行基

25、类构造函数的顺序取决于定义派生类时基类的顺序。可见,派生类构造函数的成员初始化列表中各项顺序可以任意排列。 作用域运算符:在该程序中用于解决作用域冲突问题很明显。在派生类A中的print函数中,分别使用了B1:print();和B2:print();。4.2 派生中成员的标识与访问在多重继承的情况下,派生类具有两个以上的直接基类,而这些直接基类的一部分或全部又是从另一个共同基类派生而来的,这些直接基类中从上一级基类继承来的成员拥有相同的名称,在派生类的对象中,这些同名成员在内存中同时拥有多个拷贝,如何进行分辨呢?有两种方法,一是使用作用域运算符唯一标帜并分别访问它们;二是将直接基类的共同基类设

26、置为虚基类。4.2.1 使用作用域运算符这种方法就是在需要访问的成员名前加上直接基类名和作用域运算符“:”。其格式是:直接基类名:数据成员名直接基类名:成员函数名(参数表)示例:#include iostream.hclass Apublic:int a;void fa() couta=aendl; ;class B:public Apublic:int b;class C:public Apublic:int c;class D:public B,public Cpublic:int d;void fd() coutd=dendl; ;void main()D dd;dd.d=1;dd.fd

27、();dd.B:a=2;dd.B:fa();dd.C:a=3;dd.C:fa();运行结果:d=1a=2a=3Press any key to continue说明:该示例中的继承关系和D对象结构分别如下:Aint a;void fa();Bint b;Cint c;Dint d;void fd();Dint B:a;int C:a;int B:b;int C:c;int d;void B:fa();void C:fa();void fd();4.2.2 虚基类该方法就是将直接基类的共同基类设置为虚基类,即在基类的访问方式前加上关键字“virtual“,声明虚基类的格式如下:class 派生类

28、名:virtual 访问方式 基类名 /声明派生类成员;虚基类虽然被一个派生类间接地多次继承,但派生类却只继承一份该基类的成员,这样就避免了在派生类中访问这些成员时的二义性。示例:#include iostream.hclass Apublic:int a;void fa() couta=aendl; ;class B:virtual public Apublic:int b;class C:virtual public Apublic:int c;class D:public B,public Cpublic:int d;void fd() coutd=da; int geta2() ret

29、urn a; ;void main()A aa(8);A *p=&aa;couta=aendl;couta=aa.geta1()endl;couta=aa.geta2()endl;运行结果:a=8a=8a=8Press any key to continue2引入派生类后的对象指针引入派生类后,由于派生类是由基类派生出来的,派生类和基类息息相关。因此,指向派生类和基类的指针也是相关的。在引入派生概念后,任何被说明为指向基类对象的指针,都可以指向它的公有派生类。示例:#include iostream.h#include string.hclass Aprivate:char *name;int

30、 length;public:A(char *s)length=strlen(s);name=new charlength+1;strcpy(name,s);void show() coutnameendl; ;class B:public Aprivate:int age;public:B(char *str,int age):A(str)B:age=age;void show()A:show();coutthe age is:ageshow();p1=&s2;p1-show();p2=&s2;p2-show();运行结果:SmithJeanJeanthe age is:20Press an

31、y key to continue4.3 多态性和虚函数封装性是基础,继承性是关键,多态性是补充。所谓多态性是指发出同样的消息被不同类型的对象接收时将导致不同的行为。这里所说的消息是指对类的成员函数的调用,不同的行为是指不同的实现。函数重载和运算符重载是简单的一类多态性。建立是虚函数的概念和方法基础之上的是较复杂的一类多态性。函数重载已在前面讨论过,不再重复。4.3.1 运算符重载运算符重载是指可以赋予已有的运算符多重的含义。C+中通过重新定义运算符,使它能够用于特定类的对象执行特定的功能。1运算符重载的几个问题 哪些运算符可以用作重载几乎所有的C+运算符都可以用作重载。但有几个运算符不允许重

32、载:成员选择符. 成员指针选择符.* 域作用符: 三目运算符?: 运算符重载后,优先级和结合性的变化是怎样的用户重新定义运算符,不改变原运算符的优先级和结合性。也不改变运算符的语法结构,即单目运算符只能重载为单目运算符,双目运算符只以重载为双目运算符。 编译程序如何选用哪一个运算符函数运算符重载实际是一个函数,所以运算符的重载实际上是函数的重载。编译程序对运算符重载的选择,遵循着函数重载的选择原则。当遇到不明显的运算符时,编译程序将去寻找参数匹配的运算符函数。 重载运算符有哪些限制a不可以臆造新的运算符。必须把重载运算符限制在C+语言中已有的运算符范围。b重载运算符坚持四个不改变:运算符操作数

33、个数、优先级、结合性、语法结构。 运算符重载时必须遵循的原则a重载运算符含义必须清楚。b不能有二义性。2运算符重载函数的格式单目操作符重载函数的定义格式和调用格式分别为: operator () 或operator ()双目操作符重载函数的定义格式和调用格式分别为: operator (,) 或operator (,)3运算符重载函数的两种形式下面的两种形式的重载都可以访问类中的私有成员。 重载为类的成员函数#include iostream.hclass complexprivate:double real,imag;public:complex() real=imag=0; complex

34、(double r,double i)real=r;imag=i;complex operator +(const complex &c);complex operator -(const complex &c);complex operator *(const complex &c);complex operator /(const complex &c);friend void print(const complex &c);inline complex complex:operator +(const complex &c)return complex(real+c.real,imag+

35、c.imag);inline complex complex:operator -(const complex &c)return complex(real-c.real,imag-c.imag);inline complex complex:operator *(const complex &c)return complex(real*c.real-imag*c.imag,real*c.imag+imag*c.real);inline complex complex:operator /(const complex &c)return complex(real*c.real+imag*c.i

36、mag)/(c.real*c.real+c.imag*c.imag),(imag*c.real-real*c.imag)/(c.real*c.real+c.imag*c.imag);void print(const complex &c)if(c.imag0)coutc.realc.imagi;elsecoutc.real+c.imagi;void main()complex c1(2.0,3.0),c2(4.0,-2.0),c3;c3=c1+c2;coutendlc1+c2 = ;print(c3);c3=c1-c2;coutendlc1-c2 = ;print(c3);c3=c1*c2;c

37、outendlc1*c2 = ;print(c3);c3=c1/c2;coutendlc1/c2 = ;print(c3);c3=(c1+c2)*(c1-c2)*c2/c1;coutendl(c1+c2)*(c1-c2)*c2/c1 = ;print(c3);coutendl;运行结果:c1+c2 = 6+1ic1-c2 = -2+5ic1*c2 = 14+8ic1/c2 = 0.1+0.8i(c1+c2)*(c1-c2)*c2/c1 = 31.8462+25.2308iPress any key to continue说明:该程序中定义了一个complex类,该类中定义了4个成员函数作为运算

38、符重载函数。将运算符重载函数说明为类的成员函数的格式如下: operator ()其中的operator是运算符重载的关键字。程序中出现的表达式:c1+c2,编译程序将解释为:c1.operator+(c2),其中,c1和c2是complex类的对象。operator +()是运算符+的重载函数。 重载为友元函数示例:#include iostream.hclass complexprivate:double real,imag;public:complex() real=imag=0; complex(double r,double i)real=r;imag=i;friend comple

39、x operator +(const complex &c1,const complex &c2);friend complex operator -(const complex &c1,const complex &c2);friend complex operator *(const complex &c1,const complex &c2);friend complex operator /(const complex &c1,const complex &c2);friend void print(const complex &c);complex operator +(const

40、complex &c1,const complex &c2)return complex(c1.real+c2.real,c1.imag+c2.imag);complex operator -(const complex &c1,const complex &c2)return complex(c1.real-c2.real,c1.imag-c2.imag);complex operator *(const complex &c1,const complex &c2)return complex(c1.real*c2.real-c1.imag*c2.imag,c1.real*c2.imag+c

41、1.imag*c2.real);complex operator /(const complex &c1,const complex &c2)return complex(c1.real*c2.real+c1.imag*c2.imag)/ (c2.real*c2.real+c2.imag*c2.imag), (c1.imag*c2.real-c1.real*c2.imag)/ (c2.real*c2.real+c2.imag*c2.imag);void print(const complex &c)if(c.imag0)coutc.realc.imagi;elsecoutc.real+c.im

42、agi;void main()complex c1(2.0,3.0),c2(4.0,-2.0),c3;c3=c1+c2;coutendlc1+c2 = ;print(c3);c3=c1-c2;coutendlc1-c2 = ;print(c3);c3=c1*c2;coutendlc1*c2 = ;print(c3);c3=c1/c2;coutendlc1/c2 = ;print(c3);c3=(c1+c2)*(c1-c2)*c2/c1;coutendl(c1+c2)*(c1-c2)*c2/c1 = ;print(c3);coutendl;运行结果:c1+c2 = 6+1ic1-c2 = -2+

43、5ic1*c2 = 14+8ic1/c2 = 0.1+0.8i(c1+c2)*(c1-c2)*c2/c1 = 31.8462+25.2308iPress any key to continue 两种重载形式的比较一般而言,单目运算符最好重载为成员函数,双目运算符最好重载为友元函数。4.3.2 虚函数在讨论虚函数之前,先讲一讲静态联编和动态联编。联编是指一个计算机程序自身彼此关联的过程。按联编所进行的阶段不同,可分为两种不同的联编方法:静态联编和动态联编。静态联编是指联编工作出现在编译连接阶段,又称之为早期联编或静态束定。动态联编是指联编工作出现在程序运行阶段,又称之为晚期联编或动态束定,静态联

44、编示例:#include iostream.hclass pointpublic:point(double i,double j) x=i; y=j; double area() const return 0.0; private:double x,y;class rectangle:public pointprivate:double w,h;public:rectangle(double i,double j,double k,double l);double area() const return w*h; ;rectangle:rectangle(double i,double j,double k,double l):point(i,j)w=k; h=l;void fun(point &s)couts.area()endl;void main()rectangle rec(3.0,5.2,15.0,25.0);fun(rec);运行结果:0输出结果表明:在fun()

温馨提示

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

最新文档

评论

0/150

提交评论