已阅读5页,还剩113页未读, 继续免费阅读
版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
第五章 继承与类的派生,根据软件需求从软件所要模拟的现实世界中抽象出 组成软件系统的对象类是面向对象程序设计的基础。 面向对象的封装性使这些对象类的属性和行为细节得 到了合理的保护和隐藏,并为类对象之间的通讯(方 法调用)提供了安全方便的接口。 在封装性的基础上,面向对象的继承性允许一个对 象类包含另一个或几个对象类的属性和行为,并使它 们成为自己的属性和行为,充分地反映了现实世界中 对象类之间的层次结构,为程序的代码重用提供了方 便、有效的实现机制。,在面向对象程序设计中,借助继承性的实现方法, 允许在既有类的基础上定义新类。被定义的新类可以 从一个或多个既有类中继承属性和行为,并允许重新 定义这些既有类中原有的属性和行为,还允许为新类 增加新的属性和行为,从而形成了类的建造层次。 既有类被称为基类或父类 新类被称为派生类、导出类或子类,本章要点 1 派生类的概念 2 派生类的定义方法 3 派生类成员的访问属性 4 派生类的构造函数和析构函数 5 对派生类成员访问属性的进一步讨论 6 多继承 7 继承在软件开发的重要意义,5.1 派生类的概念 继承是对象类之间的一种包含关系,这种包含关系 是通过对象类的建造层次关系实现的。因此,具有继 承关系的类之间必定拥有以下基本性质: 类间的共享特性; 类间的细微区别; 类间的层次结构。,例如: 简单的汽车分类图,汽车,5.1.2 使用继承的必要性 试想如果组成一个系统的对象类均为互不包含的独 立对象类,则将不可避免出现对象属性和行为的重复 冗余,并且这种无层次关系的对象类既不符合现实世 界的对象关系,也使对象类的定义、创建、使用和维 护复杂化。 继承为代码重用和建立类定义的层次结构提供方便 有效的手段。 例如在一个公司的管理软件设计中需要定义一个客 户类 Customer 和雇员类 Employee:,class Customer private: char name15; / 姓名 int age; / 年龄 char sex8; / 性别 double income; / 收入 public: void print(); / 显示输出状态 ;,class Employment private: char name15; / 姓名 int age; / 年龄 char sex8; / 性别 char department20; / 部门 double salary; / 工资 public: void print(); / 显示输出状态 ;,比较两个类的定义,不难发现,两个类的数据成员 和成员函数有许多相同之处。显然,如此定义两个 类,造成的代码重复是不可避免的。 如果将 Customer 和 Employee 类定义中的相同成员 抽取出来,定义一个新类 Person: class Person private: char name15; / 姓名 int age; / 年龄 char sex8; / 性别 public: void print(); / 显示输出状态 ;,Customer 和 Employee 都定义为 Person 的派生类, 那些在 Person 中已经定义的共同数据成员在 Customer 和 Employee 中就不需要再定义了,只需要在各自的定 义中增加自己的独有数据成员;而成员函数 print 也只 需要在 Person 所定义的行为操作基础上重新定义自己 的行为操作。 class Customer : public Person private: double income; / 收入 public: void print(); / 显示输出状态 ;,class Employee : public Person private: char department20; / 部门 double salary; / 工资 public: void print(); / 显示输出状态 ; 显然通过继承可以从基类 Person 派生出一组具有层 次结构的新类,构成一个公司管理系统的主要对象类 型。例如:,使用继承机制和方法设计和建造类定义的层次结构 对于建立一个面向对象的软件系统是不可缺少的。返回,Person,5.2 派生类的定义方法 定义派生类的一般形式: class 派生类名 : 派生方式 基类名 派生类的新增加成员和基类成员的新定义 ; 例如:基类 Person 和派生类的定义 class Person private: char name15; / 姓名 int age; / 年龄 char sex8; / 性别 public: void print(); / 显示输出状态 ;,class Customer : public Person private: double income; / 新增加的数据成员“收入” public: void print(); / 重新定义基类的“显示输出状态” ; 从形式上比较,派生类定义与非派生类定义的差别 仅在于定义首行中由 “:” 引出的派生表达式。其中: 派生方式:指明派生类继承基类成员的方式,方式 的种类有 public、private 和 protected。如果不指明方式名,则缺省指定派生方式为 private。 基类名:指明派生类所继承的类。,在 Java 中类的派生又被称为类的扩展,这种扩展相 当于 C+ 中的公有(public)派生,其一般形式如下: class 派生类名 :extends 基类名 派生类的新增加成员和基类成员的新定义 ,5.2.1 派生类的构成 派生类的构成可以有下图示意:,派生类名,例如整数链表类 list 的定义: 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); / 在表中查找一个结点 ;,一个链表结构的整数集合可以看成是不含重复元素 的特殊整数链表,因此整数集合类可以从整数链表类 派生。整数集合类在继承了整数链表类的所有成员的 基础上,需要新增加一个能指示集合中元素个数的数 据成员,同时还需要重新定义整数链表类的插入操作 insert,禁止重复元素被插入。 class set : public list int card; / 集合中的元素个数 public: bool insert(int); / 重新定义插入函数 ; 返回,5.3 派生类成员的访问属性 类成员的访问属性是指类成员的被访问权限,这种 权限随着作用域的变化而变化的,派生类成员的访问 属性也分为类内访问属性和类外访问属性两种情况。 1 类内访问属性 由于派生类的成员分为继承的基类成员和自身的新 增成员两种,这两种成员的类内访问属性是有所区 别的。 基类成员的访问属性 封装性所限定的类成员类外访问权限确定了基类 成员在派生类定义中被访问的限定原则:, 私有成员:不允许被访问,与派生类从基类 的继承方式无关。 公有成员:允许被访问,与派生类从基类的 继承方式无关。 新增成员的访问属性 所有的新增成员均允许被访问,与新增成员被设 定的访问属性(公有或私有)无关。 2 类外访问属性 类成员的类外访问是指在类对象定义域外访问对象 的类成员。因此,派生类成员在类定义中声明的访 问属性确定了派生类成员的类外访问属性:, 基类成员的访问属性 私有成员:不允许被访问,与派生类从基类 的继承方式无关。 公有成员:依据继承方式的不同,在基类中 被设定的公有属性会发生不同的变化。 私有继承:基类的公有成员变为派生类的 私有成员,因此在类外不允许被访问。 公有继承:基类的公有成员在派生类中仍 保持公有属性,因此在类外允许被访问。 新增加成员的访问属性 类成员在类定义中被声明的访问属性确定了类成 员的类外访问属性。,类外,例5-1 包含了两个类:由 C+ 标准模板库提供的 string 类具有丰富的字符串操作功能。edit_string 类是 从 string 类派生的,它在继承 string 类功能的基 础上增加了数据成员 光标和实现在光标处 的进行插入、替换、删除等文本编辑功能。 注意,派生类的成员名支配基类的成员名。 下面的类图描述了 string 和 edit_string 之间的派生层次 结构。,返回,5.4 派生类的构造函数和析构函数 5.4.1 派生类的构造函数 与一般非派生类相同,系统会为派生类定义一个缺 省(无参数、无显式初始化表、无数据成员初始化代 码)构造函数用于完成派生类对象创建时的内存分配 操作。但如果在派生类对象创建时需要实现以下两种 操作或其中之一,就无法使用缺省构造函数完成。 派生类对象的直接基类部分创建需要传递参数。 派生类对象的新数据成员需要通过参数传递初值。 为了满足上述对象创建操作的需要,就必须显式定义 派生类构造函数。,派生类构造函数声明和定义的一般形式: 注意: 构造函数名后面的参数表列中包含了初始化表中创 建对象的基类部分、新增数据成员和在函数体中为 新数据成员赋初始值所需要的全部参数。,构造函数名(参数表列);,类名:构造函数名(参数表列),: 基类构造函数名(参数子表列), 新数据成员名1(参数子表列), 新数据成员名n(参数子表列), 其他初始化代码 , 初始化表中创建对象的基类部分的表达式必须使用 基类构造函数名调用基类构造函数,而创建数据成 员表达式必须使用数据成员名调用数据成员类的构 造函数。 派生类构造函数的执行顺序:,基类构造函数,对象成员1 类构造函数,派生类构造 函数定义体,对象成员n 类构造函数,5.4.2 派生类的析构函数 与一般非派生类相同,系统会为派生类定义一个缺 省(无数据成员的清理代码)析构函数用于完成派生 类对象撤消时的内存回收操作。但如果在派生类对象 撤消时需要对某些新增数据成员进行内存回收之前的 清理操作(例如,指针数据成员所指向的动态内存的 回收),就无法使用缺省析构函数完成。为了满足上 述对象数据成员清理操作的需要,就必须显式定义派 生类构造函数。,析构函数的执行顺序:,派生类析构 函数定义体,对象成员n 类析构函数,基类析构函数,对象成员1 类析构函数,5.4.3 派生类应用的实例 例5-2 中定义了一个人员类 person,并以 person 为基类 派生定义了学生类 student 和教师类 teacher。另 外在学生类 student 中还包含了一个 teacher 类对 象作为描述学生班主任的数据成员。三个类都分 别定义两个构造函数(其中一个有参数表列,另 一个无参数表列)和一个析构函数。通过不同形 式的 student 类对象和 teacher 类对象定义表达式 所导致的相应类的不同构造函数的调用,验证派 生类对象创建和撤消中,基类构造函数、数据成 员类构造函数和派生类构造函数的调用顺序。,几点讨论: 1 如果派生类构造函数定义中无显式初始化表,则意 味着派生类对象的基类部分创建时,调用基类构造 函数无须参数;新增数据成员创建时,调用相应数 据类构造函数也无须参数。因此,如果基类和相应 的数据类没有定义无参数或有缺省参数值的构造函 数,将会导致编译错误。由此可见,一般情况在类 的定义中保留一个无须传递参数的构造函数是十分 必要的,除非需要禁止无参数创建类的对象。,无显式初始化表的派生类构造函数的一般形式: 系统的缺省构造函数是这种形式的一个特例,即无 参数,无显式初始化表和空定义体的类构造函数。,类名:构造函数名(参数表列) 新增数据成员赋初始值代码 ,类名:构造函数名() ,2 一般情况下,类数据成员的赋初始值操作均可以在 数据成员创建(分配内存)的同时进行,因此可以 通过初始化表同时完成数据成员的创建和赋初始值 操作。在这种情况下,如果对数据成员不需要其他 创建之后的初始化操作,就可能出现具有空定义体 的构造函数。 具有空定义体的构造函数的一般形式:,类名:构造函数名(参数表列),: 基类构造函数名(参数子表列), 新数据成员名1(参数子表列), 新数据成员名n(参数子表列), ,3 在多层次派生类构造函数的初始化表中的基类部分 表达式一般只涉及直接基类和新增数据成员的创建 和初始化操作,而间接基类的创建和初始化操作则 由直接基类的构造函数定义完成。这种分层次的构 造定义有利于简化程序编码和提高源代码的可读 性。当然,在某些特殊情况下,为了满足某种特定 要求,也允许在派生类构造函数的初始化表中对间 接基类部分进行必要的创建和初始化操作(在多重 继承将介绍这种情况的实例),但不提倡滥用。,例5-3 用高斯消元法来求线性方程组。 1 问题分析 所谓高斯消元法就是通过线性方程组的系数矩阵对 方程组进行一系列等价变换,使得变换后的系数矩 阵为一个对角线元素均为 1 的三角矩阵,然后通过 逐步回代,求得方程组的解。例如,下面的三元一 次方程组: 2x + 4y + 5z = 55 -2x + 5y 2z = 20 5x + 5y z = 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,1 1 -0.2 16.2 0 7 -2.4 52.4 2 4 5 55,1 1 -0.2 16.2 0 7 -2.4 52.4 0 2 5.4 22.6,1 1 -0.2 16.2 0 1 -0.34 7.5 0 2 5.4 22.6,1 1 -0.2 16.2 0 1 -0.34 7.5 0 0 6.1 7.6,1 1 -0.2 16.2 0 1 -0.34 7.5 0 0 1 1.2,z = 1.2 y = 7.5+1.2*0.34 = 7.9 x = 16.27.9+0.2*1.2 = 8.5,回代求出方程组的解:,为实现上述操作功能,需要定义了矩阵类 matrix 作 为对线性方程的系数矩阵进行操作的基类,它所提供 的操作功能: 构造函数:根据指定的行和列构造相应的矩阵; 重载调用运算符 operator():根据索引的行、列值, 引用相应的矩阵元素; 输出显示函数:格式显示矩阵的全部元素值。,线性方程组类 lineqns 从 matrix 派生,主要操作有: 构造函数:用传递的方程个数和解进行初始化; 参数产生:产生方程组的各变量系数值和常量值, 从而构造方程组; 高斯求解函数:使用消元法求解方程组。 lineqns 和 matrix 派生关系:,matrix,lineqns,2 详细设计 类设计 matrix 类 类定义: class matrix short rows, cols; double *elems; public: matrix(short rows, short cols); matrix(); double, lineqns 类 类定义: class lineqns public matrix int neqns; double *solution; public: lineqns(int n, double *soln); lineqns() void generate(int coef); void solve(); ; 算法描述:, generate :用于产生方程的变元系数和常数 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 endfor END, solve :高斯消元求解 使用 N-S 流程图描述,图中的符号约定说明: diag 系数矩阵主对角线元素的行、列标识; piv 同列系数中最大元素值的行标识; neqns 方程组中的方程个数标识; r 行序号循环标识; c 列序号循环标识; factor 用于消去指定系数元的变换因子标识; print 显示系数矩阵的功能函数标识; soln 线性方程组的解矩阵标识; sum 求解过程中累加和标识。 a 系数标识。 C 常数标识。,piv = diag,r = r+1,piv = r,终止求解,并退出,第diag 行中的系数和常数逐个除以系数a(diag,diag),使系数a(diag,diag)=1,factor = -系数a(r, diag), 使系数a(r, diag) - a(r, diag) = 0,r = r+1,调用(基类的)print 成员函数输出消元后的方程组系数矩阵, 类应用 main 函数的算法: 返回,创建存放方程解的数组:soln = new doubleneqns,从系数矩阵直接获取最后一个变元的解:solnneqns-1 = a(neqns,neqns+1),输入线性方程组的方程个数和相应的解,根据指定的方程个数和解,创建 lineqns 类对象 eqn,调用 eqn.generate 为方程组设置系数和常数,调用 eqn.print 输出显示所建方程组的系数矩阵,调用 eqn.solve 求解所建线性方程组,sum = 0,solnr-1 = 常数C(r, neqns+1) - sum,5.5 对派生类成员访问属性的进一步讨论 前面我们已经对派生类成员的基本访问属性进行了 讨论,从讨论中我们发现,要使派生类与继承的基类 成员更加 “无缝” 结合、更加灵活可控地继承、有两个 问题还需要进一步讨论并加以解决。这两个问题是: 基类私有成员在派生类中不可直接访问性与派生类 新增成员函数需要能直接访问基类私有成员提高行 为操作效率和灵活性之间的矛盾。 继承方式对基类成员的设定访问属性修改的局限性 与派生类期望能更加灵活、可控制地从基类继承之 间的矛盾。,5.5.1 保护成员与保护继承 1 类成员的保护访问属性 解决基类私有成员在派生类中只能通过基类的接口 (公有成员函数)访问而不允许直接访问的思路 是:在不破坏派生类封装性的前提下,“突破”基类 的封装边界。解决的方法之一是增加一种新的类成 员访问属性 保护访问属性: 一般形式:protected 类型名 数据成员名; protected 类型名 成员函数名(参数表列); 访问权限:可以在类内和派生类内被访问,而在 类外和派生类外不允许被访问。, 访问权限的继承: 私有派生:基类的保护成员在派生类中将变 成私有成员。 公有派生:基类的保护成员在派生类中保持 保护访问属性。 具有保护访问属性的类成员称为保护成员。将派生 类需要直接访问的基类私有成员定义为基类保护成 员,既可以提高这些基类成员在派生类内的访问效 率和方便性,又保持了这些类成员在派生类外不能 被直接访问的数据隐藏性。,例5-4 定义了能确定显示位置的基类 location,该类包 含两个整型的保护数据成员 x 和 y。由 location 公 有派生一个点类 point, 该派生类具有能在确定 的位置处显示、隐去和 移动到指定位置的操作 功能。再由 point 类私有派生一个圆类 circles,该 派生类继承了间接基类 location 和直接基类 point 的全部成员,并重新定义各项操作。,2 类派生的保护继承方式 类派生的继承方式的作用是确定了基类成员被继承 到派生类中成为派生类成员时,其访问属性被限定 修改的规则。增加保护继承方式的目的是使派生类 成员的类外访问属性与私有继承方式相同,而当派 生类被再次派生时,直接访问间接基类成员提供可 能性。 一般形式: class 派生类名 : protected 基类名 类成员定义代码 ;, 基类成员访问属性修改规则: 私有成员:与公有继承方式和私有继承方式 相同,在派生类内外均不允许被访问。 保护成员:基类的保护成员在派生类中保持 保护访问属性。 公有成员:基类的公有成员在派生类中变为 保护成员。 下面用图表来归纳和描述基类的 private,protected 和 public 三种类成员在以 private,protected 和 public 三种继承方式派生的新类中的访问属性的变化。, 私有派生方式继承,protected: public:,interCode,Name Address AreaCode phone,Person() Person() Person inputPerson() void prPerson(),protected: public:,Name Address AreaCode Phone Person inputPerson () void prPerson() department,yrsWork,Employee() Employee() int testYears(),class Person,class Employee:private Person, 保护派生方式继承,protected: public:,interCode,Name Address AreaCode phone,Person() Person() Person inputPerson() void prPerson(),protected: public:,Name Address AreaCode Phone Person inputPerson () void prPerson() custBalance,Costomer() Costomer() void PrtCust(),class Person,class Costomer:protected Person,custNum, 公有派生方式继承,protected: public:,interCode,Name Address AreaCode phone,Person() Person() Person inputPerson() void prPerson(),protected: public:,Name Address AreaCode Phone vendOwed,Vendor() Vendor() Person inputPerson () void prPerson() void PrtVend(),class Person,class Vendor:public Person,vendNum, 基类成员在派生类内外的访问属性一览表,5.5.2 派生友元类 如果希望基类的私有成员只在派生类中能被直接访 问,而不希望这种直接被访问的属性从派生类向下一 层次的派生类中延续,则在基类定义中将要派生的类 声明为基类的友元,即从基类派生友元类。当然,也 可以将基类的私有成员定义为保护成员,然后使用私 有继承方式定义派生类的方法得到相同效果。例如: class set; struct node int val; node *next; ;,class list node *elems; public: friend class set; ; class set : public list int card; public: set operator + (set,5.5.3 访问域声明 所谓访问域声明是在私有继承方式定义的派生类中 对基类的公有成员和保护成员进行声明,调整它们在 派生类中访问属性,使这些基类成员保持它们在基类 定义中设定的访问属性。 显然,在保护继承方式定义的派生类中,访问域声 明只对基类的公有成员有效,因为基类的保护成员在 派生类中已经保持了基类定义中原有访问属性。而在 公有继承方式定义的派生类中,访问域声明是没有意 义的,因为基类的公有成员和保护成员在派生类中都 保持了基类定义中原有访问属性。,使用访问域声明可以有效地控制在派生类外,基类 的某些公有成员可以被访问,而某些公有成员被隐 藏。还可以使派生类能够地向下一层次的派生类有选 择地提供其基类的保护成员和公有成员。 对基类成员进行访问域声明必须遵守以下规则: 1 访问域声明仅能调整对基类成员名,而不能为基类 成员重新说明类型,即便所说明的类型与基类成员 的原有类型相同,也是不允许的。如果声明的是成 员函数,则声明的也只是函数名而不准带有参数。 例如:,class 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); / 错误 ;,正确的访问域声明如下: class y : x public: x:b; / 正确 x:f; / 正确 ; 2 访问域声明只能使基类的保护和公有成员在派生类 中保持它们在基类定义的设定的访问属性,而不能 改变基类的私有成员在派生类中的访问属性,任何 试图这样做的行为都被视为破坏封装性,是非法 的。例如:,class x int a; public: ; class y : x x:a; / 非法 public: ;,3 访问域声明仅用于在派生类中保持基类(公有或保护)成员的原有访问属性,不允许修改它们的访问属性。也就是说,基类的保护成员只能在派生类的保护段中进行声明;而基类的公有成员只能在派生类的公有段中进行声明。例如: 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(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(double) 是基类保护成员,试图改变该成员函数 的保护访问属性也是不允许的。,5 如果在派生类中具有与基类中同名的类成员,则基 类中的此成员不允许在派生类中进行访问域声明,否则将产生二义性错误。例如: class x public: f(); f(int); f(char*); ;,class y : x public: void f(float); x:f; / 二义性错误 ; 返回,5.6 多继承 5.6.1 多继承的概念 所谓多继承就是一个新类是从多个基类中派生而成 的。例如,在一个面向对象的图形用户界面中,为用 户界面提供的窗口、滚动条、对话框和各种操作按钮 都是通过类对象来实现的。如果希望在既有类的基础 上定义一个新类具有两个以上既有类的全部属性和操 作,就可以通过多继承的方法完成。如,可以由窗口 类和滚动条类共同派生出一个可滚动的窗口新类。,在有些情况下,可以使用类的组合关系,即将一些 既有类对象定义为新类的成员来实现与多继承派生 的新类相同的功能。例如,同样是定义可滚动的窗 口类,可以以窗口类为基类单继承派生,并将滚动 条类对象作为新类的新增成员。在新类中窗口类的 属性和行为被继承,而滚动条的属性和行为并没有 被继承,对滚动条对象成员的使用是通过新类的内 部消息实现的。,5.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 均为公有派生,如果多继承派生的多个基类中有同名的类成员,则 派生类定义中访问该同名成员时必须使用不同基类名 和名域运算符加以区别,否则将导致二义性。例如: class x protected: int a; / 同名数据成员 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:make(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 / 定义窗口类 public: 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 / 定义派生类 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 派生类构造函数和析构函数的执行顺序 与单继承派生类对象创建时构造函数的调用顺序相 同,先构造基类部分,再构造派生类。多个基类构 造函数的执行顺序与定义时从左到右的基类排列顺 序一致。派生类新增数据成员对象的构造函数的执 行顺序也与定义时的先后排列顺序一致。,基类 1 构造函数,基类 n 构造函数,成员对象 1 构造函数,成员对象 n 构造函数,派生类构造函数定义体,派生类对象撤消时,析构函数的调用顺序与对象创 建时构造函数的调用顺序相反。,基类 1 析构函数,基类 n 析构函数,成员对象1 析构函数,成员对象 n 析构函数,派生类析构函数定义体,3 多继承派生类编程实例 例5-5 在屏幕上显示一个带有字符串的圆。 1 问题分析 定义一个圆类 circles 和字符串类 gmessage 分别用于 在屏幕的指定位置按设定半径绘制圆和在屏幕的指 定位置显示特定的字符串。为了使所显示的圆和字 符串的位置紧密相关,采用多继承机制,从 circles 和 gmessage 派生出新类 mcircle 用于完成本例的最 终需求。另外为了更好地体现面向对象的设计思 想,再定义一个 point 类,用于确定所要显示的圆和 字符串的位置。因此 circles 和 gmessage 都应该从 point 派生。, 类图描述,例5-6 一个远程网络中记录两台机器之间的平均传输时 间如下表所示: 编写一个能按照网络通讯的出发地和目的地输 入、保存传输时间,并能按照上述表格形式输出 已经保存的网络通讯时间。,1 问题分析: 使用矩阵结构保存二维表格数据是最为恰当的。但 由于本需求中二维表中的行、列位置的不是整数下 标而是字符串,所以描述该表格不能直接使用通用 矩阵类,而需要使用以字符串指示位置的特殊矩阵 类。其中,作为行、列下标的字符串可以通过向量 类将字符串映射为指示通用矩阵元素的整数下标。 显然该特殊矩阵是由向量和通用矩阵协同工作实现 的。为此,定义通用矩阵类 Matrix 用于完成通讯网 络表的基础操作,再定义向量类 AssocVec 用于将 通讯地址描述串转换为矩阵元素下标,而网络通讯 传输表类 Table 可以从这两个类派生。这些类之间 的关系如下图所示:,2 类的设计实现 向量结构 VecElem : struct VecElem char* index; / 索引字符串 int value; / 映射变量 ;, 辅助向量类 AssocVec: class AssocVec public: AssocVec(int dim); / 构造函数 AssoVec(); / 析构函数 int,下标运算符 AssocVec:operator (char* idx) 算法:,索引字串复制到elemsused+1.index 使elemsused+1.value = used+1, 并返回elemsused+1.value的引用,返回值为-1 的静态哑变 量引用,指 示调用失败,Yes,No,Yes,No,根据参数指定 的索引字串查 询已有向量, 返回匹配映射 变量的引用, 通用矩阵类 Matrix: class Matrix public: Matrix(int rows, int cols); / 构造函数 Matrix(); / 析构函数 double / 二维矩阵的行、列索引 ;,调用运算符 Matrix:operator () (short r, short c) 的算 法:,返回参数指定的要访问的矩阵元素: elems(r-1)*(c-1)的引用,返回静态哑 变量的引用,Yes,No, 网络通讯传输表类 Table: class Table : AssocVec, Matrix public: Table(short entries); / 构造函数 double ;,调用运算符 Table:operator () (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 : 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; ,base,base,base1,base2,derived,2 虚基类的概念 显然,解决上述二义性问题的办法是使派生类对象 层次结构中只有一个间接基类 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, public base2 public: derived() cout “derived a = “ a endl; / 访问 base:a 具有唯一性 ;,又例如,在定义图形用户界面时,可以考虑定义这 样三 个类: class port ; 图形显示区 class region ; 屏幕上的任意区域 class menu ; 菜单-选项的集合,base,Base1,Base2,derived,从这些类中派生出两个新类:window 类和 tools 类: class window : public port, public region ; class tools : public port, public menu ; 这两个派生类还可以再派生出新类: class appwind : public window, public tools ; 派生 appwind 的目的是为了得到一个带工具条的窗 口, 但根据上述定义得到的结果(如下图所示)是 工具条和窗口分别拥有自己的显示区,这并不是所 希望的。,所希望得到的是这两个对象为一个整体,只包含一 个显示区,即工具条是包含在窗口之内的。要做到 这一点需要在窗口类 window 和工具条类 tools 的定 义中将显示区类 port 定义为虚基类。因此 tools 和 window 的定义变为如下形式: class window : virtual public 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,+, 任何一个类都可以在作为某些派生类的虚基类的同时,又作为另一些派生类的非虚基
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 高职低压电工考试题库及答案
- 国网招聘电工考试题库及答案
- 海底世界的探险记叙事文11篇范文
- 跨部门沟通协作标准化工具提升沟通效率
- 2025年人才服务行业人才培养与职业发展研究报告及未来发展趋势预测
- 2025年区块链技术应用在金融行业研究报告及未来发展趋势预测
- 供应链管理供应商评价模型
- 2025年媒体行业数字媒体与在线内容消费研究报告及未来发展趋势预测
- 2025年新能源行业绿色能源发展路径研究报告及未来发展趋势预测
- 2025年烟草行业绿色烟草生产与消费研究报告及未来发展趋势预测
- 二十届四中全会测试题及答案单选题(20题)
- 2025重庆双福农产品批发市场有限公司招聘综合办公室文员、冻库管理员、招商员等岗位22人备考考试试题及答案解析
- 系统熟悉级航机务培训中心
- 密闭空间作业职业危害防护规范
- 主题班会《知法-学法-与法同行》课件
- 化学品泄漏事故现场处置应急操作卡
- 《思想道德与法治》 课件 第四章 明确价值要求 践行价值准则
- 《黄金分割》 完整版课件
- JJF(电子)0036-2019 示波器电流探头校准规范-(高清现行)
- 《CorelDRAW-X7设计与制作》全书教案完整版电子教案整本书教案最全单元教学设计
- 铁路运输危险源辨识及风险评介控制措施表
评论
0/150
提交评论