900 likes | 1.02k Views
继承与类的派生. 继承的概念. 目前,不加修改地直接复用已有软件比较困难。 已有软件的功能与新软件所需要的功能总是有差别的。解决这个差别有下面的途径: 修改已有软件的源代码, 它的缺点是: 需读懂源代码 可靠性差、易出错 源代码难以获得 继承( Inheritance ):在定义一个新的类时,先把一个或多个已有类的功能全部包含进来,然后再给出新功能的定义或对已有类的某些功能重新定义。. 根据软件需求从软件所要模拟的现实世界中 抽象 出 组成软件系统的 对象类 是面向对象程序设计的 基础 。 面向对象的 封装性 使这些 对象类 的属性和行为细节得
E N D
继承的概念 • 目前,不加修改地直接复用已有软件比较困难。已有软件的功能与新软件所需要的功能总是有差别的。解决这个差别有下面的途径: • 修改已有软件的源代码,它的缺点是: • 需读懂源代码 • 可靠性差、易出错 • 源代码难以获得 • 继承(Inheritance):在定义一个新的类时,先把一个或多个已有类的功能全部包含进来,然后再给出新功能的定义或对已有类的某些功能重新定义。
根据软件需求从软件所要模拟的现实世界中抽象出根据软件需求从软件所要模拟的现实世界中抽象出 组成软件系统的对象类是面向对象程序设计的基础。 面向对象的封装性使这些对象类的属性和行为细节得 到了合理的保护和隐藏,并为类对象之间的通讯(方 法调用)提供了安全方便的接口。 在封装性的基础上,面向对象的继承性允许一个对 象类包含另一个或几个对象类的属性和行为,并使它 们成为自己的属性和行为,充分地反映了现实世界中 对象类之间的层次结构,为程序的代码重用提供了方 便、有效的实现机制。
在面向对象程序设计中,借助继承性的实现方法,在面向对象程序设计中,借助继承性的实现方法, 允许在既有类的基础上定义新类。被定义的新类可以 从一个或多个既有类中继承属性和行为,并允许重新 定义这些既有类中原有的属性和行为,还允许为新类 增加新的属性和行为,从而形成了类的建造层次。 既有类被称为基类或父类 新类被称为派生类、导出类或子类。C++分为: 单一继承:一个派生类仅有一个基类; 多重继承:一个派生类有两个或两个以上的基类。
图中的箭头是从派生类指向基类。 • – 人类 :描述人的共有特征 ( 如编号、姓名、年龄、 性别等 ) 。 “ ” – 学生类: 单一继承 人类 ,并增加描述学生特征的 信息 ( 如专业和课程等 ) 。 “ ” – 职工类: 单一继承 人类 , 并增加描述职工特征的信 息 ( 如工作部门、所教课 程等 ) 。 – 在职研究生类: 多重继承 “ ” “ ” 学生类 和 职工类 。 – 类族: 人类、学生类、职 工类、在职研究生类。
派生类的概念 继承是对象类之间的一种包含关系,这种包含关系 是通过对象类的建造层次关系实现的。因此,具有继 承关系的类之间必定拥有以下基本性质: ① 类间的共享特性; ② 类间的细微区别; ③ 类间的层次结构。
运输汽车 专用汽车 客车 货车 消防车 洒水车 汽车 例如: 简单的汽车分类图
使用继承的必要性 试想如果组成一个系统的对象类均为互不包含的独 立对象类,则将不可避免出现对象属性和行为的重复 冗余,并且这种无层次关系的对象类既不符合现实世 界的对象关系,也使对象类的定义、创建、使用和维 护复杂化。 继承为代码重用和建立类定义的层次结构提供方便 有效的手段。 例如在一个公司的管理软件设计中需要定义一个客 户类 Customer和雇员类 Employee:
class Customer { private: char name[15]; // 姓名 int age; // 年龄 char sex[8]; // 性别 double income; // 收入 public: void print(); // 显示输出状态 };
class Employment { private: char name[15]; // 姓名 int age; // 年龄 char sex[8]; // 性别 char department[20]; // 部门 double salary; // 工资 public: void print(); // 显示输出状态 };
比较两个类的定义,不难发现,两个类的数据成员比较两个类的定义,不难发现,两个类的数据成员 和成员函数有许多相同之处。显然,如此定义两个 类,造成的代码重复是不可避免的。 如果将 Customer和 Employee类定义中的相同成员 抽取出来,定义一个新类 Person: class Person { private: char name[15]; // 姓名 int age; // 年龄 char sex[8]; // 性别 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 department[20]; // 部门 double salary; // 工资 public: void print(); // 显示输出状态 }; 显然通过继承可以从基类Person派生出一组具有层 次结构的新类,构成一个公司管理系统的主要对象类 型。例如:
Employee Customer Vendor Salaried Hourly Partner Client PartTime FullTime 使用继承机制和方法设计和建造类定义的层次结构 对于建立一个面向对象的软件系统是不可缺少的。返回 Person
单一继承 已定义的类名 • 格式: class ClassName :【Access】 BaseClassName { 派 … // 派生类中新增成员,可为空 生 }; 类 规定基类成员在派生类中的访问权限,可取 : 的 • public : 公有派生 类 • private : 私有派生 名 • protected : 保护派生 • 缺省: private( 对于结构体则为 public)
例如:基类 Person 和派生类的定义 class Person { private: char name[15]; // 姓名 int age; // 年龄 char sex[8]; // 性别 public: void print(); // 显示输出状态 };
class Customer : public Person { private: double income; // 新增加的数据成员“收入” public: void print(); // 重新定义基类的“显示输出状态” }; 从形式上比较,派生类定义与非派生类定义的差别 仅在于定义首行中由 “:” 引出的派生表达式。其中: ① 派生方式:指明派生类继承基类成员的方式,方式 的种类有 public、private和 protected。如果不指明方式名,则缺省指定派生方式为 private。 ② 基类名:指明派生类所继承的类。
基类的所有成员 派生类的新增加成员 派生类的构成 派生类的构成可以有下图示意: 派生类名
例如整数链表类 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); // 重新定义插入函数 … }; 返回
派生类成员的访问属性 派生类成员的访问属性我们分为类内访问属性和类外访问属性两种情况讨论。 1 类内访问属性 由于派生类的成员分为继承的基类成员和自身的新 增成员两种,这两种成员的类内访问属性是有所区 别的。 ⑴ 基类成员的访问属性 基类成员在派生类定义中被访问的限定原则: ① 私有成员:不允许被访问,与派生类从基类的继承方式无关。 ② 公有成员:允许被访问,与派生类从基类的继承方式无关。
⑵ 新增成员(自己的)的访问属性 所有的新增成员均允许被访问,与新增成员被设定的访问属性(公有或私有)无关。 2 类外访问属性 类成员的类外访问是指在类对象定义域外访问对象 的成员。因此,派生类成员在类定义中声明的访 问属性确定了派生类成员的类外访问属性:
⑴ 基类成员的访问属性 ① 私有成员:不允许被访问,与派生类从基类 的继承方式无关。 ② 公有成员:依据继承方式的不同,在基类中 被设定的公有属性会发生不同的变化。 ·私有继承:基类的公有成员变为派生类的 私有成员,因此在类外不允许被访问。 ·公有继承:基类的公有成员在派生类中仍 保持公有属性,因此在类外允许被访问。 ⑵ 新增加成员的访问属性 类成员在类定义中被声明的访问属性确定了类成 员的类外访问属性。
derived2 类内 classbase NO NO public private NO NO OK classderived1:base NO NO OK public private OK NO classderived2: publicderived1 public private NO OK 类外
派生类的构造函数和析构函数 1 派生类的构造函数 与一般非派生类相同,系统会为派生类定义一个缺 省(无参数、无显式初始化表、无数据成员初始化代 码)构造函数用于完成派生类对象创建时的内存分配 操作。但如果在派生类对象创建时需要实现以下两种 操作或其中之一,就无法使用缺省构造函数完成。 ① 派生类对象的直接基类部分创建需要传递参数。 ②派生类对象的新数据成员需要通过参数传递初值。 为了满足上述对象创建操作的需要,就必须显式定义 派生类构造函数。
派生类构造函数声明和定义的一般形式: 注意: ① 构造函数名后面的参数表列中包含了初始化表中创 建对象的基类部分、新增数据成员和在函数体中为 新数据成员赋初始值所需要的全部参数。 构造函数名(参数表列); :基类构造函数名(参数表列), 新数据成员名1(参数表列), … 新数据成员名n(参数表列) 类名::构造函数名(参数表列) { 其他初始化代码}
② 初始化表中创建对象的基类部分的表达式必须使用 基类构造函数名调用基类构造函数,而创建数据成 员表达式必须使用数据成员名调用数据成员类的构 造函数。 派生类构造函数的执行顺序: 基类构造函数 对象成员1 类构造函数 对象成员n 类构造函数 派生类构造 函数定义体
2 派生类的析构函数 与一般非派生类相同,系统会为派生类定义一个缺 省(无数据成员的清理代码)析构函数用于完成派生 类对象撤消时的内存回收操作。但如果在派生类对象 撤消时需要对某些新增数据成员进行内存回收之前的 清理操作(例如,指针数据成员所指向的动态内存的 回收),就无法使用缺省析构函数完成。为了满足上 述对象数据成员清理操作的需要,就必须显式定义派 生类析构函数。析构函数的执行顺序: 派生类析构 函数定义体 对象成员n 类析构函数 对象成员1 类析构函数 基类析构函数
• 例 派生类的构造函数和析构函数。 # include< iostream > using namespace std; class B1{ protected: int x; public: B1( int x){ this - >x=x; cout <<" 基类 B1 的构造函数 ! \ n"; } ~B1( ){ cout <<" 基类 B1 的析构函数 ! \ n"; } }; class B2{ protected: int y; public: B2( int y){ this - >y=y; cout <<" 基类 B2 的构造函数 ! \ n"; } ~B2( ){ cout <<" 基类 B2 的析构函数 ! \ n"; } };
• 基类构造函数的调用顺序: § 与继承基类的顺序有关 § 与初始化成员列表中的顺 class D: public B1,public B2 { 序无关 protected: int z; public: D( int x, int y, int z): B1(x),B2(y) { this - >z=z; cout <<" 派生类 D 的构造函数 ! \ n"; } ~D(){ cout <<" 派生类 D 的析构函数 ! \ n"; } }; int main(void){ D d(1,2,3) ; return 0; } 程序运行结果: • 说明派生类的对象: 先调用各基类的 基类 B1 的构造函数 ! 构造函数,后执行派生类的构造函数。 基类 B2 的构造函数 ! 若某个基类仍是派生类,则这种调用 派生类 D 的构造函数 ! 基类构造函数的过程递归进行。 派生类 D 的析构函数 ! • 撤消派生类的对象: 析构函数的调用 基类 B2 的析构函数 ! 顺序正好与构造函数的顺序相反。 基类 B1 的析构函数 !
• 派生类含对象成员:其构造函数的初始化成员列表既要列举基 类成员的构造函数,又要列举对象成员的构造函数。 • 例 派生类中包含对象成员。 # include< iostream > using namespace std; class B1{ protected: int x; public: B1( int x){ this - >x=x; cout <<" 基类 B1 的构造函数 ! \ n"; } ~B1( ){ cout <<" 基类 B1 的析构函数 ! \ n"; } }; class B2{ protected: int y; public: B2( int y){ this - >y=y; cout <<" 基类 B2 的构造函数 ! \ n"; } ~B2( ){ cout <<" 基类 B2 的析构函数 ! \ n"; } };
• 对象成员的构造函数的调用 • 基类成员的初始化必须 顺序与对象成员的说明顺序 使用基类名 有关,而与其在初始化成员 class D: public B1,public B2 { 列表中的顺序无关。 int z; • 对象成员的初始化必须使用 B1 b1,b2 ; 对象名 public: D( int x, int y, int z): B1(x),B2(y) , b1(2),b2(x+y) { this - >z=z; cout <<" 派生类 D 的构造函数 ! \ n"; } ~D( ){ cout <<" 派生类 D 的析构函数 ! \ n"; } • 程序运行结果: }; • 从结果看: 在创建类 基类 B1 的构造函数 ! int main(void) D 的对象 d 时,先调用 基类 B2 的构造函数 ! { D d(1,2,3) ; 基类 B1 的构造函数 ! 基类的构造函数,再 return 0; 基类 B1 的构造函数 ! 调用对象成员的构造 派生类 D 的构造函数 ! } 函数,最后执行派生 派生类 D 的析构函数 ! 类的构造函数。 基类 B1 的析构函数 ! 基类 B1 的析构函数 ! • 问题: 请写出产生上述输出结果的基类 基类 B2 的析构函数 ! 成员名或对象成员名。 基类 B1 的析构函数 !
几点讨论: 1 如果派生类构造函数定义中无显式初始化表,则意 味着派生类对象的基类部分创建时,调用基类构造 函数无须参数;新增数据成员创建时,调用相应数 据类构造函数也无须参数。因此,如果基类和相应 的数据类没有定义无参数或有缺省参数值的构造函 数,将会导致编译错误。由此可见,一般情况在类 的定义中保留一个无须传递参数的构造函数是十分 必要的,除非需要禁止无参数创建类的对象。
无显式初始化表的派生类构造函数的一般形式:无显式初始化表的派生类构造函数的一般形式: 系统的缺省构造函数是这种形式的一个特例,即无 参数,无显式初始化表和空定义体的类构造函数。 类名::构造函数名(参数表列) { 新增数据成员赋初始值代码} 类名::构造函数名() { }
2 一般情况下,类数据成员的赋初始值操作均可以在 数据成员创建(分配内存)的同时进行,因此可以 通过初始化表同时完成数据成员的创建和赋初始值 操作。在这种情况下,如果对数据成员不需要其他 创建之后的初始化操作,就可能出现具有空定义体 的构造函数。 具有空定义体的构造函数的一般形式: 类名::构造函数名(参数表列) :基类构造函数名(参数子表列), 新数据成员名1(参数子表列), … 新数据成员名n(参数子表列) { }
3 在多层次派生类构造函数的初始化表中的基类部分 表达式一般只涉及直接基类和新增数据成员的创建 和初始化操作,而间接基类的创建和初始化操作则 由直接基类的构造函数定义完成。这种分层次的构 造定义有利于简化程序编码和提高源代码的可读 性。当然,在某些特殊情况下,为了满足某种特定 要求,也允许在派生类构造函数的初始化表中对间 接基类部分进行必要的创建和初始化操作,但不提倡滥用。
对派生类成员访问属性的进一步讨论 前面我们已经对派生类成员的基本访问属性进行了 讨论,从讨论中我们发现,要使派生类与继承的基类 成员更加 “无缝” 结合、更加灵活可控地继承、有两个 问题还需要进一步讨论并加以解决。这两个问题是: ⑴ 基类私有成员在派生类中不可直接访问性与派生类 新增成员函数需要能直接访问基类私有成员提高行 为操作效率和灵活性之间的矛盾。 ⑵ 继承方式对基类成员的设定访问属性修改的局限性 与派生类期望能更加灵活、可控制地从基类继承之 间的矛盾。
保护成员与保护继承 • 类成员的保护访问属性 • 解决基类私有成员在派生类中只能通过基类的接口 • (公有成员函数)访问而不允许直接访问的思路 • 是:在不破坏派生类封装性的前提下,“突破”基类的封装边界。解决的方法之一是增加一种新的类成员访问属性 —— 保护访问属性: • ⑴ 一般形式:protected 类型名 数据成员名; • protected 类型名 成员函数名(参数表列); • ⑵访问权限:可以在类内和派生类内被访问,而在 • 类外和派生类外不允许被访问。
⑶ 访问权限的继承: ① 私有派生:基类的保护成员在派生类中将变 成私有成员。 ② 公有派生:基类的保护成员在派生类中保持 保护访问属性。 具有保护访问属性的类成员称为保护成员。将派生 类需要直接访问的基类私有成员定义为基类保护成 员,既可以提高这些基类成员在派生类内的访问效 率和方便性,又保持了这些类成员在派生类外不能 被直接访问的数据隐藏性。
2 类派生的保护继承方式 类派生的继承方式的作用是确定了基类成员被继承 到派生类中成为派生类成员时,其访问属性被限定 修改的规则。增加保护继承方式的目的是使派生类 成员的类外访问属性与私有继承方式相同,而当派 生类被再次派生时,直接访问间接基类成员提供可 能性。 ⑴ 一般形式: class 派生类名: protected基类名 { 类成员定义代码};
⑵基类成员访问属性修改规则: ① 私有成员:与公有继承方式和私有继承方式 相同,在派生类内外均不允许被访问。 ② 保护成员:基类的保护成员在派生类中保持 保护访问属性。 ③ 公有成员:基类的公有成员在派生类中变为 保护成员。 下面用图表来归纳和描述基类的 private,protected 和 public三种类成员在以 private,protected和 public 三种继承方式派生的新类中的访问属性的变化。
⑴私有派生方式继承 class Person class Employee:private Person protected: public: interCode Name Address AreaCode Phone Person inputPerson () void prPerson() department protected: public: Name Address AreaCode phone Person() ~Person() Person inputPerson() void prPerson() yrsWork Employee() ~Employee() int testYears()
⑵保护派生方式继承 class Person class Costomer:protected Person protected: public: interCode protected: public: custNum Name Address AreaCode phone Name Address AreaCode Phone Person inputPerson () void prPerson() custBalance Person() ~Person() Person inputPerson() void prPerson() Costomer() ~Costomer() void PrtCust()
⑶公有派生方式继承 class Person class Vendor:public Person protected: public: interCode protected: public: vendNum Name Address AreaCode phone Name Address AreaCode Phone vendOwed Person() ~Person() Person inputPerson() void prPerson() Vendor() ~Vendor() Person inputPerson () void prPerson() void PrtVend()
• 例 公有继承。 # include< iostream > using namespace std; class Point { // 三维直角坐标点类 float x; • 保护成员具有双重作用: protected: Ø 对于派生类,它是公有的 float y ; Ø 对于其外部,它是私有的 public: float z; Point(float x,float y,float z) { this - >x=x; this - >y=y; this - >z=z; } void Setx (float x){ this - >x=x; } void Sety (float y){ this - >y=y; } float Getx ( ){ return x; } float Gety ( ){ return y; } void ShowP ( ){ cout <<'('<<x<<','<<y<<','<<z<<')'; } };
• 基类成员的初始化: 在派生类 的构造函数中用成员初始化列 class Sphere: public Point { 表调用基类的构造函数。 float radius;// 球的半径 public: Sphere(float x,float y,float z,float r) :Point(x,y,z) { radius=r; } void ShowS ( ) { cout <<'('<< Getx ( ) <<','<< y <<','<< z <<"),"<<radius<<' \ n'; } }; • 在公有派生类 Sphere 内: 直接访问基 类的保护成员 y 和公有成员 z , 但不能 int main(void) 直接访问基类的私有成员 x 。 若将 { Sphere s(1,2,3,4); Getx () 改为 x , 则将出现编译错误。 s. ShowS ( ); cout <<'('<<s. <<','<<s. ( )<<','<<s. <<") \ n"; Getx ( ) Gety z return 0; } • 在公有派生类 Sphere 外: 派生类对象 s 只能访问类中 的公有成员如 Getx ( ) 、 Gety ( ) 和 z 。 但不能访问类 中的私有成员如 x 、 radius 和保护成员如 y 。
私有派生 • 基类中公有成员和保护成员在私有派生类中均变为私 有的,在派生类中仍可直接访问,但在派生类之外均 不可直接访问。 • 基类中的私有成员在私有派生类中不可直接访问,当 然在派生类之外,更不直接访问。
• 例 私有继承。 # include< iostream > using namespace std; class Point { //三维直角坐标点类 float x; protected: float y; public: float z; Point(float x,float y,float z) { this - >x=x; this - >y=y; this - >z=z; } void Setx (float x){ this - >x=x; } void Sety (float y){ this - >y=y; } float Getx ( ){ return x; } float Gety ( ){ return y; } void ShowP ( ){ cout <<'('<<x<<','<<y<<','<<z<<')'; } };
class Sphere: private Point {// 私有继承 Point 类,派生 Sphere 类 float radius; // 球的半径 public: Sphere(float x,float y,float z,float r):Point(x,y,z) { radius=r; } void ShowS ( ) Getx ( ) y z { cout <<'('<< <<','<< <<','<< <<"),"<<radius<<' \ n'; } }; • 在私有派生类 Sphere 内: 基类的公有 和保护成员在派生类中均变为私有, int main(void) 仍可直接使用,如 y 和 z 。 { Sphere s(1,2,3,4); s. ShowS ( ) ; return 0; • 在私有派生类 Sphere 外: 派生类对象 s 只能直接 } 访问添加的公有成员 ShowS ( ) , 而不可直接访问 基类的公有成员 Getx ( ) 、 Gety ( ) 和 z 等。