零点起飞学C之继承和派生.pptx_第1页
零点起飞学C之继承和派生.pptx_第2页
零点起飞学C之继承和派生.pptx_第3页
零点起飞学C之继承和派生.pptx_第4页
零点起飞学C之继承和派生.pptx_第5页
已阅读5页,还剩55页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

第15章 继承和派生,继承和派生是父类和子类之间的关系,是从两个不同角度谈同一件事情。继承让子类可以获得父类的特性,派生让父类可以将自己的特性遗传给子类。学习本章,读者可以设计出更加满足实际需要的类,在更高的抽象层次上考虑和解决问题,从而应对更复杂的实际需要。,15.1 由类生成类,派生和继承都是在描述同一件事情,它们是从两个角度来谈父类和子类的关系。 从已有的类遗传产生一个新的类,称为类的派生。该类被称为父类,也叫基类。新的类被称为子类。通过派生,父类将其已有的特性遗传给了子类,子类将自然拥有父类的各种特性。派生机制提供了扩展或定制基类特性的手段,子类可以增加自己独有的特性。通过类的派生就可构造可重用的类库,扩展类的特性。从子类的角度来看这件事情,即一个新类从已有的类那里获得其特性,就称为继承。从父类到子类是一个特殊化、具体化的过程,从子类到父类则是一个泛化、抽象的过程。,例如,定义动物类animal,从animal派生子类dog。在这个过程中,父类animal派生了子类dog,或者说子类dog继承于父类animal。从animal还可以派生cat或chick等子类。子类也可以继续派生。例如,dog可以继续派生policedog、armydog等具体的类。从这个过程可以看出,子类和父类是一个相对的概念。dog是animal的子类,又是policedog的父类。 一个父类可以派生多个子类,一个子类也可以继承于多个父类。如果一个子类只从一个父类派生,就称为单继承。如果一个子类继承于多个父类,就称为多重继承。任何一个类都可以作为父类派生新的子类,也都可以作为子类继承于其他的类。 从父类派生子类时,子类可以具有:继承父类的数据成员;继承父类的成员函数;增加新的数据成员;增加新的成员函数;重新定义基类中已有的成员函数;改变现有成员的属性等特征。,15.2 派生一个类,派生指从父类产生子类,并将父类的所有特性遗传给子类,这个过程就叫作派生。派生将一个父类具体并特例化为新的子类,它避免了重复定义某些公有的特性,又允许子类定义自己特有的性质。本节将详细讲解如何从一个父类派生新的子类。,15.2.1 派生的起点基类,基类就是父类,也可以是其他父类的子类。但是,只有派生了至少一个子类的类才能称为是基类。这个概念实质上是一个相对的概念,与普通类并没有什么特殊的区别。只要某个类派生了至少一个子类,就可以称其为基类。但是一般情况下,基类都会被定义为一个抽象的概念,它泛化了某一类事物的共有特征。因此,直接从这样的基类实例化一个对象是没有意义的。例如,直接用animal实例化一个类对象是没有任何意义的。基类的用处是常常可以定义基类的指针,从而指向子类,这在实现面向对象的多态性方面很有帮助。基类成员的下述性质将影响到子类的成员。,1private修饰符,被private修饰的成员是私有成员,它对外界完全封闭的。私有成员可以被类自身的成员和友元访问,但不能被包括派生类在内的其他任何类和任何普通函数访问。,2public修饰符,被public修饰的成员是公有成员,它对外界是完全公开的。公有成员可以被任何普通函数和任何类的成员函数访问,也可以被子类访问。,3protected修饰符,被protected修饰的成员是保护成员,它对外界是半开半闭的。保护成员可以被类自身的成员和友元访问,还可以被派生类的成员函数访问,但不能被任何非友元的普通函数访问。 注意:好的编程习惯是尽量避免使用友元去访问私有成员,除非确实需要。因为这破坏了类的封装性。,【示例15-1】,演示类成员的限定符用法。 class CPerson public: /公共成员区 CPerson(); virtual CPerson(); protected: /保护成员区 string m_name; bool m_sex; short m_age; public: int GetVersion(char *info); private: /私有成员区 string m_ver; ;,分析:该示例定义了一个基类CPerson的一部分。CPerson的构造函数和析构函数被声明为公有的,这是因为该类的子类将会自动调用它们。CPerson的属性m_ver表示类的版本号,它只能通过公有成员函数GetVersion()获得。它的属性m_name、m_sex等被定义为了保护属性,只能被子类访问到。,15.2.2 派生的方式,派生指从基类衍生出一个新的子类。但是派生并不是把基类的成员和派生类自己增加的成员,简单地加在一起就成为派生类。构造一个派生类需要依次进行下述工作。,(1)全继承:不加选择地继承基类的所有成员,但是不包括构造函数和析构函数。 (2)成员调整:按照指定的继承方式和重载或覆盖方式调整基类的成员满足自己的需 要,从而实现多态。 (3)重写构造函数与析构函数:子类不继承这两种函数,无论原来是否可用,子类最好重写它们。 (4)特例化:增加子类自己的成员,扩展基类的属性和方法。 其中,第(1)步是自动完成的,第(2)和第(3)步是按需进行。在从一个基类派生一个子类后,至少会在第(2)或第(3)步有至少一种改动,否则,派生就变得没有任何意义。因此首先需要有一个基类,基类提供了基本的属性和方法。然后再派生子类,子类将修改或增加新的属性方法。,类的派生格式如下: class 子类:基类 . ; 类的派生仅是在声明类时增加“:基类”。“:”表示前面的类派生于后面的基类,“派生方式”规定了子类如何继承基类的成员。子类自动接收了全部基类的成员,但构造函数和析构函数是不能被继承的,派生类要重新定义构造函数和析构函数。,【示例15-2】,使用示例15-1中的类派生新的子类。 分析:该示例使用示例15-1中的CPerson类派生了CStudent类和CStudent2类,以受保护方式继承。CPerson中的所有成员都成了CStudent中的受保护成员。CStudent除了继承CPerson的成员外,还定义了属于自己的id和school属性,重载了m_ver和GetVersion。CStudent没有重载m_ver和GetVersion。因此,CStudent访问可以直接访问GetVersion函数和m_ver,而且访问的是自身的成员。但是CStudent2则不能。而且m_GetVersion是CPerson的受保护成员,所以只能在CStudent2内部访问。所以,如果放开示例中被注释掉的两行语句,编译器会提示成员不可访问。,15.2.3 使用构造函数,构造函数是类的特殊函数,只要声明一个类,就自动为它分配了一个默认的构造函数。其声明方式如下: ClassName(); 其中,ClassName是类的名字,可以有参数,也可以不带参数。它不能有返回值,也不能用void来修饰。,构造函数的定义方式如下: ClassName:ClassName() . 构造函数是实例化类时要调用的函数,它对类对象进行了初始化。当从基类派生子类时,构造函数不被继承,子类必须自行声明。,子类声明自己的构造函数时,遵循如下原则: 初始化子类的新增成员,继承来的成员则由基类完成初始化。 若基类的声明中使用不带参数的构造函数或未声明构造函数,则子类构造函数的声明中可以省略对基类构造函数的调用,或不声明构造函数。 若基类声明了带参数的构造函数,则派生类也应声明带参数的构造函数,并显示将参数传递给基类的构造函数。,根据上述原则,当基类带参数时,构造函数的形式如下所示。 ClassName():BaseClass(),Object() . 子类的构造函数后显示给出了基类的构造函数,基类构造函数的参数来自子类的构造函数的参数。如果子类内还有其他对象需要被初始化,则也可以放在构造函数后。冒号后部分的顺序可以随意,与调用顺序无关。在声明构造函数时,不需要带冒号及冒号后的部分。,在实例化子类时,构造函数的调用顺序遵循下述原则: 基类构造函数先于子类执行,调用顺序按照继承时声明的顺序从左向右执行; 如果有内嵌对象,则调用内嵌对象的构造函数,调用顺序由声明顺序决定; 最后执行子类的构造函数。,【示例15-3】,构造函数的调用过程演示。 分析:该实例中,子类CStudent的构造函数同时也调用了基类CPerson和成员变量m_school的构造函数。从输出结果可以看出,基类和子类成员m_school的构造函数先于子类的构造函数执行。,15.2.4 使用析构函数,析构函数是类的特殊函数,只要声明一个类,就自动为它分配了一个默认的析构函数。析构函数的作用是当类不再使用时进行善后工作,比如收回动态分配的内存。其声明方式 如下: ClassName(); 其中,ClassName是类的名字,是析构函数的标志符。它不能有返回值,也不能用void来修饰。析构函数的定义方式如下: ClassName:ClassName() . ,与构造函数一样,析构函数也不被继承,子类必须自行声明。但是,是否需要定义构造函数与基类没有关系,完全依赖子类是否需要在撤销时做一些善后工作。基类的析构函数不会因派生类没有析构函数而得不到执行,它们各自是独立的。子类也不需要显式地调用基类的析构函数,系统会自动隐式调用。 析构函数的调用次序与构造函数相反。当撤销子类对象时,先执行子类的析构函数,再执行新增成员对象的析构函数,最后执行基类的析构函数。,【示例15-4】,演示析构函数的调用过程。 分析:该示例中CStudent继承于CPerson,在CStudent内又有内嵌的类对象CPerson cp。构造函数的调用顺序是按照派生类的构造函数的声明顺序来调用的,即先调用基类CPerson的构造函数,再调用内嵌成员CPerson cp的构造函数。析构函数则恰好与构造函数的调用顺序相反。从输出可以看出,析构的过程恰好与构造相反。首先析构子类,然后是子类的对象cp,最后才是基类。 注意:由于基类和子类的析构函数都会被调用到,所以在子类中释放为基类的成员申请的动态内存时,一定要先检查是否已经被释放;否则,将导致因重复释放某个指针而出错。,15.2.5 方法同名,怎么办,当子类和父类有同名的方法时,如果父类中的方法是虚拟的,则子类可以重新定义它,也可以直接使用;如果不是虚拟的,则子类的方法将覆盖父类的同名方法。如果没有明确指明,则通过子类调用的是子类中的同名成员。要想在子类中访问被覆盖的同名成员,要用域限定符来指出。这属于面向对象思想中的多态性,在第16章将会有详细讲解。,【示例15-5】,子类与父类有同名方法的示例。 分析:该示例中CPerson和CStudent有同名的方法getName()。如果直接用CStudent对象cs调用,则调用的是CStudent的getName()函数。要想调用父类的getName(),就必须用域限定符以“CPerson:getName(name);”的形式调用。域限定符显式指定了要调用的方法属于哪个类。,15.2.6 属性同名,怎么办,如果子类与父类有同名的属性时,子类将覆盖掉父类的属性。要想调用父类的属性,必须用域限定符显示指定。这是类的多态性,在第16章将有详细讲解。,【示例15-6】,演示子类与父类有同名属性时的使用方法。 分析:在CStudent的构造函数中,将实例化时的参数jack传给了父类构造函数,但是却对自己的成员m_name赋值为tom。,15.3 单 重 继 承,单重继承指从单个基类派生出子类,或者说某个类的直接父类只有一个。此前讲到的概念和示例都属于单重继承。单重继承的好处是脉络清楚,没有二义性。单重继承的格式如下所示。 class 子类:基类 . 其中,基类只能有一个,类继承层次是一棵树,每个子类有且仅有一个直接的父类。每个类最多直接继承一个父类的特性,从继承层次的根到任何一个类只存在一条唯一的线性路径。如图15-1所示为单重继承的层次结构图。,图15-1 单重继承的层次结构图,图中箭头表示了抽象泛化的过程。此前列出的示例中的类继承都属于单重继承,每个子类都只有一个父类。,15.4 多 重 继 承,多重继承指一个对象从多个基类派生而来,它让一个子类可以拥有来自多个父类的属性。可以将多重继承看做是单重继承的扩展,而单重继承则可以看做是多重继承的特例。本节将向读者详细讲解多重继承的概念和用法。,15.4.1 为什么要多重继承,多重继承的目的是改进类的重用性,允许一个子类同时具有多个父类的特性,这多个父类可以分别设计而不必相互知晓。类的多重继承层形成一个有向无环图,如15-2所示。,图15-2 多重继承的层次结构,类ClassA0派生自ClassA3和ClassA4,类ClassA3派生自ClassA5和ClassA2。在现实生活中,经常有许多事物同时具有多种身份。例如,有的人既是教师又是行政人员;有的人既是学生又是工程师;骡子兼具马和驴的两种属性;水陆两用汽车兼具陆上和地上交通工具的两种特性等。 这种情况下,往往已经有许多设计好的类可供使用。如果需要综合它们中某几个类的特性形成新的类,那么就需要使用多重继承。它提高了类的可重用性,不需要再去设计包含所有所需特性的专门类。,15.4.2 构造多重继承,多重继承的声明格式如下所示。 class ClassName:BaseClass1,.,BasseClassN 其中,冒号右边有多个基类,每个基类都可以单独规定它的派生方式。例如,骡子类可以从驴和马两种类共同派生而来,两栖动物可以从水生和陆生两种动物类派生而来。,【示例15-7】,类A同时继承自教师和工程师两个基类,类B同时继承自教师和学生两个基类。 分析:该示例定义了教师、学生、工程师3种职业类。类A表示既是教师又是工程师的一类从业人员,类B表示既是教师又是学生的在职上学人员。,15.4.3 析构函数和构造函数,多重继承下派生类的构造函数与单重继承下派生类构造函数相似,它必须同时负责该派生类所有基类构造函数的调用。同时,派生类的参数个数必须包含完成所有基类初始化所需的参数。对于所有需要给予参数进行初始化的基类,都要显式给出基类名和参数表。对于使用默认构造函数的基类,可以不给出类名。同样,对于对象成员,如果是使用默认构造函数,也不需要写出对象名和参数表。,派生类构造函数执行顺序是先执行所有基类的构造函数,再执行内嵌对象的构造函数,最后执行子类的构造函数。同一层次的各基类的构造函数执行的顺序取决于定义派生类时基类的安排顺序,与派生类构造函数中所定义的成员初始化列表的顺序无关。也就是说,执行基类构造函数的顺序取决于定义派生类时基类的顺序。,【示例15-8】,多重继承的析构函数和构造函数的调用顺序举例。 分析:该示例定义了三个基类,通过多重继承从它们派生出了两个子类。主函数内只声明了两个子类的实例,却没有使用它们,这主要是为了观察构造函数和析构函数的调用过程。对于类B,定义时CStudent在CTeacher前,因此构造函数先调用CStudent的,再调用CTeacher的,最后是B自身的。析构函数则正好相反,调用顺序是从里到外。 从输出可以看出构造函数严格按照声明子类时基类的书写顺序从左到右执行,最后才执行子类的构造函数。而且,基类构造函数执行与构造函数冒号后的基类构造函数的书写顺序无关。析构函数的执行顺序则正好与构造函数相反。,因此,多重继承的构造函数和析构函数的性质可以总结如下: 多重继承析构函数的声明方法与单重继承的相同; 多重继承的构造函数和析构函数具有与单重继承构造函数和析构函数相同的性质和特性; 多重继承构造函数和析构函数的执行顺序与单继承的相同。但应强调的是,基类之间的执行顺序是严格按照声明时从左到右的顺序来执行的,与它们在定义派生类构造函数中的次序无关。,15.4.4 多重继承的二义性,一般来讲,在子类中对基类成员的访问应该是唯一的。但是在多重继承的情况下,一个类可有多个直接的基类, 从继承层的顶部到每个子类可以有多条路径。如果这些基类中的成员有重名的情况,就会造成无法确定是引用哪个基类的成员的问题。多重继承中这种对父类成员访问不唯一的问题就称为对基类成员访问的二义性问题。二义性的存在原因及解决办法有以下3种:,1基类中有同名的成员,例如,示例15-8中基类CTeacher和CStudent都有成员m_name,如果直接从类B中访问m_name,系统将无法判断要访问的是哪个父类中的成员。用域限定符可以解决这种情况下的二义性。例如从B中访问m_name,可以写做CTeacher:m_name或CStudent:m_name的形式,明确指明要访问哪个类。,2基类与子类有同名成员函数,如果基类与子类有同名成员,则从子类访问同名的成员时访问的是子类本身的成员,若要访问基类的同名成员可以使用域限定符。,3至少两个基类继承了同一个祖先类,如果至少两个基类派生于共同的的父类,则这两个基类中都将出现基类中的同一成员。在这种情况下,就不能直接用域限定。例如,假设CTeache和CStudent都派生自CPerson类,且CPerson有成员为m_age。则用CPerson:m_age来访问m_age成员将仍然是模糊不清。这种情况下,可以用类的限定域从子类的最直接父类来访问成员。,【示例15-9】,演示多重继承的二义性问题。 分析:该示例中类A和E中都存在针对成员m_name的二义性。其中,A和其两个父类都有成员m_name。如果直接访问m_name,将是访问A本身的成员。若要访问父类的成员,就必须加限定符。E的两个父类都派生于同一个基类,所以也给E带来了二义性。如果直接访问,系统将提示存在二义性,因为无法确定m_name来自哪个父类。如果直接用基类来作限定符也会提示二义性。因此,就需要用E的父类而不是祖先类来做限定。,15.5 虚继承与虚基类,虚继承指在子类的声明中,基类被virtual修饰下的继承关系。虚继承中的基类就叫虚基类。虚继承的目的是为了解决多重继承中公共基类被重复实例化的问题。本节将详细讲解虚继承的概念和用法。,15.5.1 什么是虚继承,多重继承中,子类的继承路径有多条。如果路径上有公共的基类,那么在这些路径中某几条路径的汇合处,这个公共的基类将会产生多个实例(也称多个副本)。虽然可以用作用域“:”来区分不同的副本,但这导致了二义性和内存空间的浪费。 为了解决这个问题,C+中使用了虚继承的继承机制。虚继承将使该公共基类只被实例化一次,内存中只有它的一份副本。,为了实现虚继承,派生新类时需要用virtual修饰基类。其格式如下: class ClassName:virtual BaseClass . 其中,virtual仅对邻近的基类起作用。如果有多个基类需要被说明是虚基类,必须给每个基类都添加virtual说明。允许基类在作为某些子类的虚基类的同时,又作为另一些子类的非虚基类。虚基类是对子类而言,其本身的定义同普通类一样,只是在声明子类时被说明为虚基类。,【示例15-10】,简单的虚基类使用示例。 class A:virtual public B . 分析:它定义了从A对B的虚继承,B被说明为A的虚基类。 实际上,“虚”并不表示“无”,而是表示“存在,但无形”。既它确实存在,只是无法直接使用。一般来讲,“虚拟”包含3层意思。 存在性:即虚继承体系和虚基类确实存在,它确实是一种继承关系,也确实在内存中占用了空间; 间接性:指不能直接访问虚基类的成员,而是通过某种间接机制来完成; 共享性:这表现在虚基类会在虚继承体系中被共享,即在整个继承树中只被实例化一次,内存中没有其它的副本。 下面以两个示例来分析虚继承的作用。,【示例15-11】,多重继承中存在二义性。 分析:该示例中类A和B都是继承于基类base,类C又多重继承于A和B。因此,类A和B中都有从基类继承来的成员x,所以语句“cc.x”将会产生二义性,编译器不知道要访问的是哪个类中的成员x。所以示例中使用了域限定符来区分两个类中的x。此外,由于类base是类A和B的公有基类,所以类C的继承体系中将存在base的两个副本。这就导致了用域限定符两次访问x时,访问的是不同的x,产生了数据冗余。 利用虚基类对上述示例进行改造,让base成为A和B的虚基类,就可避免这种二义性的存在。,【示例15-12】,用虚继承解决示例15-11中的二义性问题。 分析:由于将Base到A和B的派生改成了虚继承关系,则C的继承体系中将只存在Base的一个副本。所以直接从C中访问x就是明确无误的,而且任何形式对x的赋值都是在操作同一个x。,15.5.2 初始化虚基类,在类的继承体系中,由于继承体系的层次可能很深,所以习惯上将实例化某个类对象时所用的类称为最远派生类。 为了初始化基类的成员,派生类的构造函数要调用基类的构造函数。由于虚基类在整个继承体系中只有一个实例,所以虚基类的构造函数只能被执行一次。为了保证这一点,C+规定,虚基类的成员由最远派生类的构造函数通过调用虚基类的构造函数进行初始化。在整个继承体系中,直接或间接继承虚基类的所有派生类,都必须在构造函数的成员初始化表中给出对虚基类的构造函数的调用。如果未列出,则表示调用该虚基类的默认构造函数。但是在建立对象时,只有最远派生类的构造函数才真正调用虚基类的构造函数,其他基类对虚基类构造函数的调用都将被忽略。,这样可能就会使得某些上层子类得到的虚基类子对象的状态不是自己所期望的(因为自己的初始化语句被忽略了),所以一般建议不要在虚基类中包含任何数据成员,只作为接口类来提供。,【示例15-13】,演示虚继承的构造函数和析构函数的调用顺序。 分析:派生类C继承于base2、A、B,而A和B又虚继承于base1。因此base1是最远的基类,且是虚基类,故在构造函数中会最先产生它的副本,最先调用它的构造函数。然后base2和A、B都是C的公有基类,系统将按照声明C时的基类顺序调用它们的构造函数。,通过上面的分析,可以将15.4节中多重继承的构造函数和析构函数的调用顺序完善如下。 (1)如果继承体系中有虚基类,则首先执行虚基类的构造函数,多个虚基类的构造函数按照被继承的顺序执行; (2)其次执行非虚基类的构造函数,多个非虚基类的构造函数也按照被继承的顺序执行; (3)然后执行成员对象的构造函数,多个成员对象的构造函数按照声明的顺序构造; (4)最后执行派生类自己的构造函数。 析构函数

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论