
类的继承与派生谭浩强C.ppt
67页类的继承与派生 前面我们主要讨论了面向对象程序设计的第一个重要机制-数据的封装与隐藏特性而面向对象程序设计中另一个重要机制就是代码的可重用性,代码可重用性的特征实现的主要机制之一就是类的继承类的继承机制自动为一个类提供来自于另一个类的操作和数据结构,这使得程序员只需在新类中定义已有类中没有的成分来建立新类更为实际的意义就在于,类的继承机制给程序员提供了无限重复利用程序资源的一种有效途径通过C++语言中的继承机制,可以扩充和完善旧的程序设计以适应新的需求,这样不仅可以节省程序开发的时间和资源,并且为未来程序设计增添了新的资源 1.基类和派生类 2.单继承 3.多继承 4.虚基类 综上所述,理解继承是理解面向对象程序设计所有方面的关键所以,本章是整个面向对象程序设计中的重点内容通过本章的学习,主要理解与掌握基类和派生类、单继承、多继承及虚基类的基本概念及其它们在面向对象程序设计中的基本应用 基类和派生类 继承的机制提供了利用已有的数据类型来定义新的数据类型的途径 所定义新的数据类型不仅拥有新定义的成员(数据成员与成员函数),而且还同时拥有已存在的成员 我们将这种利用已知的类来定义新类的机制称之为类的继承。
我们称已存在的用来定义新类的类为基类,又称为父类由已存在的类派生出的新类称之为派生类,又称为子类 这样,派生类继承了它父类的属性和操作同时,在子类中也可声明新的属性和新的操作,剔除了那些不适合于其用途的继承下来的操作这种机制,使得程序员可重用父类的代码,将注意力集中在为子类编写新的代码 继承是我们理解事物,解决问题的方法继承可帮助我们描述事物的层次关系,帮助我们精确地描述事物,帮助我们理解事物的本质在解决某一问题时,只要弄清事物所处的层次结构,也就找到了对应的解决方法 在C++语言中,一个派生类可以从一个基类派生,也可以从多个基类派生从一个基类派生的继承称为单继承从多个基类派生的继承称为多继承单继承形成了类的层次,像一棵倒挂的树多继承形成了一个有向无环图如图所示 基类和派生类ABC(a)单继承XYZ(b)多继承基类和派生类 一.派生类的定义 单继承的定义格式如下: class <派生类名> : <继承方式> <基类名>{<派生类新成员的定义>};其中,<派生类名>是新定义的一个类的名字,它是从<基类名>中派生的,并且按指定的<继承方式>派生的 <继承方式>常使用下列三种关键字给予描述: public:表示公有继承。
private:表示私有继承 protected:表示保护继承 这三种继承的意义,在后讨论 基类和派生类多继承的定义格式如下: class <派生类名>:<继承方式1> <基类名1>,<继承方式2> <基类名2>,....{<派生类新成员的定义>};二.派生类的三种继承方式 公有继承(public)、私有继承(private)和保护继承(protected)是常用的三种继承方式 ⒈ 公有继承(public) 公有继承的特点是基类的公有成员和保护成员作为派生类的成员时,它们都保持原有的状态,而基类的私有成员仍然是私有的 ⒉ 私有继承(private) 私有继承的特点是基类的公有成员和保护成员都作为派生类的私有成员,并且不能被这个派生类的子类所继承与访问 基类和派生类⒊ 保护继承(protected) 保护继承的特点是基类的所有公有成员和保护成员都成为派生类的保护成员,并且只能被它的派生类成员函数或友元访问,而这种继承关系还可向其子类进行传递基类的私有成员仍然是私有的 为了进一步理解三种不同继承方式在其成员的可见性方面的区别,下面从三种不同角度进行讨论 ⑴ 基类成员对基类对象的可见性:在基类外部,通过基类对象对基类成员的可访问性。
⑵ 基类成员对派生类成员函数的可见性:派生类成员对继承下来基类成员的可访问性 ⑶ 基类成员对派生类对象的可见性:在派生类外部,通过派生类对象对基类成员的可访问性 对于公有继承方式: ⑴ 基类成员对基类对象的可见性: 公有成员可见,其它不可见这里保护成员等同于私有成员 基类和派生类⑵ 基类成员对派生类成员函数的可见性: 公有成员与保护成员可见,而私有成员不可见这里保护成员等同于公有成员 ⑶ 基类成员对派生类对象的可见性: 公有成员可见,其它成员不可见 结论:在公有继承时,派生类的对象可以访问基类中的公有成员派生类的成员函数可以访问基类中的公有成员和保护成员 对于私有继承方式: ⑴ 基类成员对基类对象的可见性: 公有成员可见,其它成员不可见 ⑵ 基类成员对派生类成员函数的可见性: 公有成员和保护成员可见,而私有成员是不可见的 ⑶ 基类成员对派生类对象的可见性 所有成员都是不可见的 结论:在私有继承时,基类的成员只能由直接派生类访问,而无法再往下继承 基类和派生类对于保护继承方式与私有继承方式的情况基本相同两者的区别就在于,基类的公有成员与保护成员在私有继承的方式下,在派生类中不允许再往下继承,而保护继承方式则允许继续往下传递。
可将可见性理解为可访问性关于可访问性的另外一种规则中,称派生类对象对基类的访问为水平访问,称派生类的派生类对基类的访问为垂直访问则上述讨论可总结如下: l公有继承时,水平访问和垂直访问对基类中的公有成员不受限制 l私有继承时,水平访问和垂直访问对基类中的公有成员也不能访问 l保护继承时,对于垂直访问同于公有继承,对于水平访问同于私有继承 结论:对于基类中的私有成员,只能被基类中的成员函数和友元函数所访问,不能被其它的函数访问 三.基类与派生类的关系 任何一个类都可以派生出一个新类,派生类也可作为基类再派生出新类因此,基类与派生类的关系是相对而言的 基类和派生类这就意味着:一个基类可以是另一个基类的派生类,这样便形成了复杂的继承结构,出现了类的层次一个基类派生出一个派生类,它又可作为另一个派生类的基类,则原来的基类为该派生类的间接基类其关系如右图所示 ABC其中,类A是类C的间接基类,而类B是类A的直接派生类 基类与派生类之间的关系,可从以下几个方面进行理解: ⒈ 派生类是基类的具体化 类的层次通常反映了客观世界中某种真实的模型例如,定义输出设备为基类,而显示器、打印机等是派生类,它们的关系如下图所示。
输出设备显示器打印机基类和派生类在这种情况下,不难看出:基类是对若干个派生类的抽象,而派生类是基类的具体化基类抽取了它的派生类的公共特征,而派生类通过增加行为将抽象变为某种有用的类型 ⒉ 派生类是基类定义的延续 先定义一个抽象类,该基类中有些操作并未实现然后定义非抽象的派生类,实现抽象基类中定义的操作,虚函数就属于此类情况这时,派生类是抽象基类的实现因此,将派生类看成基类定义的延续,是常用的方法之一 ⒊ 派生类是基类的组合 在多继承时,一个派生类有多于一个的基类,这时派生类将是所有基类行为的组合 派生类将其本身与基类区别开来的方法是添加数据成员和成员函数因此,继承的机制将使得在创建新类时,只需说明新类与已有类的区别,从而大量原有的程序代码都可以复用,所以可以称类是“可复用的软件构件” 单继承 在单继承中,每个类中可以有多个派生类,但是每个派生类只能有一个基类,从而形成树形结构,如下图所示 类A类B类C类D类E类F类G类H单继承一.成员访问权限的控制 [例5.1] 分析下列程序中的访问权限,并回答所提出的问题 #include
单继承⒉ 派生类B的对象b1能否访问基类A中的成员:f1()、i1和j1? 答案:可访问f1(),而不可访问i1和j1 ⒊ 派生类C中成员函数f3()能否访问直接基类B中的成员:f2()和j2?能否访问间接基类A中的成员f1()、j1和i1? 答案:f3()可以访问f2()与j2及间接基类中的f1()和j1,不可以访问i2和i1 ⒋ 派生类C的对象c1能否访问直接基类B中的成员:f2()、i2和j2?能否访问间接基类A中的成员:f1()、j1和i1? 答案:可以访问f2()和f1(),其它的都不可访问 由此分析,可得出结论:在公有继承时,派生类的成员函数可访问基类中的公有成员和保护成员派生类的对象仅可访问基类中的公有成员 考虑:将程序中的两处继承方式的public改为private,上述的访问权限又如何变化? 单继承[例5.2] 分析下列程序,指出错误之处,并给出纠正办法 #include 说明:使用class关键字定义类时,缺省的继承方式是private [例5.3] 分析下列程序,指出错误之处,并给出纠正办法 #include 因此,派生类对象中包含有由基类中说明的数据成员和操作所构成的封装体,称之为基类子对象,它必须由基类中的构造函数来完成对其的构造及初化的操作 构造函数不能被继承,所以,派生类的构造函数必须通过调用基类的构造函数来初始化基类的子对象因此,在定义派生类的构造函数时除了对自己的数据成员进行初始外,还必须负责调用基类构造函数使基类的数据成员得以初始化如果派生类中还有子对象时,还应包含对子对象初始化的构造函数 派生类对象的构造基类子对象构造与初始化自身声明数据成员的初始化基类构造函数派生类构造函数单继承派生类构造函数的一般格式如下: <派生类名>(<派生类构造函数总参数表>):<基类构造函数名>(<参数表1>),<子对象名>(<参数表1>),.... {<派生类中自定义数据成员初始化语句> }派生类构造函数的调用顺序如下: l基类的构造函数,调用顺序按照被继承时声明的顺序 l子对象类的构造函数(如果有),调用顺序按照定义顺序 l派生类构造函数 [例5.4] 分析下列程序的输出结果: #include 调用两次类B的默认构造函数,每调用类B的默认构造函数,先调用两次类A的默认构造函数来创造基类子对象与aa子对象,于是出现前6行的输出结果 ②在程序的两个赋值语句中,系统调用B类构造函数建立匿名对象,赋值后,再调用析构函数析构对象,由于对象析构顺序与构造顺序相反,所以出现两个调用派生类B的构造函数与析构函数的12行信息 ③输出两个类B对象的数据成员值,占有两行信息 ④最后,程序结束时,要析构数组对象,因此,输出最后6行信息 2.析构函数 我们知道,当对象的生命周期结束时,系统会自动调用析构函数完成对象的析构,对于派生类的对象也是如此也就是说,当派生类对象的生命周期结束时,派生类的析构函数被执行由于析构函数同样没有继承性,因此,在执行派生类的析构函数时,基类的析构函数也将被调用在这里,我们关心的是执行顺序的问题由于析构顺序与构造顺序相反,所以在执行析构函数时,先执行派生类的析构函数,再执行基类的析构函数 单继承[例5.5] 分析下列程序的运行结果 #include 如果是这样的话,则派生类不必负责调用基类的构造函数 [例5.6] 分析下列程序的输出结果 #include 单继承4.派生类生成过程 ⑴ 吸收基类成员 在类的继承中,首先是派生类先将基类的成员全盘接收这样,派生类实际上就包含了基类中除构造函数、析构函数之外的所有成员问题是,尽管很多基类的成员,特别是非直接基类的成员,在派生类中很可能根本就不起作用,却也被继承下来,在生成对象时也要占用内存空间,造成资源浪费 ⑵ 改造基类成员 对基类成员的改造包括两个方面,一是基类成员的访问控制问题,主要依靠派生类定义时的继承方式来控制第二是对基类数据成员或成员函数的覆盖,就是在派生类中定义一个和基类数据成员或成员函数同名的成员,由于作用域不同,于是发生同名覆盖,基类中的成员就被替换成派生类中的同名成员 ⑶ 添加新的成员 派生类新成员的加入是继承与派生机制的核心,是保证派生类在功能上有所发展的关键程序员可以根据实际情况的需要,给派生类添加适当的数据和函数成员,来实现必要的新增功能在这里,特别强调的是,在派生过程中,基类的构造函数和析构函数是不能被继承下来的 单继承三.子类型化和类型适应 1.子类型化 子类型的概念涉及到行为共享,它与继承有着密切关系 概念:有一个特定的类型S,如派生类,当且仅当它至少提供了类型T的行为,如基类的行为,则称类型S是类型T的子类型。 子类型是类型之间的一般和特殊的关系 在继承中,公有继承可以实现子类型 例如有如下的定义: [例5.7]#include 单继承2.类型适应 类型适应是指两种类型之间的另一种关系体现最为具体的是类继承之间的一种类型适应关系在上例中,我们可以讲,类B是适应类A的,这就意味着类B的对象能够用于A类型的对象所能使用的场合 这样,派生类对象可以用于基类对象所能使用的场合,我们说派生类适应于基类 同样的道理,派生类对象的指针和引用也适应于基类对象的指针和引用 子类型化与类型适应是一致的若一类型是另一类型的子类型,则该类型必将遵守类型适应的原则,也就是说该类型必将适应于另一类型 子类型化的重要性就在于减少程序员编写程序代码的负担,这是因为一个函数可以用于某类的对象,则它也可用于该类型的各个子类型的对象 [例5.8] 分析下列程序的输出结果include 3.赋值兼容原则 类型的适应性,在有的资料或教课书上也称为赋值兼容原则我们通过赋值兼容原则的这个角度,对上述内容再作一小结 所谓赋值兼容,与类型适应性是同一个概念亦即,指在需要基类对象的任何地方都可以使用公有派生类的对象来替代 单继承通过公有继承,派生类得到了基类中除构造函数、析构函数之外的所有成员,而且所有成员的访问控制属性也和基类完全相同这样公有派生类实际就具备了基类的所有功能,凡是基类能解决的问题,公有派生类都可以解决 赋值兼容具体的原则如下:l派生类的对象可以赋值给基类对象 l派生类的对象可以初始化基类的引用 l派生类对象的地址可以赋给基类类型的指针 在完成上述兼容的赋值之后,派生类对象就可以作为基类的对象使用,但值得关注的问题是,只能使用从基类继承的成员 多继承 一.多继承的概念 多继承可以看作是单继承的扩展所谓多继承是指派生类具有多个基类,派生类与每个基类之间的关系仍可看作是一个单继承 类A类B类C类D类E类F类G类H类I多继承多继承下派生类的定义格式如下: class <派生类名>:<继承方式1> <基类名1>,<继承方式2> <基类名2>,....{<派生类新成员的定义>};其中,继承方式的含义与单继承继承方式的含义相同。 二.多继承派生类的构造函数 在多继承的情况下,派生类的构造函数如下: <派生类名>(<总参数表>):<基类名1>(<参数表1>),<基类名2>(<参数表2>),...<子对象名>(<参数表>)...{派生类新定义成员的初始化语句;}其中,<总参数表>中各个参数包含了其后的各个分参数表 多继承多继承下派生类的构造函数与单继承下派生类的构造函数类似,它必须负责派生类所有基类构造函数的调用同时,派生类的参数必须包含完成所有基类初始化所需的参数 派生类构造函数执行顺序是先执行所有基类的构造函数,再执行派生类本身构造函数处于同一层次的各基类构造函数的执行顺序取决于定义派生类时所指定的各基类的顺序,与派生类构造函数中所定义的成员初始化列表的顺序无关 [例5.9] 分析下列程序的行动结果include 但在多继承的情况下,可能会造成对基类中某个成员的访问出现不惟一的情况,称之为对基类成员访问的二义性问题 解决对基类成员访问二义性问题的一个途径就是用作用域运算符对成员进行惟一标识 我们首先回顾一下,在不同作用域声明的标识符的可见性原则:如果存在两个或多个具有包含关系的作用域,外层声明的标识符如果在内层没有声明同名标识符,那么它在内层仍可见;如果内层声明了同名标识符,则外层标识符在内层不可见,这时称内层标识符覆盖了外层同名标识符,这个原则称为同名覆盖原则 在类的派生层次结构中,基类的成员和派生类新增的成员都具有类作用域,二者的作用范围不同,是相互包含的两个层,派生类在内层这时,如果派生类声明了一个和某个基类成员同名的新成员(如果是成员函数,则参数的特征标也要相同,若参数特征标不同属于函数的重载),派生的新成员就覆盖了外层同名成员,直接使用成员名只能访问到派生类的成员如果加入作用域运算符,使用基类名来进行限定,就可以访问到基类的同名成员 多继承对于多继承情况,我们首先考虑各个基类之间没有任何继承关系,同时也没有共同基类的情况最典型的情况就是所有基类都没有上级基类如果某派生类的多个基类拥有同名的成员,同时,派生类又新增这样的同名成员,在这种情况下,派生类成员将覆盖所有基类的同名成员。 这时使用“对象名.成员名”方式可惟一标识和访问派生类新增成员,基类的同名成员也可使用作用域运算符访问但是,如果派生类没有声明同名成员,“对象名.成员名”方式就无法惟一标识成员,这时,从不同基类继承过来的成员具有相同的名称,同时具有相同的作用域,系统仅仅根据这些信息根本无法判断到底是调用哪个基类的成员,这时就必须通过作用域运算符来标识成员 下面通过例子对上述的规则进行讨论例若有以下定义:class A{public:void f();};多继承class B{ public:void f();void g();};class C:public A,public B{public:void g();void h();};A类{ f() }B类{ f() ,g() }C类{ g(), h() }类A、类B与类C关系的DAG图多继承如果定义一个类C的对象c1,则对函数f()的访问c1.f()便具有二义性! 原因:是访问类A中的f()还是访问类B中的f() ?解决方法:可用作用域运算符来消除二义性:c1.A::f()或c1.B::f() 解决此二义性的另一种办法是在类C中定义一个同名成员f(),类C中的f()再根据需要来决定调用A::f()还是B::f(),还是两者皆有,这样根据同名覆盖的原则,c1.f()将调用C::f()。 同样,类C中成员函数调用f()也会出现二义性问题例如: void C::h() { f(); } 这里存在二义性的访问,该函数应修改为void C::h() { A::f(); }或void C::h() { B::f(); } 或 void C::h() { A::f(); B::f() } 这里值得说明的是,类B中有一成员函数g(),类C中也有一成员函数g(),这时c1.g()则不存在二义性,同名覆盖原则可对此做出最恰当的诠注 多继承在多继承可能出现二义性问题的另一种情况,就是基类之间本身具有继承关系如果某个派生类的部分或全部直接基类是从另一个共同的基类派生而来,在这些直接基类中,从上一级基类继承来的成员就拥有相同的名称,因此派生类中也就会产生同名现象,对这种类型的同名成员也要使用作用域运算符来进行惟一标识,而且必须用直接基类进行限定 例如:class A {public:int a;};class B1:public A{private:int b1;};多继承class B2:public A{private:int b2;};class C:public B1,public B2{public:int f();private:int c;};类A{ a }类B1{b1}类B2{b2}类C{f(),c}类A、类B1、类B2与类C的DAG图若定义C类的一个对象c1,则c1.a或c1.A::a的访问存在二义性,要消除此二义性,只能用直接基类进行限定:c1.B1::a或c1.B2::a。
