第九章面向对象1--继承这一章的内容包括:基类和派生类派生类的构造函数/析构函数子类型化和类型适应多继承虚基类一、基类和派生类1.概念通过继承机制,可以利用已有的数据类型来定义新的数据类型。所定义的新的数据类型不仅拥有新定义的成员,而且还同时拥有旧的成员。我们称已存在的用来派生新类的类为基类,又称为父类。由已存在的类派生出的新类称为派生类,又称为子类。在C++语言中,一个派生类可以从一个基类派生,也可以从多个基类派生。从一个基类派生的继承称为单继承;从多个基类派生的继承称为多继承。2.语法单继承的定义格式如下:class派生类名:继承方式基类名{//派生类新定义成员};其中,派生类名是新定义的一个类的名字,它是从基类名中派生的,并且按指定的继承方式派生的。继承方式常使用如下三种关键字给予表示:public表示公有基类;private表示私有基类;protected表示保护基类;示例:classMyDate:publicDate1{//派生类新定义成员变量或函数};//派生类新定义成员变量(或函数)初始化(或实现)3派生类的三种继承方式公有继承(public)、私有继承(private)、保护继承(protected)是三种继承方式。(1)公有继承(public)公有继承的特点是基类的private、public和protected成员作为派生类的成员时,它们都保持原有的状态。(2)私有继承(private)私有继承的特点是基类的public成员和protected成员,在派生类都变为私有成员。基类的原私有成员仍为派生类的私有成员(3)保护继承(protected)保护继承的特点是基类的public成员和protected成员,在派生类都变为保护成员。基类的原私有成员仍为派生类的私有成员。下表列出三种不同的继承方式的基类特性和派生类特性:继承方式基类成员派生类成员公有继承publicprotectedprivatepublicprotectedprivate私有继承publicprotectedprivate变为private变为privateprivate保护继承publicprotectedprivate变为protectedprotectedprivate对于单级继承来说,讨论保护继承与私有继承的区别意义是不大的,他们的区别只在多级继承的情况中体现。私有继承在一些特定场合下可用于表示类的组成关系。保护继承与私有继承在实际编程中是极其少见的。Q1:C++默认的继承方式?对于C++的类(class),默认的继承方式是私有继承(private),而最常用的继承方式是公有继承(public)。对于C++的结构体(struct)默认的继承方式是公有继承(public)。4.基类与派生类的关系任何一个类都可以派生出一个新类,派生类也可以再派生出新类,因此,基类和派生类是相对而言的。基类与派生类之间的关系可以有如下几种描述,了解这些关系有助于指导实际编程工作:(1)派生类是基类的具体化类的层次通常反映了客观世界中某种真实的模型。在这种情况下,不难看出:基类是对若干个派生类的抽象,而派生类是基类的具体化。基类抽取了它的派生类的公共特征,而派生类通过增加行为将抽象类变为某种有用的类型。(2)派生类是基类定义的延续先定义一个抽象基类,该基类中有些操作并未实现。然后定义非抽象的派生类,实现抽象基类中定义的操作。例如,虚函数就属此类情况。这时,派生类是抽象的基类的实现,即可看成是基类定义的延续。这也是派生类的一种常用方法。(3)派生类是基类的组合在多继承时,一个派生类有多于一个的基类,这时派生类将是所有基类行为的组合。比如以前讲过的飞机组成的例子,用组成关系和多继承都可以,但最好使用组成关系。派生类将其本身与基类区别开来的方法是添加数据成员和成员函数。因此,继承的机制将使得在创建新类时,只需说明新类与已有类的区别,从而大量原有的程序代码都可以复用,所以有人称类是“可复用的软件构件”。在实际编程中,一定要注意,继承是软件体系结构方面的问题:(1)派生类最好是基类的一类(isakindof),否则容易出现设计概念错误。(2)继承会增加空间复杂度和时间复杂度,要慎重使用。继承链不要太长(3-5层足矣),要“头轻脚重”,越是顶层的类越要轻巧----最好是完全抽象的虚类。(3)优先使用组成关系(有的书叫聚集、聚合、复合),而不是继承关系解决问题。继承把抽象化角色(基类)和实现化角色(派生类)的关系绑定,使得两个层次之间产生了相互依赖和限制,很难独立地演化,在软件设计中应避免此类问题。组成关系没有这个缺点。(4)基类要“坚固不变”,否则派生类……。如果基类的变化是必需的,那应该在派生类和基类之间加一个设计概念上更抽象的“第三者”类,用这个“第三者”的类去“破坏”原有的继承关系。想想为什么数据库访问要通过JDBC等接口。Q2:实际编程中使用继承应注意哪些问题?要求对课件讲到的做1-2点补充。Q3:你对“优先使用组成关系,而不是继承关系”是怎样理解的?要有自已的观点。二.派生类的构造函数/析构函数当一个派生类被实例化时,其基类也被实例化,实际上一个派生类对象是由其基类对象和派生类对象“组装”而成,了解这一点才能了解派生类构造函数/析构函数的有关特性。1.派生类的构造函数构造函数不能够被继承,因此,派生类的构造函数必须通过调用基类的构造函数来初始化派生类对象。所以,在定义派生类的构造函数时除了对自己的数据成员进行初始化外,还必须负责调用基类构造函数使基类数据成员得以初始化。如果派生类中还有子对象时(比如将某个类的对象作为派生类的成员变量),还应包含对子对象初始化的构造函数。派生类构造函数的一般格式如下:派生类名(派生类构造函数总参数表):基类构造函数(参数表1),子对象名(参数表2){//派生类中数据成员初始化};派生类构造函数的调用顺序如下:基类的构造函数子对象类的构造函数(如果有的话)派生类构造函数例:classA{private:inta;public:A(){a=0;}//缺省构造函数A(inti){a=i;}//构造函数~A(){}//类A的析构函数}classB:publicA{private:intb;Aaa;//子对象aapublic:B(){b=0;}//类B的缺省构造函数B(inti,intj,intk);//构造函数~B(){}//类B的析构函数}//类B构造函数,注意顺序B::B(inti,intj,intk):A(i),aa(j){b=k;}派生类构造函数使用中应注意的问题(1)派生类构造函数的定义中可以省略对基类构造函数的调用,其条件是在基类中必须有缺省的构造函数或者根本没有定义构造函数。(2)当基类的构造函数使用一个或多个参数时,则派生类必须定义构造函数,提供将参数传递给基类构造函数途径。在有的情况下,派生类构造函数的函数体可能为空,仅起到参数传递作用。2.派生类的析构函数当派生类对象被删除时,派生类的析构函数被执行。执行顺序与执行构造函数时的顺序正好相反。三、子类型化和类型适应这一小节要掌握两个概念:子类型化和类型适应。这两个概念在软件设计书籍中常见。1.子类型化有一个特定的类型S,当且仅当它至少提供了类型T的行为,由称类型S是类型T的子类型。或者说类型T是类型S的子集。子类型主要存在于类的继承关系,但不仅限于继承关系。在C++继承中,公有继承可以实现子类型。例如:classA{public:voidp(){}};classB:publicA{public:voidf(){}};类B继承了类A,并且是公有继承方式。因此,可以说类B是类A的一个子类型,类B具备类A中的所有行为,或者说类A中的所有行为可被用于类B。子类型关系是不可逆的。这就是说,已知B是A的子类型,而认为A也是B的子类型是错误的,或者说,子类型关系是不对称的。2.类型适应回顾JAVA语言,JAVA语言的继承性都是public的(实际上JAVA语言也不允许private和protected继承),也就是说JAVA语言的子类都是父类的子类型。JAVA课本P89“类的赋值相容性”中有一句话:“子类对象即是父类对象”,说的就是子类型和类型适应问题。类型适应是指两种类型之间的关系。例如,B类型适应A类型是指B类型的对象能够用于A类型的对象所能使用的场合。或者说,可以将子类对象赋值给父类对象,反过来则不可以。派生类的对象可以用于基类对象所能使用的场合,我们说派生类适应于基类。同样道理,派生类对象的指针和引用也适应于基类对象的指针和引用。子类型化与类型适应是一致的。A类型是B类型的子类型,那么A类型必将适应于B类型。Q4:理解子类型和类型适应的概念四、多继承1.语法从多个基类派生的继承称为多继承。多继承的定义格式如下:class派生类名:继承方式1基类名1,继承方式2基类名2,…{//派生类新定义成员};示例:classMyDate:publicDate1,protectedDate2{//派生类新定义成员变量或函数};//派生类新定义成员变量初始化或函数实现2.多继承的构造函数在多继承的情况下,派生类的构造函数格式如下:派生类名(总参数表):基类名1(参数表1),基类名2(参数表2),…子对象名(参数表n+1),…{//派生类构造函数体}其中,总参数表中各个参数包含了其后的各个分参数表。多继承下派生类的构造函数与单继承下派生类构造函数相似,它必须同时负责该派生类所有基类构造函数的调用。3.多继承的二义性问题一般说来,在派生类中对基类成员的访问应该是唯一的,但是,由于多继承情况下,可能造成对基类中某成员的访问出现了不唯一的情况,则称为对基类成员访问的二义性问题。例1:下面举一个的例子,对二义性问题进行讨论:classA{public:voidf();};classB{public:voidf();voidg();};classC:publicA,publicB{public:voidg();voidh();};如果实例化一个类C的对象c1,则对函数f()的访问c1.f();便具有二义性:是访问类A中的f(),还是访问类B中的f()呢?解决的方法可用以前学过的成员名限定法来消除二义性,例如:c1.A::f();或者c1.B::f();例2:下面再举一个的例子,对菱型继承的二义性问题进行讨论,这个问题比较重要:当一个派生类从多个基类派生类,而这些基类又有一个共同的基类,也就是说菱型继承时,则对该基类中说明的成员进行访问时,也可能会出现二义性。例如:classA{public:inta;};classB1:publicA{private:intb1;};classB2:publicA{private:intb2;};classC:publicB1,publicB2{public:intf();private:intc;};当实例化一个C类对象c1,内存是什么状况呢?对象c1是由2个A对象,1个B1对象,1个B2对象,1个C对象“组合”而成,这几个对象不一定占用连续的空间,它们彼此用指针或引用关联成一个整体。为什么会有两个A对象?因为B1和B2各实例化了一个A对象。由于有两个A对象存在,下面的访问都有二义性:c1.a;c1.A::a;而下面的访问是正确的:c1.B1::a;c1.B2::a;由于二义性的原因,一个类不可以从同一个类中直接继承一次以上,例如:classA:publicB,publicB{…}这是错误的。上面的两个例子,消除二义性的解决办法都不算好,有没有更好的办法?答案是使用虚基类。五、虚基类1.虚基类消除二义性再回到前面例2的菱型继承问题,由类A,类B1和类B2以及类C组成了类继承的(菱型)层次结构。该结构产生二义性的原因是:类C的对象将包含两个类A的子对象(类B1和类B2各实例化出一个类A对象)。如果要想使这个公共基类在派生类中只产生一个基类子对象,则必须将这个基类设定为虚基类。实际上,引进虚基类的真正目的就是为了解