1、a1第五章第五章 继承与类的派生继承与类的派生a2 根据软件需求从软件所要模拟的现实世界中抽象出组成软件系统的对象类是面向对象程序设计的基础。面向对象的封装性使这些对象类的属性和行为细节得到了合理的保护和隐藏,并为类对象之间的通讯(方法调用)提供了安全方便的接口。在封装性的基础上,面向对象的继承性允许一个对象类包含另一个或几个对象类的属性和行为,并使它们成为自己的属性和行为,充分地反映了现实世界中对象类之间的层次结构,为程序的代码重用提供了方便、有效的实现机制。a3 在面向对象程序设计中,借助继承性的实现方法,允许在既有类的基础上定义新类。被定义的新类可以从一个或多个既有类中继承属性和行为,并
2、允许重新定义这些既有类中原有的属性和行为,还允许为新类增加新的属性和行为,从而形成了类的建造层次。既有类被称为基类或父类 新类被称为派生类、导出类或子类a4本章要点1 派生类的概念2 派生类的定义方法3 派生类成员的访问属性4 派生类的构造函数和析构函数5 对派生类成员访问属性的进一步讨论6 多继承7 继承在软件开发的重要意义a55.1 派生类的概念派生类的概念 继承是对象类之间的一种包含关系,这种包含关系是通过对象类的建造层次关系实现的。因此,具有继承关系的类之间必定拥有以下基本性质:类间的共享特性;类间的细微区别;类间的层次结构。a6例如:简单的汽车分类图 汽车运输汽车专用汽车客车货车消防
3、车洒水车a75.1.2 使用继承的必要性使用继承的必要性 试想如果组成一个系统的对象类均为互不包含的独立对象类,则将不可避免出现对象属性和行为的重复冗余,并且这种无层次关系的对象类既不符合现实世界的对象关系,也使对象类的定义、创建、使用和维护复杂化。继承为代码重用和建立类定义的层次结构提供方便有效的手段。例如在一个公司的管理软件设计中需要定义一个客户类 Customer 和雇员类 Employee:a8 class Customer private:char name15;/姓名int age;/年龄char sex8;/性别double income;/收入 public:void prin
4、t();/显示输出状态;a9 class Employment private:char name15;/姓名int age;/年龄char sex8;/性别char department20;/部门double salary;/工资 public:void print();/显示输出状态;a10比较两个类的定义,不难发现,两个类的数据成员和成员函数有许多相同之处。显然,如此定义两个类,造成的代码重复是不可避免的。如果将 Customer 和 Employee 类定义中的相同成员抽取出来,定义一个新类 Person:class Person private:char name15;/姓名int
5、 age;/年龄char sex8;/性别 public:void print();/显示输出状态;a11 Customer 和 Employee 都定义为 Person 的派生类,那些在 Person 中已经定义的共同数据成员在 Customer 和 Employee 中就不需要再定义了,只需要在各自的定义中增加自己的独有数据成员;而成员函数 print 也只需要在 Person 所定义的行为操作基础上重新定义自己的行为操作。class Customer:public Personprivate:double income;/收入public:void print();/显示输出状态;a12
6、 class Employee:public Person private:char department20;/部门double salary;/工资 public:void print();/显示输出状态;显然通过继承可以从基类 Person 派生出一组具有层次结构的新类,构成一个公司管理系统的主要对象类型。例如:a13 使用继承机制和方法设计和建造类定义的层次结构对于建立一个面向对象的软件系统是不可缺少的。返回Person EmployeeCustomerVendorSalariedHourlyPartner ClientPartTimeFullTimea145.2 派生类的定义方法派生
7、类的定义方法 定义派生类的一般形式:class 派生类名:派生方式 基类名派生类的新增加成员和基类成员的新定义 ;例如:基类 Person 和派生类的定义class Person private:char name15;/姓名int age;/年龄char sex8;/性别public:void print();/显示输出状态;a15class Customer:public Personprivate:double income;/新增加的数据成员“收入”public:void print();/重新定义基类的“显示输出状态”;从形式上比较,派生类定义与非派生类定义的差别仅在于定义首行中由“
8、:”引出的派生表达式。其中:派生方式:指明派生类继承基类成员的方式,方式 的种类有 public、private 和 protected。如果不指明方式名,则缺省指定派生方式为 private。基类名:指明派生类所继承的类。a16 在 Java 中类的派生又被称为类的扩展,这种扩展相当于 C+中的公有(public)派生,其一般形式如下:class 派生类名:extends 基类名 派生类的新增加成员和基类成员的新定义 a175.2.1 派生类的构成派生类的构成 派生类的构成可以有下图示意:派生类名基类的所有成员派生类的新增加成员a18 例如整数链表类 list 的定义:class list/
9、链表类名超前声明 class node /结点类定义 int val;node*next;public:friend class list;a19 class list /整数链表类定义 node*elems/链表头指针 public:list();list();bool insert(int);/在表头插入一个结点bool deletes(int);/从表中删除一个结点bool contains(int);/在表中查找一个结点;a20一个链表结构的整数集合可以看成是不含重复元素的特殊整数链表,因此整数集合类可以从整数链表类派生。整数集合类在继承了整数链表类的所有成员的基础上,需要新增加一个能
10、指示集合中元素个数的数据成员,同时还需要重新定义整数链表类的插入操作insert,禁止重复元素被插入。class set:public list int card;/集合中的元素个数 public:bool insert(int);/重新定义插入函数;返回a215.3 派生类成员的访问属性派生类成员的访问属性 类成员的访问属性是指类成员的被访问权限,这种权限随着作用域的变化而变化的,派生类成员的访问属性也分为类内访问属性和类外访问属性两种情况。1 类内访问属性 由于派生类的成员分为继承的基类成员和自身的新 增成员两种,这两种成员的类内访问属性是有所区 别的。基类成员的访问属性封装性所限定的类成
11、员类外访问权限确定了基类成员在派生类定义中被访问的限定原则:a22 私有成员:不允许被访问,与派生类从基类 的继承方式无关。公有成员:允许被访问,与派生类从基类的 继承方式无关。新增成员的访问属性所有的新增成员均允许被访问,与新增成员被设定的访问属性(公有或私有)无关。2 类外访问属性 类成员的类外访问是指在类对象定义域外访问对象 的类成员。因此,派生类成员在类定义中声明的访 问属性确定了派生类成员的类外访问属性:a23 基类成员的访问属性 私有成员:不允许被访问,与派生类从基类 的继承方式无关。公有成员:依据继承方式的不同,在基类中 被设定的公有属性会发生不同的变化。私有继承:基类的公有成员
12、变为派生类的私有成员,因此在类外不允许被访问。公有继承:基类的公有成员在派生类中仍保持公有属性,因此在类外允许被访问。新增加成员的访问属性类成员在类定义中被声明的访问属性确定了类成员的类外访问属性。a24 class baseprivatepublicclass derived2:public derived1privatepublicclass derived1:baseprivatepublicOKOKNONONONOderived2类内类外OKNOOKNONONOa25 包含了两个类:由 C+标准模板库提供的 string 类具有丰富的字符串操作功能。edit_string 类是从 st
13、ring 类派生的,它在继承 string 类功能的基础上增加了数据成员 光标和实现在光标处的进行插入、替换、删除等文本编辑功能。注意,派生类的成员名支配基类的成员名。下面的类图描述了 string 和 edit_string 之间的派生层次结构。a26返回stringedit_string-cursor:int+get_cursor_pos():int+move_cursor(in how_much:int)+add_at_cursor(in new_text:string):int+repl_at_cursor(in new_text:string):int+dele_at_cursor(
14、in how_much:int)a275.4 派生类的构造函数和析构函数派生类的构造函数和析构函数5.4.1 派生类的构造函数派生类的构造函数 与一般非派生类相同,系统会为派生类定义一个缺省(无参数、无显式初始化表、无数据成员初始化代码)构造函数用于完成派生类对象创建时的内存分配操作。但如果在派生类对象创建时需要实现以下两种操作或其中之一,就无法使用缺省构造函数完成。派生类对象的直接基类部分创建需要传递参数。派生类对象的新数据成员需要通过参数传递初值。为了满足上述对象创建操作的需要,就必须显式定义派生类构造函数。a28派生类构造函数声明和定义的一般形式:注意:构造函数名后面的参数表列中包含了初
15、始化表中创 建对象的基类部分、新增数据成员和在函数体中为 新数据成员赋初始值所需要的全部参数。构造函数名(参数表列);类名:构造函数名(参数表列):基类构造函数名(参数子表列),新数据成员名1(参数子表列),新数据成员名n(参数子表列)其他初始化代码 a29 初始化表中创建对象的基类部分的表达式必须使用 基类构造函数名调用基类构造函数,而创建数据成 员表达式必须使用数据成员名调用数据成员类的构 造函数。派生类构造函数的执行顺序:基类构造函数对象成员1类构造函数派生类构造函数定义体对象成员n类构造函数a305.4.2 派生类的析构函数派生类的析构函数 与一般非派生类相同,系统会为派生类定义一个缺
16、省(无数据成员的清理代码)析构函数用于完成派生类对象撤消时的内存回收操作。但如果在派生类对象撤消时需要对某些新增数据成员进行内存回收之前的清理操作(例如,指针数据成员所指向的动态内存的回收),就无法使用缺省析构函数完成。为了满足上述对象数据成员清理操作的需要,就必须显式定义派生类构造函数。a31析构函数的执行顺序:派生类析构函数定义体对象成员n类析构函数基类析构函数对象成员1类析构函数a325.4.3 派生类应用的实例派生类应用的实例 中定义了一个人员类 person,并以 person 为基类 派生定义了学生类 student 和教师类 teacher。另外在学生类 student 中还包含
17、了一个 teacher 类对象作为描述学生班主任的数据成员。三个类都分别定义两个构造函数(其中一个有参数表列,另一个无参数表列)和一个析构函数。通过不同形式的 student 类对象和 teacher 类对象定义表达式所导致的相应类的不同构造函数的调用,验证派生类对象创建和撤消中,基类构造函数、数据成员类构造函数和派生类构造函数的调用顺序。a33几点讨论:1 如果派生类构造函数定义中无显式初始化表,则意 味着派生类对象的基类部分创建时,调用基类构造 函数无须参数;新增数据成员创建时,调用相应数 据类构造函数也无须参数。因此,如果基类和相应 的数据类没有定义无参数或有缺省参数值的构造函 数,将会
18、导致编译错误。由此可见,一般情况在类 的定义中保留一个无须传递参数的构造函数是十分 必要的,除非需要禁止无参数创建类的对象。a34无显式初始化表的派生类构造函数的一般形式:系统的缺省构造函数是这种形式的一个特例,即无参数,无显式初始化表和空定义体的类构造函数。类名:构造函数名(参数表列)新增数据成员赋初始值代码 类名:构造函数名()a352 一般情况下,类数据成员的赋初始值操作均可以在 数据成员创建(分配内存)的同时进行,因此可以 通过初始化表同时完成数据成员的创建和赋初始值 操作。在这种情况下,如果对数据成员不需要其他 创建之后的初始化操作,就可能出现具有空定义体 的构造函数。具有空定义体的
19、构造函数的一般形式:类名:构造函数名(参数表列):基类构造函数名(参数子表列),新数据成员名1(参数子表列),新数据成员名n(参数子表列)a363 在多层次派生类构造函数的初始化表中的基类部分 表达式一般只涉及直接基类和新增数据成员的创建 和初始化操作,而间接基类的创建和初始化操作则 由直接基类的构造函数定义完成。这种分层次的构 造定义有利于简化程序编码和提高源代码的可读 性。当然,在某些特殊情况下,为了满足某种特定 要求,也允许在派生类构造函数的初始化表中对间 接基类部分进行必要的创建和初始化操作(在多重 继承将介绍这种情况的实例),但不提倡滥用。a37 用高斯消元法来求线性方程组。1 问题
20、分析所谓高斯消元法就是通过线性方程组的系数矩阵对方程组进行一系列等价变换,使得变换后的系数矩阵为一个对角线元素均为 1 的三角矩阵,然后通过逐步回代,求得方程组的解。例如,下面的三元一次方程组:2x+4y+5z=55-2x+5y 2z=205x+5y z=81使用高斯消元法对该线性方程组的系数矩阵进行等价变换的过程和逐步回代求解的过程如下所示:a38 2 4 5 55-2 5 -2 20 5 5 -1 81 5 5 -1 81 2 4 5 55-2 5 -2 20 1 1 -0.2 16.2 2 4 5 55-2 5 -2 20 1 1 -0.2 16.2-2 5 -2 20 2 4 5 55
21、1 1 -0.2 16.20 7 -2.4 52.42 4 5 551 1 -0.2 16.20 7 -2.4 52.40 2 5.4 22.61 1 -0.2 16.20 1-0.34 7.50 2 5.4 22.61 1 -0.2 16.20 1-0.34 7.50 0 6.1 7.6调整1-3行顺序使系数A11在第1列中最大等价变换第1行使系数A11=1调整2-3行顺序使系数A22在第2列中最大等价变换第2行使系数A21=0等价变换第3行使系数A31=0等价变换第2行使系数A22=1等价变换第3行使系数A33=1等价变换第3行使系数A32=0a391 1 -0.2 16.20 1-0.3
22、4 7.50 0 1 1.2z=1.2 y=7.5+1.2*0.34=7.9x=16.27.9+0.2*1.2=8.5回代求出方程组的解:a40 为实现上述操作功能,需要定义了矩阵类 matrix 作为对线性方程的系数矩阵进行操作的基类,它所提供的操作功能:构造函数:根据指定的行和列构造相应的矩阵;重载调用运算符 operator():根据索引的行、列值,引用相应的矩阵元素;输出显示函数:格式显示矩阵的全部元素值。matrix-rows:short-cols:short-elems:double+operator()(in rows:short,in cols:short):double&+p
23、rint()a41 线性方程组类 lineqns 从 matrix 派生,主要操作有:构造函数:用传递的方程个数和解进行初始化;参数产生:产生方程组的各变量系数值和常量值,从而构造方程组;高斯求解函数:使用消元法求解方程组。lineqns 和 matrix 派生关系:lineqns-neqns:int-solution:double*+generate(in coef:int)+solve()matrixlineqnsa422 详细设计 类设计 matrix 类类定义:class matrix short rows,cols;double*elems;public:matrix(short r
24、ows,short cols);matrix();double&operator()(short row,short col);void print();a43 lineqns 类类定义:class lineqns public matrixint neqns;double*solution;public:lineqns(int n,double*soln);lineqns()void generate(int coef);void solve();算法描述:a44 generate:用于产生方程的变元系数和常数generate(coef)参数 coef 指定系数的值域范围BEGIN 计算系数
25、的中值 mid=coef/2;for i=1 to 方程个数 n,step=1 设置方程组矩阵中的常数(i,n+1)的初值为0;for j=1 to 变量个数 n,step=1计算系数(i,j)=mid rand()%coef;计算常数(i,n+1)+=系数(i,j);endfor endforENDa45 solve:高斯消元求解使用 N-S 流程图描述,图中的符号约定说明:diag系数矩阵主对角线元素的行、列标识;piv同列系数中最大元素值的行标识;neqns方程组中的方程个数标识;r行序号循环标识;c列序号循环标识;factor用于消去指定系数元的变换因子标识;print显示系数矩阵的功
26、能函数标识;soln线性方程组的解矩阵标识;sum求解过程中累加和标识。a系数标识。C常数标识。a46 for diag=1 to neqnspiv=diagfor r=diag+1 to neqnsr=r+1系数a(piv,diag)系数a(r,diag)Yes Nopiv=r 系数a(piv,diag)0Yes NoC=a(piv,neqns+1)0Yes No输出方程组无解输出方程组有无穷解终止求解,并退出 piv diagYes No交换第diag 行和第 piv 行中存放的方程系数和常数第diag 行中的系数和常数逐个除以系数a(diag,diag),使系数a(diag,diag)=
27、1for r=diag+1 to neqns factor=-系数a(r,diag),使系数a(r,diag)-a(r,diag)=0 for c=diag+1 to neqns+1 系数a(r,c)=系数a(r,c)系数a(diag,c)*factor,c=c+1 r=r+1 调用(基类的)print 成员函数输出消元后的方程组系数矩阵a47 类应用 main 函数的算法:返回创建存放方程解的数组:soln=new doubleneqns从系数矩阵直接获取最后一个变元的解:solnneqns-1=a(neqns,neqns+1)for r=1 to neqns输出显示 solnr存放的变元的
28、解输入线性方程组的方程个数和相应的解根据指定的方程个数和解,创建 lineqns 类对象 eqn调用 eqn.generate 为方程组设置系数和常数调用 eqn.print 输出显示所建方程组的系数矩阵调用 eqn.solve 求解所建线性方程组 for r=neqns-1 to 1 sum=0 for diag=r+1 to neqnssum=sum+系数a(r,diag)*solndiag-1solnr-1=常数C(r,neqns+1)-suma485.5 对派生类成员访问属性的进一步讨论对派生类成员访问属性的进一步讨论 前面我们已经对派生类成员的基本访问属性进行了讨论,从讨论中我们发现
29、,要使派生类与继承的基类成员更加“无缝”结合、更加灵活可控地继承、有两个问题还需要进一步讨论并加以解决。这两个问题是:基类私有成员在派生类中不可直接访问性与派生类 新增成员函数需要能直接访问基类私有成员提高行 为操作效率和灵活性之间的矛盾。继承方式对基类成员的设定访问属性修改的局限性 与派生类期望能更加灵活、可控制地从基类继承之 间的矛盾。a495.5.1 保护成员与保护继承保护成员与保护继承1 类成员的保护访问属性 解决基类私有成员在派生类中只能通过基类的接口(公有成员函数)访问而不允许直接访问的思路 是:在不破坏派生类封装性的前提下,“突破”基类 的封装边界。解决的方法之一是增加一种新的类
30、成 员访问属性 保护访问属性:一般形式:protected 类型名 数据成员名;protected 类型名 成员函数名(参数表列);访问权限:可以在类内和派生类内被访问,而在 类外和派生类外不允许被访问。a50 访问权限的继承:私有派生:基类的保护成员在派生类中将变 成私有成员。公有派生:基类的保护成员在派生类中保持 保护访问属性。具有保护访问属性的类成员称为保护成员。将派生 类需要直接访问的基类私有成员定义为基类保护成 员,既可以提高这些基类成员在派生类内的访问效 率和方便性,又保持了这些类成员在派生类外不能 被直接访问的数据隐藏性。a51定义了能确定显示位置的基类 location,该类包
31、含两个整型的保护数据成员 x 和 y。由 location 公有派生一个点类 point,该派生类具有能在确定的位置处显示、隐去和 移动到指定位置的操作功能。再由 point 类私有派生一个圆类 circles,该派生类继承了间接基类 location 和直接基类 point 的全部成员,并重新定义各项操作。a522 类派生的保护继承方式 类派生的继承方式的作用是确定了基类成员被继承 到派生类中成为派生类成员时,其访问属性被限定 修改的规则。增加保护继承方式的目的是使派生类 成员的类外访问属性与私有继承方式相同,而当派 生类被再次派生时,直接访问间接基类成员提供可 能性。一般形式:class
32、派生类名:protected 基类名 类成员定义代码 ;a53 基类成员访问属性修改规则:私有成员:与公有继承方式和私有继承方式 相同,在派生类内外均不允许被访问。保护成员:基类的保护成员在派生类中保持 保护访问属性。公有成员:基类的公有成员在派生类中变为 保护成员。下面用图表来归纳和描述基类的 private,protected 和 public 三种类成员在以 private,protected 和 public 三种继承方式派生的新类中的访问属性的变化。a54 私有派生方式继承protected:public:interCodeNameAddressAreaCodephonePerson
33、()Person()Person inputPerson()void prPerson()protected:public:NameAddressAreaCodePhonePerson inputPerson()void prPerson()departmentyrsWorkEmployee()Employee()int testYears()class Personclass Employee:private Persona55 保护派生方式继承protected:public:interCodeNameAddressAreaCodephonePerson()Person()Person i
34、nputPerson()void prPerson()protected:public:NameAddressAreaCodePhonePerson inputPerson()void prPerson()custBalanceCostomer()Costomer()void PrtCust()class Personclass Costomer:protected PersoncustNuma56 公有派生方式继承protected:public:interCodeNameAddressAreaCodephonePerson()Person()Person inputPerson()void
35、 prPerson()protected:public:NameAddressAreaCodePhonevendOwedVendor()Vendor()Person inputPerson()void prPerson()void PrtVend()class Personclass Vendor:public PersonvendNuma57 基类成员在派生类内外的访问属性一览表派生方式派生方式基类中的访问权基类中的访问权 派生类内的访问权派生类内的访问权派生类外的访问权派生类外的访问权publicpublicpublicaccessibleprotectedprotectedinacces
36、sibleprivateinaccessibleinaccessibleprotectedpublicprotectedinaccessibleprotectedprotectedinaccessibleprivateinaccessibleinaccessibleprivatepublicprivateinaccessibleprotectedprivateinaccessibleprivateinaccessibleinaccessiblea585.5.2 派生友元类派生友元类 如果希望基类的私有成员只在派生类中能被直接访问,而不希望这种直接被访问的属性从派生类向下一层次的派生类中延续,则
37、在基类定义中将要派生的类声明为基类的友元,即从基类派生友元类。当然,也可以将基类的私有成员定义为保护成员,然后使用私有继承方式定义派生类的方法得到相同效果。例如:class set;struct node int val;node*next;a59class list node*elems;public:friend class set;class set:public list int card;public:set operator+(set&);/允许访问 list 的私有成员set operator*(set&);/允许访问 list 的私有成员;a605.5.3 访问域声明访问域声明
38、 所谓访问域声明是在私有继承方式定义的派生类中对基类的公有成员和保护成员进行声明,调整它们在派生类中访问属性,使这些基类成员保持它们在基类定义中设定的访问属性。显然,在保护继承方式定义的派生类中,访问域声明只对基类的公有成员有效,因为基类的保护成员在派生类中已经保持了基类定义中原有访问属性。而在公有继承方式定义的派生类中,访问域声明是没有意义的,因为基类的公有成员和保护成员在派生类中都保持了基类定义中原有访问属性。a61 使用访问域声明可以有效地控制在派生类外,基类的某些公有成员可以被访问,而某些公有成员被隐藏。还可以使派生类能够地向下一层次的派生类有选择地提供其基类的保护成员和公有成员。对基
39、类成员进行访问域声明必须遵守以下规则:1 访问域声明仅能调整对基类成员名,而不能为基类 成员重新说明类型,即便所说明的类型与基类成员 的原有类型相同,也是不允许的。如果声明的是成 员函数,则声明的也只是函数名而不准带有参数。例如:a62class x int a;public:int b;int f(int i,int j);class y:x public:int x:b;/错误x:f(int i,int j);/错误;a63 正确的访问域声明如下:class y:x public:x:b;/正确x:f;/正确;2 访问域声明只能使基类的保护和公有成员在派生类 中保持它们在基类定义的设定的访
40、问属性,而不能 改变基类的私有成员在派生类中的访问属性,任何 试图这样做的行为都被视为破坏封装性,是非法 的。例如:a64class xint a;public:;class y:xx:a;/非法public:;a653 访问域声明仅用于在派生类中保持基类(公有或保护)成员的原有访问属性,不允许修改它们的访问属性。也就是说,基类的保护成员只能在派生类的保护段中进行声明;而基类的公有成员只能在派生类的公有段中进行声明。例如:class x int a;protected:int b;public:int c;a66 class y:x public:x:b;/错误 protected:x:c;/
41、错误;正确的访问域声明应为:class y:x public:x:c;protected:x:b;a674 在派生类中对基类的重载成员函数名的访问域声明 将调整基类中所有以该名命名的成员函数的访问属 性。例如:class x public:f();f(int);f(char*);a68 class y:x public:x:f;在派生类中说明了 x:f 后,基类 x 中所有以 f 命名的 成员函数在派生类中都保持原有的公有访问属性。若基类中的这些重载成员函数处在不同的访问域,那么,在派生类中就不能进行访问域声明。例如:a69 class x f(float);protected:f(doubl
42、e);public:f();f(int);f(char*);a70 class y:x public:x:f;/错误;导致错误的原因:f(float)是基类私有成员,试图改变该成员函数的私有访问属性是绝对不允许的;f(double)是基类保护成员,试图改变该成员函数的保护访问属性也是不允许的。a715 如果在派生类中具有与基类中同名的类成员,则基 类中的此成员不允许在派生类中进行访问域声明,否则将产生二义性错误。例如:class x public:f();f(int);f(char*);a72 class y:x public:void f(float);x:f;/二义性错误;返回a735.6
43、 多继承多继承5.6.1 多继承的概念多继承的概念 所谓多继承就是一个新类是从多个基类中派生而成的。例如,在一个面向对象的图形用户界面中,为用户界面提供的窗口、滚动条、对话框和各种操作按钮都是通过类对象来实现的。如果希望在既有类的基础上定义一个新类具有两个以上既有类的全部属性和操作,就可以通过多继承的方法完成。如,可以由窗口类和滚动条类共同派生出一个可滚动的窗口新类。a74 在有些情况下,可以使用类的组合关系,即将一些 既有类对象定义为新类的成员来实现与多继承派生 的新类相同的功能。例如,同样是定义可滚动的窗 口类,可以以窗口类为基类单继承派生,并将滚动 条类对象作为新类的新增成员。在新类中窗
44、口类的 属性和行为被继承,而滚动条的属性和行为并没有 被继承,对滚动条对象成员的使用是通过新类的内 部消息实现的。a755.6.2 多继承的定义多继承的定义 多重继承派生类定义的一般形式:class 派生类名:继承方式 基类名1,继承方式 基类名n 派生类新增的数据成员和成员函数 ;注意,在每个基类名之前必须有继承方式,如果缺省继承方式,则表示从该基类私有派生。例如:class c:public a,b;/c 对 a 公有派生,对 b 私有派生class c:a,public b;/c 对 a 私有派生,对 b 公有派生class c:public a,public b ;/c 对 a,b 均
45、为公有派生a76 如果多继承派生的多个基类中有同名的类成员,则派生类定义中访问该同名成员时必须使用不同基类名和名域运算符加以区别,否则将导致二义性。例如:class x protected:int a;/同名数据成员 public:void make(int i)a=i;/同名成员函数;a77 class y protected:int a;/同名数据成员 public:void make(int i)a=i;/同名成员函数;class z:public x,public y public:int make()return x:a*y:a;/避免对基类中的同名数据成员 a 的二义性访问;a78
46、 在派生类外访问基类的同名成员时也须使用不同基类名和名域运算符加以区别,否则也将导致二义性。例如:main()z z1;z1.x:make(10);z1.y:make(20);cout z1.make()n;return 1;a79 如果使用组合关系定义派生类z:class z:public x public:y y1;int make()return a*y1.a;则可以避免上述的二义性错误。a805.6.3 多继承的构造函数和析构函数多继承的构造函数和析构函数1 构造函数的定义 与单继承派生类相同,在如下情况时必须显式定义 多继承派生类构造函数:派生类对象中只要有一个直接基类部分的创建需要
47、传递参数。派生类对象的新数据成员需要通过参数传初值。多继承类构造函数的定义的一般形式:a81 例如:class window/定义窗口类 public:window(int top,int left,int bottom,int right);window();构造函数名(参数表列);派生类名:构造函数名(参数表列):基类1构造函数(参数子表列),基类n构造函数(参数子表列),新数据成员1(参数子表列),新数据成员n(参数子表列)其他初始化代码;a82 class scrollbar/定义滚动条类 public:scrollbar(int top,int left,int bottom,int
48、 right);scrollbar();class scrollbarwin:window,scrollbar/定义派生类 public:scrollbarwin(int top,int left,int bottom,int right);scrollbarwin();a83 scrollbarwin:scrollbarwin(int tp,int lt,int bm,int rt):window(tp,lt,bm,rt),scrollbar(tp,rt-20,bm,rt)a842 派生类构造函数和析构函数的执行顺序 与单继承派生类对象创建时构造函数的调用顺序相 同,先构造基类部分,再构造派
49、生类。多个基类构 造函数的执行顺序与定义时从左到右的基类排列顺 序一致。派生类新增数据成员对象的构造函数的执 行顺序也与定义时的先后排列顺序一致。基类 1 构造函数基类 n 构造函数成员对象 1 构造函数成员对象 n 构造函数派生类构造函数定义体a85 派生类对象撤消时,析构函数的调用顺序与对象创 建时构造函数的调用顺序相反。基类 1 析构函数基类 n 析构函数成员对象1 析构函数成员对象 n 析构函数派生类析构函数定义体a863 多继承派生类编程实例 在屏幕上显示一个带有字符串的圆。1 问题分析 定义一个圆类 circles 和字符串类 gmessage 分别用于 在屏幕的指定位置按设定半径
50、绘制圆和在屏幕的指 定位置显示特定的字符串。为了使所显示的圆和字 符串的位置紧密相关,采用多继承机制,从 circles 和 gmessage 派生出新类 mcircle 用于完成本例的最 终需求。另外为了更好地体现面向对象的设计思 想,再定义一个 point 类,用于确定所要显示的圆和 字符串的位置。因此 circles 和 gmessage 都应该从 point 派生。a87 类图描述point#x:int#y:intpoint#x:int#y:intcircles#radius:int+show()gmessage-font:int-field:int-msg:char*+show()m