第五部分继承与类的派生教学课件

上传人:工**** 文档编号:589643786 上传时间:2024-09-11 格式:PPT 页数:118 大小:625.03KB
返回 下载 相关 举报
第五部分继承与类的派生教学课件_第1页
第1页 / 共118页
第五部分继承与类的派生教学课件_第2页
第2页 / 共118页
第五部分继承与类的派生教学课件_第3页
第3页 / 共118页
第五部分继承与类的派生教学课件_第4页
第4页 / 共118页
第五部分继承与类的派生教学课件_第5页
第5页 / 共118页
点击查看更多>>
资源描述

《第五部分继承与类的派生教学课件》由会员分享,可在线阅读,更多相关《第五部分继承与类的派生教学课件(118页珍藏版)》请在金锄头文库上搜索。

1、第五章第五章 继承与类的派生继承与类的派生 根据软件需求从软件所要模拟的现实世界中抽象出组成软件系统的对象类是面向对象程序设计的基础。面向对象的封装性使这些对象类的属性和行为细节得到了合理的保护和隐藏,并为类对象之间的通讯(方法调用)提供了安全方便的接口。 在封装性的基础上,面向对象的继承性允许一个对象类包含另一个或几个对象类的属性和行为,并使它们成为自己的属性和行为,充分地反映了现实世界中对象类之间的层次结构,为程序的代码重用提供了方便、有效的实现机制。 在面向对象程序设计中,借助继承性的实现方法,允许在既有类的基础上定义新类。被定义的新类可以从一个或多个既有类中继承属性和行为,并允许重新定

2、义这些既有类中原有的属性和行为,还允许为新类增加新的属性和行为,从而形成了类的建造层次。 既有类被称为基类或父类 新类被称为派生类、导出类或子类本章要点1 派生类的概念2 派生类的定义方法3 派生类成员的访问属性4 派生类的构造函数和析构函数5 对派生类成员访问属性的进一步讨论6 多继承7 继承在软件开发的重要意义5.1 派生类的概念派生类的概念 继承是对象类之间的一种包含关系,这种包含关系是通过对象类的建造层次关系实现的。因此,具有继承关系的类之间必定拥有以下基本性质: 类间的共享特性; 类间的细微区别; 类间的层次结构。例如: 简单的汽车分类图 汽车运输汽车专用汽车客车货车消防车洒水车5.

3、1.2 使用继承的必要性使用继承的必要性 试想如果组成一个系统的对象类均为互不包含的独立对象类,则将不可避免出现对象属性和行为的重复冗余,并且这种无层次关系的对象类既不符合现实世界的对象关系,也使对象类的定义、创建、使用和维护复杂化。继承为代码重用和建立类定义的层次结构提供方便有效的手段。例如在一个公司的管理软件设计中需要定义一个客户类 Customer 和雇员类 Employee: class Customer private:char name15;/ 姓名int age;/ 年龄char sex8;/ 性别double income;/ 收入 public:void print();/

4、显示输出状态 ; class Employment private:char name15;/ 姓名int age;/ 年龄char sex8;/ 性别char department20;/ 部门double salary;/ 工资 public:void print();/ 显示输出状态 ;比较两个类的定义,不难发现,两个类的数据成员和成员函数有许多相同之处。显然,如此定义两个类,造成的代码重复是不可避免的。 如果将 Customer 和 Employee 类定义中的相同成员抽取出来,定义一个新类 Person: class Person private:char name15;/ 姓名in

5、t age;/ 年龄char sex8;/ 性别 public:void print();/ 显示输出状态 ; Customer 和 Employee 都定义为 Person 的派生类,那些在 Person 中已经定义的共同数据成员在 Customer 和 Employee 中就不需要再定义了,只需要在各自的定义中增加自己的独有数据成员;而成员函数 print 也只需要在 Person 所定义的行为操作基础上重新定义自己的行为操作。class Customer : public Personprivate:double income;/ 收入public:void print();/ 显示输出

6、状态; class Employee : public Person private:char department20;/ 部门double salary;/ 工资 public:void print();/ 显示输出状态 ; 显然通过继承可以从基类 Person 派生出一组具有层次结构的新类,构成一个公司管理系统的主要对象类型。例如: 使用继承机制和方法设计和建造类定义的层次结构对于建立一个面向对象的软件系统是不可缺少的。返回Person EmployeeCustomerVendorSalariedHourlyPartner ClientPartTimeFullTime5.2 派生类的定义

7、方法派生类的定义方法 定义派生类的一般形式:class 派生类名 : 派生方式 基类名派生类的新增加成员和基类成员的新定义 ;例如:基类 Person 和派生类的定义class Person private:char name15;/ 姓名int age;/ 年龄char sex8;/ 性别public:void print();/ 显示输出状态;class Customer : public Personprivate: double income;/ 新增加的数据成员“收入”public: void print();/ 重新定义基类的“显示输出状态”; 从形式上比较,派生类定义与非派生类定

8、义的差别仅在于定义首行中由 “:” 引出的派生表达式。其中: 派生方式:指明派生类继承基类成员的方式,方式 的种类有 public、private 和 protected。如果不指明方式名,则缺省指定派生方式为 private。 基类名:指明派生类所继承的类。 在 Java 中类的派生又被称为类的扩展,这种扩展相当于 C+ 中的公有(public)派生,其一般形式如下: class 派生类名 :extends 基类名 派生类的新增加成员和基类成员的新定义 5.2.1 派生类的构成派生类的构成 派生类的构成可以有下图示意:派生类名基类的所有成员派生类的新增加成员 例如整数链表类 list 的定义

9、: class list/ 链表类名超前声明 class node / 结点类定义 int val;node *next; public:friend class list; ; class list / 整数链表类定义 node *elems/ 链表头指针 public:list();list();bool insert(int);/ 在表头插入一个结点bool deletes(int);/ 从表中删除一个结点bool contains(int);/ 在表中查找一个结点 ;一个链表结构的整数集合可以看成是不含重复元素的特殊整数链表,因此整数集合类可以从整数链表类派生。整数集合类在继承了整数链

10、表类的所有成员的基础上,需要新增加一个能指示集合中元素个数的数据成员,同时还需要重新定义整数链表类的插入操作insert,禁止重复元素被插入。 class set : public list int card;/ 集合中的元素个数 public:bool insert(int);/ 重新定义插入函数 ;返回5.3 派生类成员的访问属性派生类成员的访问属性 类成员的访问属性是指类成员的被访问权限,这种权限随着作用域的变化而变化的,派生类成员的访问属性也分为类内访问属性和类外访问属性两种情况。1 类内访问属性 由于派生类的成员分为继承的基类成员和自身的新 增成员两种,这两种成员的类内访问属性是有所

11、区 别的。 基类成员的访问属性封装性所限定的类成员类外访问权限确定了基类成员在派生类定义中被访问的限定原则: 私有成员:不允许被访问,与派生类从基类 的继承方式无关。 公有成员:允许被访问,与派生类从基类的 继承方式无关。 新增成员的访问属性所有的新增成员均允许被访问,与新增成员被设定的访问属性(公有或私有)无关。2 类外访问属性 类成员的类外访问是指在类对象定义域外访问对象 的类成员。因此,派生类成员在类定义中声明的访 问属性确定了派生类成员的类外访问属性: 基类成员的访问属性 私有成员:不允许被访问,与派生类从基类 的继承方式无关。 公有成员:依据继承方式的不同,在基类中 被设定的公有属性

12、会发生不同的变化。 私有继承:基类的公有成员变为派生类的私有成员,因此在类外不允许被访问。 公有继承:基类的公有成员在派生类中仍保持公有属性,因此在类外允许被访问。 新增加成员的访问属性类成员在类定义中被声明的访问属性确定了类成员的类外访问属性。 class baseprivatepublicclass derived2: public derived1privatepublicclass derived1:baseprivatepublicOKOKNONONONOderived2类内类外OKNOOKNONONO例例5-15-1 包含了两个类:由 C+ 标准模板库提供的 string 类具有丰

13、富的字符串操作功能。edit_string 类是从 string 类派生的,它在继承 string 类功能的基础上增加了数据成员 光标和实现在光标处的进行插入、替换、删除等文本编辑功能。注意,派生类的成员名支配基类的成员名。下面的类图描述了 string 和 edit_string 之间的派生层次结构。返回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:st

14、ring):int+dele_at_cursor(in how_much:int)5.4 派生类的构造函数和析构函数派生类的构造函数和析构函数5.4.1 派生类的构造函数派生类的构造函数 与一般非派生类相同,系统会为派生类定义一个缺省(无参数、无显式初始化表、无数据成员初始化代码)构造函数用于完成派生类对象创建时的内存分配操作。但如果在派生类对象创建时需要实现以下两种操作或其中之一,就无法使用缺省构造函数完成。 派生类对象的直接基类部分创建需要传递参数。 派生类对象的新数据成员需要通过参数传递初值。为了满足上述对象创建操作的需要,就必须显式定义派生类构造函数。派生类构造函数声明和定义的一般形式

15、:注意: 构造函数名后面的参数表列中包含了初始化表中创 建对象的基类部分、新增数据成员和在函数体中为 新数据成员赋初始值所需要的全部参数。构造函数名(参数表列);类名:构造函数名(参数表列): 基类构造函数名(参数子表列), 新数据成员名1(参数子表列), 新数据成员名n(参数子表列) 其他初始化代码 初始化表中创建对象的基类部分的表达式必须使用 基类构造函数名调用基类构造函数,而创建数据成 员表达式必须使用数据成员名调用数据成员类的构 造函数。派生类构造函数的执行顺序:基类构造函数对象成员1类构造函数派生类构造函数定义体对象成员n类构造函数5.4.2 派生类的析构函数派生类的析构函数 与一般

16、非派生类相同,系统会为派生类定义一个缺省(无数据成员的清理代码)析构函数用于完成派生类对象撤消时的内存回收操作。但如果在派生类对象撤消时需要对某些新增数据成员进行内存回收之前的清理操作(例如,指针数据成员所指向的动态内存的回收),就无法使用缺省析构函数完成。为了满足上述对象数据成员清理操作的需要,就必须显式定义派生类构造函数。析构函数的执行顺序:派生类析构函数定义体对象成员n类析构函数基类析构函数对象成员1类析构函数5.4.3 派生类应用的实例派生类应用的实例例例5-25-2 中定义了一个人员类 person,并以 person 为基类 派生定义了学生类 student 和教师类 teache

17、r。另外在学生类 student 中还包含了一个 teacher 类对象作为描述学生班主任的数据成员。三个类都分别定义两个构造函数(其中一个有参数表列,另一个无参数表列)和一个析构函数。通过不同形式的 student 类对象和 teacher 类对象定义表达式所导致的相应类的不同构造函数的调用,验证派生类对象创建和撤消中,基类构造函数、数据成员类构造函数和派生类构造函数的调用顺序。几点讨论:1 如果派生类构造函数定义中无显式初始化表,则意 味着派生类对象的基类部分创建时,调用基类构造 函数无须参数;新增数据成员创建时,调用相应数 据类构造函数也无须参数。因此,如果基类和相应 的数据类没有定义无

18、参数或有缺省参数值的构造函 数,将会导致编译错误。由此可见,一般情况在类 的定义中保留一个无须传递参数的构造函数是十分 必要的,除非需要禁止无参数创建类的对象。无显式初始化表的派生类构造函数的一般形式:系统的缺省构造函数是这种形式的一个特例,即无参数,无显式初始化表和空定义体的类构造函数。类名:构造函数名(参数表列) 新增数据成员赋初始值代码 类名:构造函数名() 2 一般情况下,类数据成员的赋初始值操作均可以在 数据成员创建(分配内存)的同时进行,因此可以 通过初始化表同时完成数据成员的创建和赋初始值 操作。在这种情况下,如果对数据成员不需要其他 创建之后的初始化操作,就可能出现具有空定义体

19、 的构造函数。 具有空定义体的构造函数的一般形式:类名:构造函数名(参数表列) : 基类构造函数名(参数子表列), 新数据成员名1(参数子表列), 新数据成员名n(参数子表列) 3 在多层次派生类构造函数的初始化表中的基类部分 表达式一般只涉及直接基类和新增数据成员的创建 和初始化操作,而间接基类的创建和初始化操作则 由直接基类的构造函数定义完成。这种分层次的构 造定义有利于简化程序编码和提高源代码的可读 性。当然,在某些特殊情况下,为了满足某种特定 要求,也允许在派生类构造函数的初始化表中对间 接基类部分进行必要的创建和初始化操作(在多重 继承将介绍这种情况的实例),但不提倡滥用。例例5-3

20、5-3 用高斯消元法来求线性方程组。1 问题分析所谓高斯消元法就是通过线性方程组的系数矩阵对方程组进行一系列等价变换,使得变换后的系数矩阵为一个对角线元素均为 1 的三角矩阵,然后通过逐步回代,求得方程组的解。例如,下面的三元一次方程组:2x + 4y + 5z = 55-2x + 5y 2z = 205x + 5y z = 81使用高斯消元法对该线性方程组的系数矩阵进行等价变换的过程和逐步回代求解的过程如下所示: 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

21、 1 1 -0.2 16.2-2 5 -2 20 2 4 5 551 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行使系数

22、A33 = 1等价变换第3行使系数A32 = 01 1 -0.2 16.20 1 -0.34 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回代求出方程组的解: 为实现上述操作功能,需要定义了矩阵类 matrix 作为对线性方程的系数矩阵进行操作的基类,它所提供的操作功能: 构造函数:根据指定的行和列构造相应的矩阵; 重载调用运算符 operator():根据索引的行、列值, 引用相应的矩阵元素; 输出显示函数:格式显示矩阵的全部元素值。matrix-rows:short-cols:short-elems:

23、double+operator ()(in rows:short, in cols:short):double&+print() 线性方程组类 lineqns 从 matrix 派生,主要操作有: 构造函数:用传递的方程个数和解进行初始化; 参数产生:产生方程组的各变量系数值和常量值, 从而构造方程组; 高斯求解函数:使用消元法求解方程组。 lineqns 和 matrix 派生关系:lineqns-neqns:int-solution:double*+generate(in coef:int)+solve()matrixlineqns2 详细设计 类设计 matrix 类类定义:class

24、matrix short rows, cols;double *elems;public:matrix(short rows, short cols);matrix();double& operator() (short row, short col);void print(); lineqns 类类定义:class lineqns public matrixint neqns;double *solution;public:lineqns(int n, double *soln);lineqns()void generate(int coef);void solve();算法描述: gene

25、rate :用于产生方程的变元系数和常数generate(coef)参数 coef 指定系数的值域范围BEGIN 计算系数的中值 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 endforEND solve :高斯消元求解使用 N-S 流程图描述,图中的符号约定说明: diag系数矩阵主对角线元素的行、列标识; piv

26、同列系数中最大元素值的行标识; neqns方程组中的方程个数标识; r行序号循环标识; c列序号循环标识; factor用于消去指定系数元的变换因子标识; print显示系数矩阵的功能函数标识; soln线性方程组的解矩阵标识; sum求解过程中累加和标识。 a系数标识。 C常数标识。 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输出方程组无解输

27、出方程组有无穷解终止求解,并退出 piv diagYes No交换第diag 行和第 piv 行中存放的方程系数和常数第diag 行中的系数和常数逐个除以系数a(diag,diag),使系数a(diag,diag)=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 成员函数输出消元后的方程

28、组系数矩阵 类应用 main 函数的算法:返回创建存放方程解的数组:soln = new doubleneqns从系数矩阵直接获取最后一个变元的解:solnneqns-1 = a(neqns,neqns+1) for r = 1 to neqns输出显示 solnr存放的变元的解输入线性方程组的方程个数和相应的解根据指定的方程个数和解,创建 lineqns 类对象 eqn调用 eqn.generate 为方程组设置系数和常数调用 eqn.print 输出显示所建方程组的系数矩阵调用 eqn.solve 求解所建线性方程组 for r = neqns-1 to 1 sum = 0 for dia

29、g = r+1 to neqnssum = sum + 系数a(r, diag) * solndiag-1solnr-1 = 常数C(r, neqns+1) - sum5.5 对派生类成员访问属性的进一步讨论对派生类成员访问属性的进一步讨论 前面我们已经对派生类成员的基本访问属性进行了讨论,从讨论中我们发现,要使派生类与继承的基类成员更加 “无缝” 结合、更加灵活可控地继承、有两个问题还需要进一步讨论并加以解决。这两个问题是: 基类私有成员在派生类中不可直接访问性与派生类 新增成员函数需要能直接访问基类私有成员提高行 为操作效率和灵活性之间的矛盾。 继承方式对基类成员的设定访问属性修改的局限性

30、 与派生类期望能更加灵活、可控制地从基类继承之 间的矛盾。5.5.1 保护成员与保护继承保护成员与保护继承1 类成员的保护访问属性 解决基类私有成员在派生类中只能通过基类的接口 (公有成员函数)访问而不允许直接访问的思路 是:在不破坏派生类封装性的前提下,“突破”基类 的封装边界。解决的方法之一是增加一种新的类成 员访问属性 保护访问属性: 一般形式:protected 类型名 数据成员名; protected 类型名 成员函数名(参数表列); 访问权限:可以在类内和派生类内被访问,而在 类外和派生类外不允许被访问。 访问权限的继承: 私有派生:基类的保护成员在派生类中将变 成私有成员。 公有

31、派生:基类的保护成员在派生类中保持 保护访问属性。 具有保护访问属性的类成员称为保护成员。将派生 类需要直接访问的基类私有成员定义为基类保护成 员,既可以提高这些基类成员在派生类内的访问效 率和方便性,又保持了这些类成员在派生类外不能 被直接访问的数据隐藏性。例例5-45-4定义了能确定显示位置的基类 location,该类包含两个整型的保护数据成员 x 和 y。由 location 公有派生一个点类 point, 该派生类具有能在确定的位置处显示、隐去和 移动到指定位置的操作功能。再由 point 类私有派生一个圆类 circles,该派生类继承了间接基类 location 和直接基类 po

32、int 的全部成员,并重新定义各项操作。2 类派生的保护继承方式 类派生的继承方式的作用是确定了基类成员被继承 到派生类中成为派生类成员时,其访问属性被限定 修改的规则。增加保护继承方式的目的是使派生类 成员的类外访问属性与私有继承方式相同,而当派 生类被再次派生时,直接访问间接基类成员提供可 能性。 一般形式:class 派生类名 : protected 基类名 类成员定义代码 ; 基类成员访问属性修改规则: 私有成员:与公有继承方式和私有继承方式 相同,在派生类内外均不允许被访问。 保护成员:基类的保护成员在派生类中保持 保护访问属性。 公有成员:基类的公有成员在派生类中变为 保护成员。

33、下面用图表来归纳和描述基类的 private,protected 和 public 三种类成员在以 private,protected 和 public 三种继承方式派生的新类中的访问属性的变化。 私有派生方式继承protected:public:interCodeNameAddressAreaCodephonePerson()Person()Person inputPerson()void prPerson()protected:public:NameAddressAreaCodePhonePerson inputPerson ()void prPerson()departmentyrsWo

34、rkEmployee()Employee()int testYears()class Personclass Employee:private Person 保护派生方式继承protected:public:interCodeNameAddressAreaCodephonePerson()Person()Person inputPerson()void prPerson()protected:public:NameAddressAreaCodePhonePerson inputPerson ()void prPerson()custBalanceCostomer()Costomer()void

35、 PrtCust()class Personclass Costomer:protected PersoncustNum 公有派生方式继承protected:public:interCodeNameAddressAreaCodephonePerson()Person()Person inputPerson()void prPerson()protected:public:NameAddressAreaCodePhonevendOwedVendor()Vendor()Person inputPerson ()void prPerson()void PrtVend()class Personcla

36、ss Vendor:public PersonvendNum 基类成员在派生类内外的访问属性一览表派生方式派生方式基类中的访问权基类中的访问权 派生类内的访问权派生类内的访问权派生类外的访问权派生类外的访问权publicpublicpublicaccessibleprotectedprotectedinaccessibleprivateinaccessibleinaccessibleprotectedpublicprotectedinaccessibleprotectedprotectedinaccessibleprivateinaccessibleinaccessibleprivatepub

37、licprivateinaccessibleprotectedprivateinaccessibleprivateinaccessibleinaccessible5.5.2 派生友元类派生友元类 如果希望基类的私有成员只在派生类中能被直接访问,而不希望这种直接被访问的属性从派生类向下一层次的派生类中延续,则在基类定义中将要派生的类声明为基类的友元,即从基类派生友元类。当然,也可以将基类的私有成员定义为保护成员,然后使用私有继承方式定义派生类的方法得到相同效果。例如:class set;struct node int val;node *next;class list node *elems;p

38、ublic:friend class set;class set : public list int card;public:set operator + (set&);/ 允许访问 list 的私有成员set operator * (set&); / 允许访问 list 的私有成员;5.5.3 访问域声明访问域声明 所谓访问域声明是在私有继承方式定义的派生类中对基类的公有成员和保护成员进行声明,调整它们在派生类中访问属性,使这些基类成员保持它们在基类定义中设定的访问属性。 显然,在保护继承方式定义的派生类中,访问域声明只对基类的公有成员有效,因为基类的保护成员在派生类中已经保持了基类定义中原

39、有访问属性。而在公有继承方式定义的派生类中,访问域声明是没有意义的,因为基类的公有成员和保护成员在派生类中都保持了基类定义中原有访问属性。 使用访问域声明可以有效地控制在派生类外,基类的某些公有成员可以被访问,而某些公有成员被隐藏。还可以使派生类能够地向下一层次的派生类有选择地提供其基类的保护成员和公有成员。对基类成员进行访问域声明必须遵守以下规则:1 访问域声明仅能调整对基类成员名,而不能为基类 成员重新说明类型,即便所说明的类型与基类成员 的原有类型相同,也是不允许的。如果声明的是成 员函数,则声明的也只是函数名而不准带有参数。 例如:class x int a;public:int b;

40、int f(int i, int j);class y : x public:int x:b;/ 错误x:f(int i, int j);/ 错误; 正确的访问域声明如下: class y : x public:x:b;/ 正确x:f;/ 正确 ;2 访问域声明只能使基类的保护和公有成员在派生类 中保持它们在基类定义的设定的访问属性,而不能 改变基类的私有成员在派生类中的访问属性,任何 试图这样做的行为都被视为破坏封装性,是非法 的。例如:class xint a;public:;class y : xx:a;/ 非法public:;3 访问域声明仅用于在派生类中保持基类(公有或保护)成员的原

41、有访问属性,不允许修改它们的访问属性。也就是说,基类的保护成员只能在派生类的保护段中进行声明;而基类的公有成员只能在派生类的公有段中进行声明。例如: class x int a; protected:int b; public:int c; ; class y : x public: x:b;/ 错误 protected: x:c;/ 错误 ; 正确的访问域声明应为: class y : x public:x:c; protected:x:b; ;4 在派生类中对基类的重载成员函数名的访问域声明 将调整基类中所有以该名命名的成员函数的访问属 性。例如: class x public:f();f

42、(int);f(char*); ; class y : x public:x:f; ; 在派生类中说明了 x:f 后,基类 x 中所有以 f 命名的 成员函数在派生类中都保持原有的公有访问属性。 若基类中的这些重载成员函数处在不同的访问域, 那么,在派生类中就不能进行访问域声明。例如: class x f(float); protected:f(double); public:f();f(int);f(char*); ; class y : x public:x:f;/ 错误 ; 导致错误的原因: f(float) 是基类私有成员,试图改变该成员函数的私有访问属性是绝对不允许的; f(doub

43、le) 是基类保护成员,试图改变该成员函数的保护访问属性也是不允许的。5 如果在派生类中具有与基类中同名的类成员,则基 类中的此成员不允许在派生类中进行访问域声明,否则将产生二义性错误。例如: class x public:f();f(int);f(char*); ; class y : x public: void f(float);x:f; / 二义性错误 ;返回5.6 多继承多继承5.6.1 多继承的概念多继承的概念 所谓多继承就是一个新类是从多个基类中派生而成的。例如,在一个面向对象的图形用户界面中,为用户界面提供的窗口、滚动条、对话框和各种操作按钮都是通过类对象来实现的。如果希望在既

44、有类的基础上定义一个新类具有两个以上既有类的全部属性和操作,就可以通过多继承的方法完成。如,可以由窗口类和滚动条类共同派生出一个可滚动的窗口新类。 在有些情况下,可以使用类的组合关系,即将一些 既有类对象定义为新类的成员来实现与多继承派生 的新类相同的功能。例如,同样是定义可滚动的窗 口类,可以以窗口类为基类单继承派生,并将滚动 条类对象作为新类的新增成员。在新类中窗口类的 属性和行为被继承,而滚动条的属性和行为并没有 被继承,对滚动条对象成员的使用是通过新类的内 部消息实现的。5.6.2 多继承的定义多继承的定义 多重继承派生类定义的一般形式:class 派生类名:继承方式 基类名1, 继承

45、方式 基类名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 均为公有派生 如果多继承派生的多个基类中有同名的类成员,则派生类定义中访问该同名成员时必须使用不同基类名和名域运算符加以区别,否则将导致二义性。例如: class x protected:int a;

46、/ 同名数据成员 public:void make(int i) a = i; / 同名成员函数 ; 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 的二义性访问 ; 在派生类外访问基类的同名成员时也须使用不同基类名和名域运算符加以区别,否则也将导致二义性。例如: main() z z1;z1.x:make(10);z1.y:m

47、ake(20); cout z1.make() n;return 1; 如果使用组合关系定义派生类z: class z : public x public:y y1;int make() return a * y1.a; ;则可以避免上述的二义性错误。5.6.3 多继承的构造函数和析构函数多继承的构造函数和析构函数1 构造函数的定义 与单继承派生类相同,在如下情况时必须显式定义 多继承派生类构造函数: 派生类对象中只要有一个直接基类部分的创建需要传递参数。 派生类对象的新数据成员需要通过参数传初值。 多继承类构造函数的定义的一般形式: 例如: class window/ 定义窗口类 publi

48、c:window(int top, int left, int bottom, int right);window(); ;构造函数名(参数表列);派生类名:构造函数名(参数表列) :基类1构造函数 (参数子表列), 基类n构造函数 (参数子表列),新数据成员1(参数子表列), 新数据成员n(参数子表列) 其他初始化代码 ; class scrollbar/ 定义滚动条类 public:scrollbar(int top, int left, int bottom, int right);scrollbar(); ; class scrollbarwin : window, scrollbar

49、/ 定义派生类 public:scrollbarwin(int top,int left,int bottom,int right);scrollbarwin(); ; scrollbarwin:scrollbarwin(int tp, int lt, int bm, int rt): window(tp, lt, bm, rt), scrollbar(tp, rt - 20, bm, rt) 2 派生类构造函数和析构函数的执行顺序 与单继承派生类对象创建时构造函数的调用顺序相 同,先构造基类部分,再构造派生类。多个基类构 造函数的执行顺序与定义时从左到右的基类排列顺 序一致。派生类新增数据成

50、员对象的构造函数的执 行顺序也与定义时的先后排列顺序一致。基类 1 构造函数基类 n 构造函数成员对象 1 构造函数成员对象 n 构造函数派生类构造函数定义体 派生类对象撤消时,析构函数的调用顺序与对象创 建时构造函数的调用顺序相反。基类 1 析构函数基类 n 析构函数成员对象1 析构函数成员对象 n 析构函数派生类析构函数定义体3 多继承派生类编程实例例例5-55-5 在屏幕上显示一个带有字符串的圆。1 问题分析 定义一个圆类 circles 和字符串类 gmessage 分别用于 在屏幕的指定位置按设定半径绘制圆和在屏幕的指 定位置显示特定的字符串。为了使所显示的圆和字 符串的位置紧密相关

51、,采用多继承机制,从 circles 和 gmessage 派生出新类 mcircle 用于完成本例的最 终需求。另外为了更好地体现面向对象的设计思 想,再定义一个 point 类,用于确定所要显示的圆和 字符串的位置。因此 circles 和 gmessage 都应该从 point 派生。 类图描述point#x:int#y:intpoint#x:int#y:intcircles#radius:int+show()gmessage-font:int-field:int-msg:char*+show()mcircle+show()例例5-65-6 一个远程网络中记录两台机器之间的平均传输时间如

52、下表所示:编写一个能按照网络通讯的出发地和目的地输入、保存传输时间,并能按照上述表格形式输出已经保存的网络通讯时间。BeijingShanghaiQingdaoBeijing03.5512.45shanghai2.34010.31Qingdao15.369.3201 问题分析: 使用矩阵结构保存二维表格数据是最为恰当的。但 由于本需求中二维表中的行、列位置的不是整数下 标而是字符串,所以描述该表格不能直接使用通用 矩阵类,而需要使用以字符串指示位置的特殊矩阵 类。其中,作为行、列下标的字符串可以通过向量 类将字符串映射为指示通用矩阵元素的整数下标。 显然该特殊矩阵是由向量和通用矩阵协同工作实现

53、 的。为此,定义通用矩阵类 Matrix 用于完成通讯网 络表的基础操作,再定义向量类 AssocVec 用于将 通讯地址描述串转换为矩阵元素下标,而网络通讯 传输表类 Table 可以从这两个类派生。这些类之间 的关系如下图所示:Matrix#rows:int#cols:int#elems:double*+operator()(in r:int, in c:int):double&+Print(in r:int)+Print()AssocVec#dim:int#used:int#elems:VecElem*+operator(in idx:char*):int&VecElem+index:c

54、har*+value:int0n *elemsTable+operator()(in src:char*, in dest:char*):double&+Print()2 类的设计实现 向量结构 VecElem :struct VecElem char* index;/ 索引字符串int value;/ 映射变量; 辅助向量类 AssocVec:class AssocVec public: AssocVec(int dim);/ 构造函数 AssoVec();/ 析构函数 int& operator (char* idx);/ 重载下标运算符,通过索引字串访问映射。protected: Vec

55、Elem* elems; / 指向动态创建的向量数组的指针 int dim;/ 向量数组的维数 int used;/ 向量数组中被使用的元素个数;下标运算符 AssocVec:operator (char* idx) 算法: for i = 0 to used 索引字串复制到elemsused+1.index使elemsused+1.value = used+1,并返回elemsused+1.value的引用返回值为-1的静态哑变量引用,指示调用失败 elemsi.index = 参数指定字串?YesNo used dim 并且为参数指定的 索引字串动态分配存储 空间成功?YesNo根据参数指

56、定的索引字串查询已有向量,返回匹配映射变量的引用 通用矩阵类 Matrix:class Matrix public: Matrix(int rows, int cols);/ 构造函数 Matrix();/ 析构函数 double& operator()(short r, short c); / 重载调用运算符,实现矩阵元素的访问 void print(short r);/ 矩阵行显示操作 void print();/ 矩阵显示操作protected: double* elems;/ 指向动态创建的矩阵数组的指针 short rows, cols; / 二维矩阵的行、列索引; 调用运算符 Ma

57、trix:operator () (short r, short c) 的算法:返回参数指定的要访问的矩阵元素:elems(r-1)*(c-1)的引用返回静态哑变量的引用 参数指定的要访问的矩阵元素的 行、列索引值 r 和 c 是否合法?YesNo 网络通讯传输表类 Table:class Table : AssocVec, Matrix public: Table(short entries); / 构造函数 double& operator()(char* src, char* dest);/ 重载调用运算符,通过索引字串访问矩阵元素 Print();;调用运算符 Table:operat

58、or () (char* src, char* dest) 的算法:调用 AssocVec:operator 将参数索引字串 src 和 dest 映射为整数索引 r 和 c 调用 Matrix:operator(r, c) 返回 r 和 c 确定的矩阵元素的引用 5.6.4 虚基类虚基类1 为什麽要使用虚基类 在多继承派生中一种可能产生二义性的情况: 派生类有多个直接基类是同一个间接基类的派 生类; 在派生类中需要访问共同间接基类的成员。 例如: class base / 共同的间接基类 protected:int a; public:base() a = 5; ; class base1

59、: public base/ 直接基类1 public:base1() cout base1 a = a endl; ; class base2 : public base/ 直接基类2 public:base2() cout base2 a = a endl; ; class derived : public base1, public base2 public:derived() cout derived a = a endl; / 二义性:base1:a 还是 base2:a ? ; main() derived obj;return 0; basebasebase1base2deriv

60、ed2 虚基类的概念 显然,解决上述二义性问题的办法是使派生类对象 层次结构中只有一个间接基类 base 实体。C+ 允许 在派生类的定义中使用关键字 virtual 将基类说明为 虚基类来实现此目的。用虚基类重新定义上例中的 直接基类: class base1 : virtual public base public:base1() cout base1 a = a endl; ; class base2 : virtual public base public:base2() cout base2 a = a endl; ; class derived : public base1, pu

61、blic base2 public:derived() cout derived a = a endl; / 访问 base:a 具有唯一性 ; 又例如,在定义图形用户界面时,可以考虑定义这 样三 个类: class port ;图形显示区 class region ;屏幕上的任意区域 class menu ;菜单-选项的集合baseBase1Base2derived 从这些类中派生出两个新类:window 类和 tools 类: class window : public port, public region ; class tools : public port, public menu

62、 ; 这两个派生类还可以再派生出新类: class appwind : public window, public tools ; 派生 appwind 的目的是为了得到一个带工具条的窗 口, 但根据上述定义得到的结果(如下图所示)是 工具条和窗口分别拥有自己的显示区,这并不是所 希望的。 所希望得到的是这两个对象为一个整体,只包含一 个显示区,即工具条是包含在窗口之内的。要做到 这一点需要在窗口类 window 和工具条类 tools 的定 义中将显示区类 port 定义为虚基类。因此 tools 和 window 的定义变为如下形式: class window : virtual publ

63、ic port, public region ; class tools : virtual public port, public menu ; Window abc + 此时窗口即变为如下图所示的形式:使用虚基类的两点说明: 关键字 virtual 与继承方式 public,protected 和 private 的先后顺序无关紧要,它只说明是虚拟派生。 例如下面两个虚拟派生的声明等价。 class derived : virtual public base ; class derived : public virtual base ; Window abc + 任何一个类都可以在作为某些

64、派生类的虚基类的同时,又作为另一些派生类的非虚基类,例如: class b ; class x : virtual public b ; class y : virtual public b ; class z : public b ; class aa : public x, public y, public z ; 类 aa 的对象的层次结构如图所示: bbxyzaa3 虚基类的初始化 多继承中,虚基类与非虚基类在构造函数的调用顺 序上不同。虚基类构造函数调用顺序有三个原则: 若同一层次中同时包含虚基类和非虚基类,先调用虚基类构造函数,再调用非虚基类的构造函数,即“同层优先”。 若同一层次

65、中包含多个虚基类,虚基类的构造函数按照定义表达式中的顺序从左至右调用。 若虚基类是一个派生类,则仍然先调用该虚基类的基类构造函数,再调用该虚基类的构造函数。 注意,一般情况下,虚基类只定义不带参数的或带 缺省参数的构造函数。否则虚基类的构造函数必须 在最终派生类构造函数的初始化表中,显式列出。例例5-75-7 该程序的运行结果所显示的信息描述了上述虚基类初始化时构造函数的调用顺序原则。各个类的派生层次图如下: base2 base level1 base2 level2 toplevel例例5-85-8 定义一个多继承派生类 mix 用来实现在显示器屏幕上的以一个指定点为中心画弧、圆、椭圆和矩

66、形。mix 的基类为弧类 arcs、圆类 circles、椭圆类 ellipses 和矩形类 rectangles 。这些基类又由一个共同的基类 point 派生而来,因此需要将 point 定义为虚基类。各类之间的关系图如下:pointmixarcscirclesrectanglesellipses不难看出,多继承机制是一把双刃剑,它虽然为类提供了更加强大和灵活的派生定义能力,为解决某些复杂问题提供了一种有效的手段,但同时也因为多继承本身具有复杂的类间包含关系,引起了类的不稳定性和加大了处理共享和二义性的难度。虚基类机制正是为了有效地解决多继承引起的问题而引入。因此,在 C+ 的有些编程环境

67、中对多继承的实现做了一定的限制,例如 Visual C+ 的 MFC 编程向导就不提供多继承选择(但还是支持手工编程实现多继承功能的)。Java 是不支持多继承机制的,所以在 Java 的类扩展(extends)定义中只允许从一个已定义类进行扩展,从根本上避免了 C+ 的多继承所造成的麻烦。而单继承带来的过分限制问题是通过允许一个类可以实现(implements)多个接口(抽象方法类)来解决的。关于 Java 的接口和接口的实现与 C+ 的抽象类类似,将在第六章中做简单介绍。返回5.7 继承在软件开发中的重要意义继承在软件开发中的重要意义 继承是面向对象程序设计最重要的技术之一。继承为软件重用

68、和建立良好的软件层次结构提供了符合认识规律和知识拓展规律的设计机制和方便、有效的实现方法。 正是继承所提供的机制和方法,使软件开发系统的厂商为各种程序设计开发的需要开发了各类实用的类库,作为用户设计开发各类软件的基础。目前在不同的系统平台上的不同软件开发系统中,用于各类软件设计开发的基类库很多,归纳它们的组成、功能、特点大致有以下几点:1 为各类软件需要的通用功能提供抽象基类、模板基 类、应用基类等,用户可以从这些基类派生实现 类、创建模板实例或直接使用基类。例如 C+ 的标 准流类库、标准模板库 STL 等。2 为各类软件框架提供构造和运行基类等,用户可以 从这些基类派生满足特定需求的类,组

69、成特定的程 序运行框架。例如 Visual C+ MFC ,java Beans 等类 库中程序框架基类。3 为各类软件需要的专项功能,例如数据库应用、网 络通讯等,提供基类,用户可以从这些基类派生实 际应用类。例如 Visual C+ MFC 中的 ODBC 基类、 ADO 组件类等。4 基类库的实现技术(静态库、动态库、组件)各有 不同,但都不允许用户修改库中的基类,避免了一 个软件的特定需求影响到其他软件对基类的使用, 提高了通用性。5 使用基类的软件只需要对派生类新增的部分进行编 译,然后与基类一起链接,大大提高了调试程序的 效率。如果厂家修改了基类,只要基类的公用接口 不变,使用基类的派生类就无须修改,但需要重新 编译链接。6 基类库的使用一般都依赖于不同的操作系统和软件 开发环境。但有些基类库已经成为某种语言标准的 一部分,仅使用这些基类库的程序在任何操作操作 系统和软件开发环境中一般都不需要修改,具有良 好的移植性,例如 C+ 的标准流类库、标准模板库 STL 等。返回

展开阅读全文
相关资源
正为您匹配相似的精品文档
相关搜索

最新文档


当前位置:首页 > 资格认证/考试 > 自考

电脑版 |金锄头文库版权所有
经营许可证:蜀ICP备13022795号 | 川公网安备 51140202000112号