《C++面向对象程序设计》由会员分享,可在线阅读,更多相关《C++面向对象程序设计(106页珍藏版)》请在金锄头文库上搜索。
1、第第5章章 继承与派生继承与派生5.1 继承与派生的概念继承与派生的概念5.2 派生类的声明方式派生类的声明方式5.3 派生类的构成派生类的构成5.4 派生类成员的访问属性派生类成员的访问属性5.5 派生类的构造函数和析构函数派生类的构造函数和析构函数5.6 多重继承多重继承5.7 基类与派生类的转换基类与派生类的转换5.8 继承与组合继承与组合5.9 继承在软件开发中的重要意义继承在软件开发中的重要意义 面向对象程序设计有面向对象程序设计有4个主要特点个主要特点: 抽象、封装、抽象、封装、继承和多态性。类的继承机制解决了软件重用问题。继承和多态性。类的继承机制解决了软件重用问题。5.1 继承
2、与派生的概念继承与派生的概念 一个新类从已有的类那里获得其已有特性(数一个新类从已有的类那里获得其已有特性(数据成员和函数成员),这种现象称为类的继承。据成员和函数成员),这种现象称为类的继承。通通过继承,一个新建子类从已有的父类那里获得父类过继承,一个新建子类从已有的父类那里获得父类的特性。从另一角度说,的特性。从另一角度说,从已有的类从已有的类(父类父类)产生一产生一个新的子类,称为类的派生。个新的子类,称为类的派生。类的继承是用已有的类的继承是用已有的类来建立专用类的编程技术。派生类继承了基类的类来建立专用类的编程技术。派生类继承了基类的所有数据成员和成员函数,并可以对成员作必要的所有数
3、据成员和成员函数,并可以对成员作必要的增加或调整。增加或调整。一个基类可以派生出多个派生类,每一个基类可以派生出多个派生类,每一个派生类又可以作为基类再派生出新的派生类。一个派生类又可以作为基类再派生出新的派生类。 继承机制主要解决下面的问题:当两个类的内容继承机制主要解决下面的问题:当两个类的内容(数据成员和成员函数)基本相同或有一部分相同时,(数据成员和成员函数)基本相同或有一部分相同时,可以利用原来声明的类作为基础,再加上新的内容,可以利用原来声明的类作为基础,再加上新的内容,以减少重复的工作量。如图所示:以减少重复的工作量。如图所示: 一个派生类只从一个基类派生,这称为一个派生类只从一
4、个基类派生,这称为单继承单继承,这种继承关系所形成的层次是一个树形结构,可以这种继承关系所形成的层次是一个树形结构,可以用图用图5.3表示。表示。图图5.3 请注意图中箭头的方向,在本课程中约定,请注意图中箭头的方向,在本课程中约定,箭头表示继承的方向,从派生类指向基类。箭头表示继承的方向,从派生类指向基类。 一个派生类不仅可以从一个基类派生,也可以从一个派生类不仅可以从一个基类派生,也可以从多个基类派生。一个派生类有两个或多个基类的称为多个基类派生。一个派生类有两个或多个基类的称为多重继承多重继承(multiple inheritance),这种继承关系所形这种继承关系所形成的结构如下图所示
5、。成的结构如下图所示。 基类和派生类的关系是基类和派生类的关系是: 派生类是基类的具体化,派生类是基类的具体化,而基类则是派生类的抽象。如下图所示而基类则是派生类的抽象。如下图所示:5.2 派生类的声明方式派生类的声明方式声明派生类的一般形式为声明派生类的一般形式为:class 派生类名派生类名: 继承方式继承方式 基类名基类名派生类新增加的成员派生类新增加的成员 ; 继承方式包括继承方式包括: public,private和和protected。此项是可选的,如果不写此项,则默认为此项是可选的,如果不写此项,则默认为private。 假设已经声明了一个基类假设已经声明了一个基类Student
6、,在此基础在此基础上通过单继承建立一个派生类上通过单继承建立一个派生类Student1:class Student1: public Student /声明基类是声明基类是Student public:void display_1( ) /新增加的成员函数新增加的成员函数 coutage: ageendl; coutaddress: addrnumnamesex;void display( ) cout num: numendl; cout name: nameendl; cout sex: sexendl;private : /基类私有成员基类私有成员 int num; string nam
7、e; char sex; class Student1: public Student /以以public方式声明派生类方式声明派生类Student1 public: void display_1( ) coutnum: numendl; /企图引用基类的私有成企图引用基类的私有成员,员,错误错误 coutname: nameendl; /企图引用基类的私有成员,企图引用基类的私有成员,错误错误 cout sex: sexendl; /企图引用基类的私有成员,企图引用基类的私有成员,错误错误 coutage: ageendl; /引用派生类的私有成员,正确引用派生类的私有成员,正确 cout
8、address: addrendl; /引用派生类的私有成员,正确引用派生类的私有成员,正确 private: int age; string addr; ;如何对上面的派生类进行修改?如何对上面的派生类进行修改?假设对象假设对象stud中已有数据,可以将中已有数据,可以将display_1( )定义如定义如下下: void display_1( ) coutage: ageendl; /引用派生类的私有成员,正确引用派生类的私有成员,正确 cout address: addrendl; /引用派生类的私有成员,正确引用派生类的私有成员,正确 并将并将main函数()写为函数()写为: int
9、 main( ) Student1 stud;/定义派生类定义派生类Student1的对象的对象stud stud.display( ); /调用基类公用成员函数调用基类公用成员函数 stud.display_1(); /调用派生类的公用成员函数调用派生类的公用成员函数 return 0; 在声明一个派生类时将基类的继承方式指定为在声明一个派生类时将基类的继承方式指定为private的,称为私有继承,的,称为私有继承,用私有继承方式建立的派用私有继承方式建立的派生类称为私有派生类生类称为私有派生类(private derived class) ,其基其基类称为私有基类类称为私有基类(priva
10、te base class)。 私有基类的公用成员和保护成员在派生类中的访私有基类的公用成员和保护成员在派生类中的访问属性相当于派生类中的私有成员,问属性相当于派生类中的私有成员,即派生类的成员即派生类的成员函数能访问它们,而在派生类外不能访问。私有基类函数能访问它们,而在派生类外不能访问。私有基类的私有成员在派生类中成为不可访问的成员,只有基的私有成员在派生类中成为不可访问的成员,只有基类的成员函数可以引用它们。类的成员函数可以引用它们。一个基类成员在基类中一个基类成员在基类中的访问属性和在派生类中的访问属性可能是不同的的访问属性和在派生类中的访问属性可能是不同的(取决于继承方式取决于继承方
11、式)。)。私有基类的成员在私有派生类私有基类的成员在私有派生类中的访问属性见表中的访问属性见表5.2。5.4.2 私有继承私有继承图图5.7 各成员在派生类中的访问属性。各成员在派生类中的访问属性。例例5.2 将例将例5.1中的继承方式改为用私有继承方式。中的继承方式改为用私有继承方式。class Student1: private Student public: void display_1( ) /输出两个数据成员的值输出两个数据成员的值 coutage:ageendl; /引用派生类的私有成员引用派生类的私有成员 coutaddress:addrendl; /引用派生类的私有成员引用派生
12、类的私有成员private: int age; string addr;int main( ) Student1 stud1; /定义一个定义一个Student1类的对象类的对象stud1 stud1.display(); /错误,错误, display()在派生类中是私有函数在派生类中是私有函数 stud1.display_1( ); /正确,正确,Display_1函数是派生类公用函数函数是派生类公用函数 stud1.age=18; /错误。外界不能引用派生类的私有成员错误。外界不能引用派生类的私有成员 return 0; 可以看到可以看到: (1) 不能通过派生类不能通过派生类对象对象(
13、如如stud1)引用从私有引用从私有基类继承过来的任何成员基类继承过来的任何成员(如如stud1.display()或或stud1.num)。(2) 派生类的派生类的成员函数成员函数不能访问私有基类的私有不能访问私有基类的私有成员,但可以访问私有基类的公用成员成员,但可以访问私有基类的公用成员(如如stud1.display_1函数可以调用基类的公用成函数可以调用基类的公用成员函数员函数display,但不能引用基类的私有成员但不能引用基类的私有成员num)。(3)私有基类的公用成员函数调用方法私有基类的公用成员函数调用方法:通过派:通过派生类的成员函数调用私有基类的公用成员函生类的成员函数调
14、用私有基类的公用成员函数,从而引用私有基类的私有成员。数,从而引用私有基类的私有成员。将上面的私有派生类的成员函数定义改写为:将上面的私有派生类的成员函数定义改写为:void display_1( )/输出输出5个数据成员的值个数据成员的值 display(): /调用基类的公用成员函数调用基类的公用成员函数 coutage: ageendl; /输出派生类的私有数据成员输出派生类的私有数据成员 cout “ address: ”addrendl; /同上同上int main( ) Student1 stud1; stud1.display_1( );/display_1是派生类的公用函数是派
15、生类的公用函数 return 0; 由于私有派生类限制太多,使用不方便,一由于私有派生类限制太多,使用不方便,一般不常使用。般不常使用。 从类的用户角度来看,保护成员等价于私从类的用户角度来看,保护成员等价于私有成员。但有一点与私有成员不同,保护成员有成员。但有一点与私有成员不同,保护成员可以被派生类的成员函数引用,如下图可以被派生类的成员函数引用,如下图。5.4.3 保护成员和保护继承保护成员和保护继承 在基类中的私有成员,任何派生类都不能访问。在基类中的私有成员,任何派生类都不能访问。若希望在派生类中能访问它们,应当把它们声明为保若希望在派生类中能访问它们,应当把它们声明为保护成员。一个类
16、中声明了保护成员,意味着该类可能护成员。一个类中声明了保护成员,意味着该类可能要用作基类。要用作基类。 在定义一个派生类时将基类的继承方式指定为在定义一个派生类时将基类的继承方式指定为protected的,称为保护继承的,称为保护继承,用保护继承方式建立的,用保护继承方式建立的派生类称为保护派生类派生类称为保护派生类(protected derived class),其其基类称为受保护的基类基类称为受保护的基类(protected base class),简称简称保护基类。保护基类。 保护继承的特点是保护继承的特点是: 保护基类的公用成员和保护保护基类的公用成员和保护成员在派生类中都成了保护成
17、员,其私有成员仍为基成员在派生类中都成了保护成员,其私有成员仍为基类私有。也就是把基类原有的公用成员也保护起来,类私有。也就是把基类原有的公用成员也保护起来,不让类外任意访问。不让类外任意访问。 基类成员在派生类中的访问属性汇总表:基类成员在派生类中的访问属性汇总表:表表5.3 上表可以看出上表可以看出: (1)基类的私有成员被派生类继承后变为不可)基类的私有成员被派生类继承后变为不可访问的成员,派生类中的一切成员均无法访问它们。访问的成员,派生类中的一切成员均无法访问它们。如果需要在派生类中引用基类的某些成员,应当将基如果需要在派生类中引用基类的某些成员,应当将基类的这些成员声明为类的这些成
18、员声明为protected,而不要声明为而不要声明为private。 (2)比较私有继承和保护继承中的访问属性可)比较私有继承和保护继承中的访问属性可以发现:在直接派生类中,以上两种继承方式的作用以发现:在直接派生类中,以上两种继承方式的作用实际上是相同的实际上是相同的,但是如果继续派生,在新的派生类但是如果继续派生,在新的派生类中,两种继承方式的作用就不同了。中,两种继承方式的作用就不同了。 如果善于利用保护成员,可以在类的层次结构中如果善于利用保护成员,可以在类的层次结构中找到数据共享与成员隐蔽之间的结合点。既可实现某找到数据共享与成员隐蔽之间的结合点。既可实现某些成员的隐蔽,又可方便地继
19、承,能实现代码重用与些成员的隐蔽,又可方便地继承,能实现代码重用与扩充。扩充。小结小结: (1)在派生类中,成员有在派生类中,成员有4种不同的访问属性种不同的访问属性: 公用:派生类内和派生类外都可以访问。公用:派生类内和派生类外都可以访问。 受保护:派生类内可以访问,派生类外不能访受保护:派生类内可以访问,派生类外不能访问,其下一层的派生类可以访问。问,其下一层的派生类可以访问。 私有:派生类内可以访问,派生类外不能访问。私有:派生类内可以访问,派生类外不能访问。 不可访问:派生类内和派生类外都不能访问。不可访问:派生类内和派生类外都不能访问。说明说明: 访问属性是指在派生类中所获得的访问属
20、性。访问属性是指在派生类中所获得的访问属性。 在派生类外部是指在建立派生类对象的模块中、在派生类外部是指在建立派生类对象的模块中、在派生类范围之外。在派生类范围之外。 如果派生类继续派生,则在不同的继承方式下,如果派生类继续派生,则在不同的继承方式下,成员所获得的访问属性是不同的。成员所获得的访问属性是不同的。(2) 类的成员在不同作用域中有不同的访问属性。类的成员在不同作用域中有不同的访问属性。派生类的成员的访问属性如下表所示:派生类的成员的访问属性如下表所示:例例5.3 在派生类中引用保护成员。在派生类中引用保护成员。#include class Student /声明基类声明基类 pub
21、lic: /基类公用成员基类公用成员 void display( ); protected : /基类保护成员基类保护成员int num;string name;char sex;void Student:display( ) /定义基类成员函数定义基类成员函数 coutnum: numendl; coutname: nameendl; coutsex: sexendl; class Student1: protected Student /用用protected方式声明派生类方式声明派生类Student1public:void display1( ); /派生类公用成员函数派生类公用成员函数
22、private:int age; /派生类私有数据成员派生类私有数据成员string addr; /派生类私有数据成员派生类私有数据成员;void Student1:display1( ) /定义派生类公用成员函数定义派生类公用成员函数 coutnum: numendl; /引用基类的保护成员,合法引用基类的保护成员,合法 coutname: nameendl; /引用基类的保护成员,合法引用基类的保护成员,合法 coutsex: sexendl; /引用基类的保护成员,合法引用基类的保护成员,合法 coutage: ageendl; /引用派生类的私有成员,合法引用派生类的私有成员,合法 c
23、outaddress: addrendl; /引用派生类的私有成员,合法引用派生类的私有成员,合法 int main( ) Student1 stud1; /stud1是派生类是派生类Student1类的对象类的对象 stud1.display1( ); /合法,合法,display1是派生类中的公用成员函数是派生类中的公用成员函数 stud1.num=10023; /错误,外界不能访问保护成员错误,外界不能访问保护成员 return 0; 请补充、修改上面的程序,使之能正常运行。程请补充、修改上面的程序,使之能正常运行。程序中应包括输入数据的函数。序中应包括输入数据的函数。 私有继承和保护继
24、承方式在使用时需要十分小心,私有继承和保护继承方式在使用时需要十分小心,很容易搞错,一般不常用。很容易搞错,一般不常用。 如果有上图所示的派生关系如果有上图所示的派生关系: 类类A为基类,类为基类,类B是类是类A的派生类,类的派生类,类C是类是类B的派生类,则类的派生类,则类C也是类也是类A的派生类。类的派生类。类B称为类称为类A的直接派生类,类的直接派生类,类C称为类称为类A的间接派生类。类的间接派生类。类A是类是类B的直接基类,是类的直接基类,是类C的间的间接基类。在多级派生的情况下,各成员的访问属性仍接基类。在多级派生的情况下,各成员的访问属性仍按以上原则确定。按以上原则确定。5.4.4
25、 多级派生时的访问属性多级派生时的访问属性例例5.4 多级派生的访问属性示例多级派生的访问属性示例。class A /基类基类 public: int i; protected: void f2( ); int j; private: int k;class B: public A /public方式方式 public:void f3( ); protected:void f4( ); private: int m;class C: protected B /protected方式方式 public:void f5( ); private: int n; 类类A是类是类B的公用基类,类的公用基
26、类,类B是类是类C的保护基类。的保护基类。各成员在不同类中的访问属性如下各成员在不同类中的访问属性如下:if2jk f3f4 m f5 n基类基类A公公用用 保保护护 保保护护 私私有有公公 用用派派 生生类类B 公公用用 保保护护 保保护护 不不可可访问访问 公公用用 保保护护 私私有有 保保 护护派派 生生类类C 保保护护 保保护护 保保护护 不不可可访问访问 保保护护 保保护护 不不可可访问访问 公公用用 私私有有 无论哪一种继承方式,在派生类中都不能访问基无论哪一种继承方式,在派生类中都不能访问基类的私有成员,它只能被本类的成员函数所访问。类的私有成员,它只能被本类的成员函数所访问。
27、如果在多级派生时都采用公用继承方式,那么直如果在多级派生时都采用公用继承方式,那么直到最后一级派生类都能访问基类的公用成员和保护成到最后一级派生类都能访问基类的公用成员和保护成员。员。 如果采用私有继承方式,经过若干次派生之后,如果采用私有继承方式,经过若干次派生之后,基类的所有的成员已经变成不可访问的了。基类的所有的成员已经变成不可访问的了。 如果采用保护继承方式,在派生类外是无法访问如果采用保护继承方式,在派生类外是无法访问派生类中的任何成员的。而且经过多次派生后,人们派生类中的任何成员的。而且经过多次派生后,人们很难清楚地记住哪些成员可以访问,哪些成员不能访很难清楚地记住哪些成员可以访问
28、,哪些成员不能访问,很容易出错。问,很容易出错。 在实际中,常用的是公用继承。在实际中,常用的是公用继承。 构造函数的主要作用是对数据成员初始化。如果构造函数的主要作用是对数据成员初始化。如果需要对类中的数据成员初始化,应自己定义构造函数。需要对类中的数据成员初始化,应自己定义构造函数。在设计派生类的构造函数时,不仅要考虑派生类所增在设计派生类的构造函数时,不仅要考虑派生类所增加的数据成员的初始化,还应当考虑基类的数据成员加的数据成员的初始化,还应当考虑基类的数据成员初始化初始化,以便使派生类的数据成员和基类的数据成员以便使派生类的数据成员和基类的数据成员同时都被初始化。同时都被初始化。 解决
29、这个问题的思路是解决这个问题的思路是: 在执行派生类在执行派生类的构造函数时,调用基类的构造函数。的构造函数时,调用基类的构造函数。5.5 派生类的构造函数和析构函数派生类的构造函数和析构函数 任何派生类都包含基类的成员,简单的派生类任何派生类都包含基类的成员,简单的派生类只有一个基类,而且只有一级派生只有一个基类,而且只有一级派生(只有直接派生类,只有直接派生类,没有间接派生类没有间接派生类),在派生类的数据成员中不包含基,在派生类的数据成员中不包含基类的对象类的对象(即子对象即子对象)。例例5.5 简单的派生类的构造函数。简单的派生类的构造函数。#include class Student
30、 /声明基类声明基类Student public: Student(int n,string nam,char s) /基类构造函数基类构造函数 num=n; name =nam; sex=s; Student( ) /基类析构函数基类析构函数 protected: /保护部分保护部分 int num; string name; char sex ; ;5.5.1 简单的派生类的构造函数简单的派生类的构造函数class Student1: public Student /声明派生类声明派生类Student1 public: /派生类的公用部分派生类的公用部分 Student1(int n,st
31、ring nam,char s,int a,string ad):Student(n,nam,s) age=a; /派生类新增的数据成员初始化派生类新增的数据成员初始化 addr=ad; /派生类新增的数据成员初始化派生类新增的数据成员初始化 /以上为派生类构造函数以上为派生类构造函数 void show( ) coutnum: numendl; coutname: nameendl; coutsex: sexendl; coutage: ageendl; coutaddress: addrendlendl; Student1( ) /派生类析构函数派生类析构函数 private: /派生类的
32、私有部分派生类的私有部分 int age; string addr; ;int main( ) Student1 stud1(10010,Wang-li,f,19,115 Beijing Road,Shanghai); Student1 stud2(10011,Zhang-fun,m,21,213 Shanghai Road,Beijing); stud1.show( ); /输出第一个学生的数据输出第一个学生的数据 stud2.show( ); /输出第二个学生的数据输出第二个学生的数据 return 0; 运行结果为运行结果为:num:10010name:Wang-lisex:faddre
33、ss: 115 Beijing Road,Shanghainum:10011name:Zhang-funsex:maddress: 213 Shanghai Road,Beijing1、派生类构造函数的声明方法、派生类构造函数的声明方法:派生类构造函数名(总参数表列)派生类构造函数名(总参数表列): 基类构造函数名(参数表列)基类构造函数名(参数表列) 派生类中新增数据成员初始化语句派生类中新增数据成员初始化语句2、参数传递过程、参数传递过程 在在main函数中,建立对象函数中,建立对象stud1时指定了时指定了5个实参。个实参。它们依次传递给派生类构造函数它们依次传递给派生类构造函数Stud
34、ent1的形参。派生的形参。派生类构造函数将前面类构造函数将前面3个传递给基类构造函数的形参。个传递给基类构造函数的形参。通过通过Student (n, nam, s)把把3个值再传给基类个值再传给基类构造函数的形参,如下图所示。构造函数的形参,如下图所示。Student( n, nam, s ) Student(int n,string nam,char s)/基类构造函数的首部基类构造函数的首部3、派生类构造函数的实现、派生类构造函数的实现 (1)派生类构造函数也可以在类外面定义,而在)派生类构造函数也可以在类外面定义,而在类体中只写该函数的声明。类体中只写该函数的声明。 (2)在类中对派
35、生类构造函数作声明时,不包)在类中对派生类构造函数作声明时,不包括基类构造函数名及其参数表列(即括基类构造函数名及其参数表列(即Student(n,nam,s))。)。只在定义函数时才将它列出。只在定义函数时才将它列出。 (3)调用基类构造函数时的实参是从派生类构造函)调用基类构造函数时的实参是从派生类构造函数的总参数表中得到的,也可以不从派生类构造函数的数的总参数表中得到的,也可以不从派生类构造函数的总参数表中传递过来,而直接使用常量或全局变量。例总参数表中传递过来,而直接使用常量或全局变量。例如,派生类构造函数首行可以写成以下形式如,派生类构造函数首行可以写成以下形式: Student1(
36、string nam,char s,int a,string ad) : Student(10010,nam,s) (4)可以利用初始化表对构造函数的数据成员可以利用初始化表对构造函数的数据成员初始化,而且可以利用初始化表调用派生类的基类构初始化,而且可以利用初始化表调用派生类的基类构造函数,实现对基类数据成员的初始化。也可以在同造函数,实现对基类数据成员的初始化。也可以在同一个构造函数的定义中同时实现这两种功能。如下例:一个构造函数的定义中同时实现这两种功能。如下例:Student1(int n, string nam,char s,int a, string ad):Student(n,n
37、am,s) age=a;/在函数体中对派生类数据成员初始化在函数体中对派生类数据成员初始化 addr=ad; 可将构造函数改写为以下形式可将构造函数改写为以下形式: Student1(int n, string nam,char s,int a, string ad):Student(n,nam,s),age(a),addr(ad) 这样函数体为空,更显得简单和方便。这样函数体为空,更显得简单和方便。4、构造函数和析构函数的执行顺序、构造函数和析构函数的执行顺序 在建立一个对象时,执行构造函数的顺序是在建立一个对象时,执行构造函数的顺序是: 派生类构造函数先调用基类构造函数;派生类构造函数先调
38、用基类构造函数;再执行派生再执行派生类构造函数本身类构造函数本身(即派生类构造函数的函数体即派生类构造函数的函数体)。 在派生类对象释放时,析构函数的执行顺序与在派生类对象释放时,析构函数的执行顺序与构造函数相反。即先执行派生类析构函数构造函数相反。即先执行派生类析构函数,再执行其再执行其基类析构函数基类析构函数。 如果类的数据成员中包含有类的对象,则该数据如果类的数据成员中包含有类的对象,则该数据成员就是类对象中的内嵌对象,称为子对象。成员就是类对象中的内嵌对象,称为子对象。例例5.6 包含子对象的派生类的构造函数。包含子对象的派生类的构造函数。#include #include class
39、 Student /声明基类声明基类 public: /公用部分公用部分Student(int n, string nam ) /基类构造函数,与例基类构造函数,与例11.5相同相同 num=n; name=nam; void display( ) /成员函数,输出基类数据成员成员函数,输出基类数据成员 coutnum:numendlname:nameendl; protected: /保护部分保护部分int num; string name;5.5.2 有子对象的派生类的构造函数有子对象的派生类的构造函数class Student1: public Student /声明公用派生类声明公用派
40、生类Student1 public: Student1(int n, string nam,int n1, string nam1,int a, string ad) :Student(n,nam),monitor(n1,nam1) /派生类构造函数派生类构造函数 age=a; addr=ad; void show( ) coutThis student is:endl; display( ); /输出输出num和和name coutage: ageendl; /输出输出age coutaddress: addrendlendl; /输出输出addr void show_monitor( )
41、/成员函数,输出子对象成员函数,输出子对象 coutendlClass monitor is:endl; monitor.display( ); /调用基类成员函数调用基类成员函数 private: /派生类的私有数据派生类的私有数据 Student monitor; /定义子对象定义子对象(班长班长) int age; string addr; ;int main( ) Student1 stud1(10010,Wang-li,10001,Li- sun,19,115 Beijing Road,Shanghai); stud1.show( ); /输出学生的数据输出学生的数据 stud1.s
42、how_monitor(); /输出子对象的数据输出子对象的数据 return 0;运行结果如下运行结果如下: This student is:num: 10010name: Wang-liage: 19address:115 Beijing Road,Shanghai Class monitor is:num:10001name:Li-sun1、派生类构造函数的任务、派生类构造函数的任务 (1) 对基类数据成员初始化;对基类数据成员初始化; (2) 对子对象数据成员初始化;对子对象数据成员初始化; (3) 对派生类数据成员初始化。对派生类数据成员初始化。程序中派生类构造函数首部如下:程序中派
43、生类构造函数首部如下:Student1(int n, string nam,int n1, string nam1,int a, string ad):Student(n,nam),monitor(n1,nam1)其构造函数的参数传递如下图所示。其构造函数的参数传递如下图所示。2、定义派生类构造函数的一般形式为、定义派生类构造函数的一般形式为派生类构造函数名(总参数表列)派生类构造函数名(总参数表列): 基类构造函数名基类构造函数名(参数表列),子对象名(参数表列),子对象名(参数表列参数表列) 派生类中新增数成员据成员初始化语句派生类中新增数成员据成员初始化语句3、执行派生类构造函数的顺序是
44、、执行派生类构造函数的顺序是: 调用基类构造函数,对基类数据成员初始化;调用基类构造函数,对基类数据成员初始化; 调用子对象构造函数,对子对象数据成员初始化;调用子对象构造函数,对子对象数据成员初始化; 再执行派生类构造函数本身,对派生类数据成员初再执行派生类构造函数本身,对派生类数据成员初始化。始化。4、说明、说明 派生类构造函数的总参数表列中的参数,应当包派生类构造函数的总参数表列中的参数,应当包括基类构造函数和子对象的参数表列中的参数。基类括基类构造函数和子对象的参数表列中的参数。基类构造函数和子对象的次序可以是任意的。编译系统是构造函数和子对象的次序可以是任意的。编译系统是根据相同的参
45、数名根据相同的参数名(而不是根据参数的顺序而不是根据参数的顺序)来确立它来确立它们的传递关系的。但是们的传递关系的。但是习惯上一般先写基类构造函数。习惯上一般先写基类构造函数。 如果有多个子对象,派生类构造函数的写法依此如果有多个子对象,派生类构造函数的写法依此类推,应列出每一个子对象名及其参数表列。类推,应列出每一个子对象名及其参数表列。 一个类不仅可以派生出一个派生类,派生类还可一个类不仅可以派生出一个派生类,派生类还可以继续派生,形成派生的层次结构。以继续派生,形成派生的层次结构。例例5.7 多级派生情况下派生类的构造函数。多级派生情况下派生类的构造函数。#include #includ
46、eusing namespace std;class Student /声明基类声明基类 public: /公用部分公用部分 Student(int n, string nam ) /基类构造函数基类构造函数 num=n; name=nam; void display( ) /输出基类数据成员输出基类数据成员coutnum:numendl; coutname:nameendl; protected: /保护部分保护部分 int num; /基类有两个数据成员基类有两个数据成员 string name;5.5.3 多层派生时的构造函数多层派生时的构造函数class Student1: publi
47、c Student /声明公用派生类声明公用派生类Student1 public: Student1(int n,char nam10,int a):Student(n,nam)/派生类构造函数派生类构造函数 age=a; /对派生类新增的数据成员初始化对派生类新增的数据成员初始化 void show( ) /输出输出num,name和和age display( ); /输出输出num和和name coutage: ageendl; private: /派生类的私有数据派生类的私有数据 int age; /增加一个数据成员增加一个数据成员 ;class Student2:public Stud
48、ent1 /声明间接公用派生类声明间接公用派生类Student2 public:Student2(int n, string nam,int a,int s):Student1(n,nam,a)score=s; void show_all( ) /输出全部数据成员输出全部数据成员show( ); /输出输出num和和name coutscore:scoreendl; /输出输出ageprivate: int score; /增加一个数据成员增加一个数据成员 ;int main( ) Student2 stud(10010,Li,17,89); stud.show_all( ); /输出学生的全
49、部数据输出学生的全部数据 return 0; 运行时的输出如下运行时的输出如下: num:10010name:Liage:17score:89 其派生关系如图其派生关系如图5.13所示。所示。图图5.131、基类和其派生类构造函数的写法、基类和其派生类构造函数的写法: 基类的构造函数首部基类的构造函数首部: Student(int n, string nam)派生类派生类Student1的构造函数首部的构造函数首部: Student1(int n, string nam,int a):Student(n,nam) 派生类派生类Student2的构造函数首部的构造函数首部: Student2(i
50、nt n, string nam,int a,int s):Student1(n,nam,a)2、构造函数的执行顺序:、构造函数的执行顺序: Student( ) Student1( ) Student2( )3、数据成员的初始化顺序、数据成员的初始化顺序: 先初始化基类的数据成员先初始化基类的数据成员num和和name。 再初始化再初始化Student1的数据成员的数据成员age。 最后再初始化最后再初始化Student2的数据成员的数据成员score。在使用派生类构造函数时,有以下特殊的形式在使用派生类构造函数时,有以下特殊的形式: (1) 当不需要对派生类新增的成员进行任当不需要对派生类
51、新增的成员进行任何初始化操作时,派生类构造函数的函数体可以为何初始化操作时,派生类构造函数的函数体可以为空,即构造函数是空函数。如:空,即构造函数是空函数。如:Student1(int n, string nam, int n1, string nam1):Student(n,nam),monitor(n1,nam1) 其作用是将参数传递给基类构造函数和子对象,其作用是将参数传递给基类构造函数和子对象,并在执行派生类构造函数时调用基类构造函数和子并在执行派生类构造函数时调用基类构造函数和子对象构造函数。在实际工作中常见这种用法。对象构造函数。在实际工作中常见这种用法。5.5.4 派生类构造函数
52、的特殊形式派生类构造函数的特殊形式 (2) 如果在基类中没有定义构造函数,或如果在基类中没有定义构造函数,或定义了没有参数的构造函数,那么在定义派生类构造定义了没有参数的构造函数,那么在定义派生类构造函数时可不写基类构造函数。调用派生类构造函数时函数时可不写基类构造函数。调用派生类构造函数时系统会自动首先调用基类的默认构造函数。系统会自动首先调用基类的默认构造函数。(3)如果在基类和子对象类型的声明中都如果在基类和子对象类型的声明中都没有定义带参数的构造函数,而且也不需对派生类自没有定义带参数的构造函数,而且也不需对派生类自己的数据成员初始化,则可以不必显式地定义派生类己的数据成员初始化,则可
53、以不必显式地定义派生类构造函数。在建立派生类对象时,系统会自动调用系构造函数。在建立派生类对象时,系统会自动调用系统提供的派生类的默认构造函数,并在执行派生类默统提供的派生类的默认构造函数,并在执行派生类默认构造函数的过程中,调用基类的默认构造函数和子认构造函数的过程中,调用基类的默认构造函数和子对象类型默认构造函数。对象类型默认构造函数。 (4)如果在基类或子对象类型的声明中定如果在基类或子对象类型的声明中定义了带参数的构造函数,那么就必须显式地定义派生义了带参数的构造函数,那么就必须显式地定义派生类构造函数,并在派生类构造函数中写出基类或子对类构造函数,并在派生类构造函数中写出基类或子对象
54、类型的构造函数及其参数表。象类型的构造函数及其参数表。(5)如果在基类中既定义无参的构造函数,如果在基类中既定义无参的构造函数,又定义了有参的构造函数又定义了有参的构造函数(构造函数重载构造函数重载),则在定义,则在定义派生类构造函数时,既可以包含基类构造函数及其参派生类构造函数时,既可以包含基类构造函数及其参数,也可以不包含基类构造函数。在调用派生类构造数,也可以不包含基类构造函数。在调用派生类构造函数时,根据构造函数的内容决定调用基类的有参的函数时,根据构造函数的内容决定调用基类的有参的构造函数还是无参的构造函数。编程者可以根据派生构造函数还是无参的构造函数。编程者可以根据派生类的需要决定
55、采用哪一种方式。类的需要决定采用哪一种方式。 派生类不能继承基类的析构函数,所以派生类不能继承基类的析构函数,所以如果需要的话,程序员应当在派生类中定义自己的析如果需要的话,程序员应当在派生类中定义自己的析构函数,用来对派生类中所增加的成员进行清理工作。构函数,用来对派生类中所增加的成员进行清理工作。在执行派生类的析构函数时,系统会自动调用基类的在执行派生类的析构函数时,系统会自动调用基类的析构函数和子对象的析构函数,对基类和子对象进行析构函数和子对象的析构函数,对基类和子对象进行清理。清理。 析构函数的调用顺序与构造函数正好相析构函数的调用顺序与构造函数正好相反反: 先执行派生类自己的析构函
56、数,对派生类新增加先执行派生类自己的析构函数,对派生类新增加的成员进行清理,然后调用子对象的析构函数,对子的成员进行清理,然后调用子对象的析构函数,对子对象进行清理,最后调用基类的析构函数,对基类进对象进行清理,最后调用基类的析构函数,对基类进行清理。行清理。5.5.5 派生类的析构函数派生类的析构函数 一个派生类有两个或多个基类,派生类从一个派生类有两个或多个基类,派生类从两个或多个基类中继承所需的属性,称为两个或多个基类中继承所需的属性,称为多重继承多重继承。5.6.1 声明多重继承的方法声明多重继承的方法 如果已声明了类如果已声明了类A、类类B和类和类C,可以声可以声明多重继承的派生类明
57、多重继承的派生类D: class D:public A,private B,protected C 类类D新增加的成员新增加的成员 ; D是多重继承的派生类,是多重继承的派生类,D按不同的继承按不同的继承方式的规则继承了类方式的规则继承了类A,B,C的属性,并按其不同的继的属性,并按其不同的继承方式确定各基类的成员在派生类中的访问权限。承方式确定各基类的成员在派生类中的访问权限。5.6 多重继承多重继承 多重继承派生类的构造函数形式与单继多重继承派生类的构造函数形式与单继承时的构造函数形式基本相同,只是在初始表中包含承时的构造函数形式基本相同,只是在初始表中包含多个基类构造函数。其一般形式如下
58、:多个基类构造函数。其一般形式如下:派生类构造函数名派生类构造函数名(总参数表列总参数表列): 基类基类1构造函数构造函数(参数表列参数表列), 基类基类2构造函数构造函数(参数表列参数表列), 基类基类3构造函数构造函数 (参数表列参数表列) 派生类中新增数成员据成员初始化语句派生类中新增数成员据成员初始化语句其中:其中: (1)各基类的排列顺序任意。各基类的排列顺序任意。(2)派生类构派生类构造函数的执行顺序同样为造函数的执行顺序同样为: 先调用基类的构造函数,先调用基类的构造函数,再执行派生类构造函数的函数体。再执行派生类构造函数的函数体。调用基类构造函数调用基类构造函数的顺序是按照声明
59、派生类时基类出现的顺序的顺序是按照声明派生类时基类出现的顺序。5.6.2 多重继承派生类的构造函数多重继承派生类的构造函数 例例5.8 声明一个教师声明一个教师(Teacher)类和一个学生类和一个学生(Student)类,用多重继承的方式声明一个研究生类,用多重继承的方式声明一个研究生(Graduate)派生类。派生类。说明:说明: 教师类中包括数据成员教师类中包括数据成员name(姓名姓名)、age(年龄年龄)、title(职称职称)。学生类中包括数据成员。学生类中包括数据成员name1(姓名姓名)、age(性别性别)、score(成绩成绩)。在定义派生类对象时。在定义派生类对象时给出初始
60、化的数据,然后输出这些数据。给出初始化的数据,然后输出这些数据。#include #include class Teacher /声明类声明类Teacher(教师教师) public: /公用部分公用部分 Teacher(string nam,int a, string t) /构造函数构造函数 name=nam; age=a; title=t; void display( ) /输出教师有关数据输出教师有关数据 coutname:nameendl; coutageageendl; couttitle:titleendl; protected: /保护部分保护部分 string name; i
61、nt age; string title; /职称职称;class Student /定义类定义类Student(学生学生) public: Student(char nam,char s,float sco) strcpy(name1,nam); sex=s; score=sco; /构造函数构造函数 void display1( ) /输出学生有关数据输出学生有关数据 coutname:name1endl; coutsex:sexendl; coutscore:scoreendl; protected: /保护部分保护部分 string name1; char sex; float sco
62、re; /成绩成绩 ;class Graduate:public Teacher,public Student /多重继承多重继承 public: Graduate(string nam,int a,char s, string t,float sco,float w): Teacher(nam,a,t),Student(nam,s,sco),wage(w) void show( ) /输出研究生的有关数据输出研究生的有关数据 coutname:nameendl; coutage:ageendl; coutsex:sexendl; coutscore:scoreendl; couttitle:
63、titleendl; coutwages:wageendl; private: float wage; /工资工资 ;int main( ) Graduate grad1(Wang-li,24,f,assistant,89.5,1234.5); grad1.show( ); return 0;程序运行结果如下程序运行结果如下: name: Wang-li age: 24sex:fscore: 89.5 title: assistancewages: 1234.5 在两个基类中分别用在两个基类中分别用name和和name1来代来代表同一个人的姓名,表同一个人的姓名,Graduate类的构造函数总
64、参数表类的构造函数总参数表中的参数中的参数nam分别传递给两个基类的构造函数作为实分别传递给两个基类的构造函数作为实参。参。 解决方法解决方法: 在两个基类中使用同一个数据在两个基类中使用同一个数据成员名成员名name,而在而在show函数中引用数据成员时指明函数中引用数据成员时指明其作用域,如其作用域,如coutname:Teacher:nameendl; 通过这个程序还可以发现一个问题通过这个程序还可以发现一个问题: 在多在多重继承时,从不同的基类中会继承一些重复的数据。重继承时,从不同的基类中会继承一些重复的数据。如果有多个基类,问题会更突出。在设计派生类时要如果有多个基类,问题会更突出
65、。在设计派生类时要细致考虑其数据成员,尽量减少数据冗余。细致考虑其数据成员,尽量减少数据冗余。 多重继承的二义性问题是由于继承的成员同多重继承的二义性问题是由于继承的成员同名而产生的问题。名而产生的问题。 如果类如果类A和类和类B中都有成员函数中都有成员函数display和数据和数据成员成员a,类类C是类是类A和类和类B的直接派生类。分别讨论的直接派生类。分别讨论下列下列3种情况种情况: (1)两个基类有同名成员。两个基类有同名成员。(2)两个基类和派生类三者都有同名成员。两个基类和派生类三者都有同名成员。(3)如果类如果类A和类和类B是从同一个基类派生的。是从同一个基类派生的。5.6.3 多
66、重继承引起的二义性问题多重继承引起的二义性问题(1) 两个基类有同名成员。如图两个基类有同名成员。如图5.14所示。所示。图图5.14class A public: int a;void display( );class B public: int a; void display( );class C :public A,public B public : int b; void show();在在main函数中如果有以下调用函数中如果有以下调用:C c1;c1.a=3;/错误错误c1.display(); /错误错误解决办法解决办法:c1.A:a=3;/引用引用c1对象中的基类对象中的基类A
67、的数据成员的数据成员ac1.A:display(); /调用调用c1对象中的基类对象中的基类A的的display函数函数如果是在派生类如果是在派生类C中通过派生类成员函数中通过派生类成员函数show访问基类访问基类A的的display和和a,可以不必写对象名而直接写可以不必写对象名而直接写A:a=3; /指当前对象指当前对象A:display( );图图5.15可以将以上的继承关系表示为:可以将以上的继承关系表示为:(2) 两个基类和派生类三者都有同名成员。将上面的两个基类和派生类三者都有同名成员。将上面的C类声明改为:类声明改为:class C :public A,public B int
68、a; void display();其继承关系可表示为:其继承关系可表示为:在在main函数中定义函数中定义C类对象类对象c1,并有如下调用并有如下调用:C c1;c1.a=3;/调用调用C 类的数据成员类的数据成员c1.display( ); /调用调用C类的类的display函数函数说明说明:(1)从基类继承来的的同名成员在派生类从基类继承来的的同名成员在派生类中被屏蔽,成为中被屏蔽,成为“不可见不可见”的。的。(2)不同的成员函数,只有在函数名和参不同的成员函数,只有在函数名和参数个数相同、类型相匹配的情况下才发生同名覆盖。数个数相同、类型相匹配的情况下才发生同名覆盖。(3)要在派生类外
69、访问基类要在派生类外访问基类A中的成员,应中的成员,应指明作用域指明作用域A。如如: c1.A:a=3; c1.A:display();(3) 如果类如果类A和类和类B是从同一个基类派生的。是从同一个基类派生的。即其派生关系如下图所示:即其派生关系如下图所示:class N public: int a; void display() coutA:a=”aendl; ;class A: public N public: int a1;class B:public N public:int a2; ;class C :public A,public B public :int a3;void sh
70、ow( )couta3=a3endl;int main( ) C c1;/定义定义C类对象类对象c1 通过继承,派生类通过继承,派生类C的成员情况如下图所示:的成员情况如下图所示:经过这样的继承关系后,在类经过这样的继承关系后,在类C中出现了中出现了从类从类N中继承过来的多个同名成员。对它们的访问不中继承过来的多个同名成员。对它们的访问不能通过类能通过类N来标识只能通过类来标识只能通过类N的直接派生类名来指的直接派生类名来指定相应的成员。如定相应的成员。如c1.a3=3;/访问类访问类C新增的数据成员新增的数据成员c1.show( ); /访问类访问类C新增的函数成员新增的函数成员c1.N:a
71、=3; /错误错误c1.N:display();/错误错误c1.A:a=3; /访问类访问类A从类从类N中继承的数据成员中继承的数据成员c1.A:display();/访问类访问类A从类从类N中继承的函数成员中继承的函数成员1. 虚基类的作用虚基类的作用 如果一个派生类有多个直接基类,而这些直接基如果一个派生类有多个直接基类,而这些直接基类又有一个共同的基类,则在最终的派生类中会保留类又有一个共同的基类,则在最终的派生类中会保留该间接共同基类数据成员的多份同名成员。这样既使该间接共同基类数据成员的多份同名成员。这样既使派生类冗余大量增加,同时也给调用带来很多麻烦。派生类冗余大量增加,同时也给调
72、用带来很多麻烦。 C+提供虚基类提供虚基类(virtual base class)的方法,使的方法,使得在继承间接共同基类时只保留一份成员。得在继承间接共同基类时只保留一份成员。 对于如下的继承关系:对于如下的继承关系:5.6.4 虚基类虚基类 如果将类如果将类A声明为虚基类,则当基类通声明为虚基类,则当基类通过多条派生路径被一个派生类继承时,该派生类只继过多条派生路径被一个派生类继承时,该派生类只继承该基类一次。方法如下承该基类一次。方法如下: class A ; /声明基类声明基类Aclass B :virtual public A ;/A是是B的虚基类的虚基类class C :virtu
73、al public A ; /A是是C的虚基类的虚基类 注意注意: 虚基类并不是在声明基类时声明虚基类并不是在声明基类时声明的,而是在声明派生类时,指定继承方式时声明的。的,而是在声明派生类时,指定继承方式时声明的。因为一个基类可以在生成一个派生类时作为虚基类,因为一个基类可以在生成一个派生类时作为虚基类,而在生成另一个派生类时不作为虚基类。而在生成另一个派生类时不作为虚基类。声明虚基类的一般形式为声明虚基类的一般形式为class 派生类名派生类名: virtual 继承方式继承方式 基类名基类名 在派生类在派生类B和和C中作了上面的虚基类声明中作了上面的虚基类声明后,派生类后,派生类D中的成
74、员如下表所示。中的成员如下表所示。D类int data;int data_b;int data_cvoid fun( );int data_d;void fun_d( );特别注意特别注意: 为了保证虚基类在派生类中只继承一次,应为了保证虚基类在派生类中只继承一次,应当在该基类的所有直接派生类中声明为虚基类。当在该基类的所有直接派生类中声明为虚基类。否则仍然会出现对基类的多次继承。如果以下图否则仍然会出现对基类的多次继承。如果以下图方式进行继承的话,派生类方式进行继承的话,派生类E将从类将从类D路径派生的路径派生的部分还保留一份基类成员。部分还保留一份基类成员。2. 虚基类的初始化虚基类的初始
75、化 如果在虚基类中定义了带参数的构造函如果在虚基类中定义了带参数的构造函数,而且没有定义默认构造函数,则在其所有派生类数,而且没有定义默认构造函数,则在其所有派生类(包括直接派生或间接派生的派生类包括直接派生或间接派生的派生类)中,通过构造函中,通过构造函数的初始化表对虚基类进行初始化。例如数的初始化表对虚基类进行初始化。例如class A/定义基类定义基类A A(int i) ; /带一个参数的基类构造函数带一个参数的基类构造函数class B :virtual public A /A作为作为B的虚基类的虚基类 B(int n):A(n) ; /B类构造函数,在初始化表中对虚基类初始化类构造
76、函数,在初始化表中对虚基类初始化class C :virtual public A /A作为作为C的虚基类的虚基类 C(int n):A(n) ; /C类构造函数,在初始化表中对虚基类初始化类构造函数,在初始化表中对虚基类初始化class D :public B,public C D(int n):A(n),B(n),C(n) ; /类类D的构造函数,在初始化表中对所有基的构造函数,在初始化表中对所有基类初始化。类初始化。注意注意: (1)在定义类)在定义类D的构造函数时,与以的构造函数时,与以往使用的方法有所不同。规定往使用的方法有所不同。规定: 在最后的派生类在最后的派生类中不仅要负责对其
77、直接基类进行初始化,还要负中不仅要负责对其直接基类进行初始化,还要负责对虚基类初始化。责对虚基类初始化。(2)C+编译系统只执行最后的派编译系统只执行最后的派生类对虚基类的构造函数的调用生类对虚基类的构造函数的调用,而忽略虚基类,而忽略虚基类的其他派生类的其他派生类(如类如类B和类和类C) 对虚基类的构造函对虚基类的构造函数的调用,这就保证了虚基类的数据成员不会被数的调用,这就保证了虚基类的数据成员不会被多次初始化。多次初始化。3. 虚基类的简单应用举例虚基类的简单应用举例 例例5.9 在例在例5.8的基础上,在的基础上,在Teacher类和类和Student类类之上增加一个共同的基类之上增加
78、一个共同的基类Person,如图如图5.25所示。作所示。作为人员的一些基本数据都放在为人员的一些基本数据都放在Person中,在中,在Teacher类类和和Student类中再增加一些必要的数据。类中再增加一些必要的数据。图图5.25#include class Person /声明公共基类声明公共基类Person public:Person(string nam,char s,int a)/构造函数构造函数name=nam;sex=s;age=a; protected: string name; char sex; int age;class Teacher : virtual publi
79、c Person /声明声明Person为公用继承的虚基类为公用继承的虚基类 public: Teacher(string nam,char s,int a, string t):Person(nam,s,a) /构造函数构造函数 title=t; protected: string title; /职称职称;class Student:virtual public Person /声明声明Person为公用继承的虚基类为公用继承的虚基类public:Student(string nam,char s,int a,float sco) /构造函数构造函数 :Person(nam,s,a),sc
80、ore(sco) /初始化表初始化表protected: /保护成员保护成员 float score; /成绩成绩 ;class Graduate:public Teacher,public Student public:Graduate(string nam,char s,int a, string t,float sco,float w)/构造函数构造函数:Person(nam,s,a),Teacher(nam,s,a,t),Student(nam,s,a,sco),wage(w)/初始化表初始化表 void show( ) /输出研究生的有关数据输出研究生的有关数据 coutname:n
81、ameendl; coutage:ageendl; coutsex:sexendl; coutscore:scoreendl; couttitle:titleendl; coutwages:wageendl; private: float wage; /工资工资 ;int main( ) /主函数主函数 Graduate grad1(Wang-li,f,24,assistant,89.5,1234.5); grad1.show( ); return 0;运行结果为运行结果为:name: Wang-li age:24sex:fscore:89.5title:assistantwages:1234
82、.5可以看到可以看到: 使用多重继承时经常会出现二义性使用多重继承时经常会出现二义性问题。问题。 许多专业人员认为许多专业人员认为: 不要提倡在程不要提倡在程序中使用多重继承,只有在比较简单和不易出序中使用多重继承,只有在比较简单和不易出现二义性的情况或实在必要时才使用多重继承,现二义性的情况或实在必要时才使用多重继承,能用单一继承解决的问题就不要使用多重继承。能用单一继承解决的问题就不要使用多重继承。 也是由于这个原因,有些面向对象也是由于这个原因,有些面向对象的程序设计语言的程序设计语言(如如Java,Smalltalk)并不支持多并不支持多重继承。重继承。 基类与基类与公用派生类公用派生
83、类对象之间有赋值兼容关系对象之间有赋值兼容关系,由,由于派生类中包含从基类继承的成员,因此可以将派生于派生类中包含从基类继承的成员,因此可以将派生类的值赋给基类对象,在用到基类对象的时候可以用类的值赋给基类对象,在用到基类对象的时候可以用其派生类对象代替。具体表现在其派生类对象代替。具体表现在: (1)派生类对象可以向基类对象赋值。派生类对象可以向基类对象赋值。 (2)派生类对象可以替代基类对象向基类对象的派生类对象可以替代基类对象向基类对象的引用进行赋值或初始化。引用进行赋值或初始化。 (3)如果函数的参数是基类对象或基类对象的引如果函数的参数是基类对象或基类对象的引用,相应的实参可以用子类
84、对象。用,相应的实参可以用子类对象。 (4)派生类对象的地址可以赋给指向基类对象的派生类对象的地址可以赋给指向基类对象的指针变量。指针变量。5.7 基类与派生类的转换基类与派生类的转换(1) 派生类对象可以向基类对象赋值。派生类对象可以向基类对象赋值。如如:A a1; /定义基类定义基类A对象对象a1B b1; /定义类定义类A的公用派生类的公用派生类B的对象的对象b1a1=b1; /用派生类用派生类B对象对象b1对基类对象对基类对象a1赋值赋值 在赋值时舍弃派生类自己的成员。所谓在赋值时舍弃派生类自己的成员。所谓赋值只是对数据成员赋值,对成员函数不存在赋值问赋值只是对数据成员赋值,对成员函数
85、不存在赋值问题。题。 说明说明: 赋值后不能企图通过对象赋值后不能企图通过对象a1去访问派生去访问派生类对象类对象b1的成员,因为的成员,因为b1的成员与的成员与a1的成员是不同的成员是不同的。假设的。假设age是派生类是派生类B中增加的公用数据成员,分中增加的公用数据成员,分析下面的用法析下面的用法: a1.age=23; /错误,错误,a1中不包含派生类中增加的中不包含派生类中增加的成员成员b1.age=21; /正确,正确,b1中包含派生类中增加的成中包含派生类中增加的成员员 子类型关系是单向的、不可逆的。只能子类型关系是单向的、不可逆的。只能用子类对象对其基类对象赋值,而不能用基类对象
86、对用子类对象对其基类对象赋值,而不能用基类对象对其子类对象赋值。其子类对象赋值。因为基类对象不包含派生类的成员,因为基类对象不包含派生类的成员,无法对派生类的成员赋值。同理,无法对派生类的成员赋值。同理,同一基类的不同派同一基类的不同派生类对象之间也不能赋值。生类对象之间也不能赋值。(2) 派生类对象可以替代基类对象向基类对象的引用派生类对象可以替代基类对象向基类对象的引用进行赋值或初始化。进行赋值或初始化。如如:A a1; /定义基类定义基类A对象对象a1B b1; /定义公用派生类定义公用派生类B对象对象b1A& r=b1; /用派生类用派生类B对象对象b1对基类对象的引用进行初对基类对象
87、的引用进行初始化始化 此时此时r并不是并不是b1的别名,也不与的别名,也不与b1共享同一段存储共享同一段存储单元。它只是单元。它只是b1中基类部分的别名,中基类部分的别名,r与与b1中基类部分共享同中基类部分共享同一段存储单元,一段存储单元,r与与b1具有相同的起始地址。具有相同的起始地址。(3) 如果函数的参数是基类对象或基类对象的引用,如果函数的参数是基类对象或基类对象的引用,相应的实参可以用子类对象。相应的实参可以用子类对象。如有一函数如有一函数fun: void fun(A& r) /形参是类形参是类A的对象的引用变量的对象的引用变量 coutr.numendl; /输出该引用变量的数
88、据成员输出该引用变量的数据成员num 函数的形参是类函数的形参是类A的对象的引用变量。的对象的引用变量。由于子类对象与派生类对象赋值兼容,派生类对象能由于子类对象与派生类对象赋值兼容,派生类对象能自动转换类型,在调用自动转换类型,在调用fun函数时可以用派生类函数时可以用派生类B的的对象对象b1作实参作实参: fun(b1); 输出类输出类B的对象的对象b1的基类数据成员的基类数据成员num的的值。并且在值。并且在fun函数中只能输出派生类中基类成员的函数中只能输出派生类中基类成员的值。值。(4) 派生类对象的地址可以赋给指向基类对象的指针派生类对象的地址可以赋给指向基类对象的指针变量,也就是
89、说,指向基类对象的指针变量也可以指变量,也就是说,指向基类对象的指针变量也可以指向派生类对象。向派生类对象。 例例5.10 定义一个基类定义一个基类Student,再定义,再定义Student类类的公用派生类的公用派生类Graduate(研究生研究生),用指向基类对象的,用指向基类对象的指针输出数据。指针输出数据。#include class Student/声明声明Student类类public: Student(int, string,float); /声明构造函数声明构造函数 void display( ); /声明输出函数声明输出函数private: int num; string n
90、ame; float score;Student:Student(int n, string nam,float s) /定义构造函数定义构造函数 num=n; name=nam; score=s;void Student:display( ) /定义输出函数定义输出函数 coutendlnum:numendl; coutname:nameendl; coutscore:scoreendl;class Graduate:public Student /声明公用派生类声明公用派生类Graduate public: Graduate(int, string ,float,float); /声明构造
91、函数声明构造函数 void display( ); /声明输出函数声明输出函数 private: float pay; /工资工资;Graduate:Graduate(int n, string nam,float s,float p): Student(n,nam,s),pay(p) /定义构造函数定义构造函数void Graduate:display() /定义输出函数定义输出函数 Student:display(); /调用调用Student类的类的display函数函数 coutpay=paydisplay( ); /调用调用stud1.display函数函数 pt=&grad1; /
92、指针指向指针指向grad1 pt-display( ); /调用调用grad1.display函数函数 程序的输出结果程序的输出结果: num:1001name:Liscore:87.5num:2001name:wangscore:98.5提出的问题:提出的问题:(1)最后为什么没有输出)最后为什么没有输出pay的值?的值? pt是指向是指向Student类对象的指针变量,即类对象的指针变量,即使让它指向了使让它指向了grad1,但实际上但实际上pt指向的是指向的是grad1中从中从基类继承的部分。通过指向基类对象的指针,只能访基类继承的部分。通过指向基类对象的指针,只能访问派生类中的基类成员
93、,而不能访问派生类增加的成问派生类中的基类成员,而不能访问派生类增加的成员。员。(2)如何才能输出)如何才能输出pay的值?的值? 可另设一个指向派生类对象的指针变量可另设一个指向派生类对象的指针变量ptr,使它指向使它指向grad1,然后用然后用ptr-display()调用派调用派生类对象的生类对象的display函数。但这不大方便。函数。但这不大方便。(3)如何方便的解决这个问题?)如何方便的解决这个问题? 用指向基类对象的指针变量指向子类用指向基类对象的指针变量指向子类对象是合法的、安全的,不会出现编译上的错误。对象是合法的、安全的,不会出现编译上的错误。但在应用上却不能完全满足人们的
94、希望,人们有但在应用上却不能完全满足人们的希望,人们有时希望通过使用基类指针能够调用基类和子类对时希望通过使用基类指针能够调用基类和子类对象的成员。在下一章可以使用象的成员。在下一章可以使用虚函数和多态性虚函数和多态性来来解决这个问题。解决这个问题。 在一个类中以另一个类的对象作为数据成在一个类中以另一个类的对象作为数据成员的,称为类的组合员的,称为类的组合(composition)。例:声明例:声明Professor类是类是Teacher类的派生类,另有一类的派生类,另有一个类个类BirthDate,包含,包含year,month,day等数据成员。可等数据成员。可以将教授生日的信息加入到以
95、将教授生日的信息加入到Professor类的声明中。类的声明中。class Teacher/教师类教师类 public: private: int num; string name; char sex; ;5.8 继承与组合继承与组合class BirthDate /生日类生日类 public: private:int year;int month;int day;class Professor : public Teacher /教授类教授类 public: private:BirthDate birthday; /BirthDate类的对象作为数据类的对象作为数据成员成员 ;类的组合和继承
96、一样,是软件重用的重要类的组合和继承一样,是软件重用的重要方式。组合和继承都是有效地利用已有类的资源。但方式。组合和继承都是有效地利用已有类的资源。但二者的概念和用法不同。继承是纵向的,组合是横向二者的概念和用法不同。继承是纵向的,组合是横向的。的。有了继承,使软件的重用成为可能。继有了继承,使软件的重用成为可能。继承是承是C+和和C的最重要的区别之一。的最重要的区别之一。 由于由于C+提供了继承的机制,这就吸引提供了继承的机制,这就吸引了许多厂商开发各类实用的类库。用户将它们作为基了许多厂商开发各类实用的类库。用户将它们作为基类去建立适合于自己的类类去建立适合于自己的类(即派生类即派生类),
97、并在此基础上,并在此基础上设计自己的应用程序。类库的出现使得软件的重用更设计自己的应用程序。类库的出现使得软件的重用更加方便,类库并不是加方便,类库并不是C+编译系统的一部分。不同的编译系统的一部分。不同的C+编译系统提供的由不同厂商开发的类库一般是不编译系统提供的由不同厂商开发的类库一般是不同的。同的。5.9 继承在软件开发中的重要意义继承在软件开发中的重要意义 对类库中类的声明一般放在头文件中,对类库中类的声明一般放在头文件中,类的实现类的实现(函数的定义部分函数的定义部分)是单独编译的,以目标代是单独编译的,以目标代码形式存放在系统某一目录下。用户使用类库时,不码形式存放在系统某一目录下
98、。用户使用类库时,不需要了解源代码,但必须知道头文件的使用方法和怎需要了解源代码,但必须知道头文件的使用方法和怎样去连接这些目标代码样去连接这些目标代码(在哪个子目录下在哪个子目录下),以便源程,以便源程序在编译后与之连接。序在编译后与之连接。 由于基类是单独编译的,在程序编译时由于基类是单独编译的,在程序编译时只需对派生类新增的功能进行编译,这就大大提高了只需对派生类新增的功能进行编译,这就大大提高了调试程序的效率。如果在必要时修改了基类,只要基调试程序的效率。如果在必要时修改了基类,只要基类的公用接口不变,派生类不必修改,但基类需要重类的公用接口不变,派生类不必修改,但基类需要重新编译,派
99、生类也必须重新编译,否则不起作用。新编译,派生类也必须重新编译,否则不起作用。 只所以选择使用继承而不选择对已有的只所以选择使用继承而不选择对已有的类加以修改,使之满足自己应用的要求,主要原因有:类加以修改,使之满足自己应用的要求,主要原因有:(1) 有许多基类是被程序的其他部分或其有许多基类是被程序的其他部分或其他程序使用的,这些程序要求保留原有的基类不受破他程序使用的,这些程序要求保留原有的基类不受破坏。坏。(2) 用户往往得不到基类的源代码。用户往往得不到基类的源代码。(3) 在类库中,一个基类可能已被指定与在类库中,一个基类可能已被指定与用户所需的多种组件建立了某种关系,因此在类库中用户所需的多种组件建立了某种关系,因此在类库中的基类是不容许修改的。的基类是不容许修改的。(4) 实际上,许多基类并不是从已有的其实际上,许多基类并不是从已有的其他程序中选取来的,而是专门作为基类设计的。他程序中选取来的,而是专门作为基类设计的。(5) 在面向对象程序设计中,需要设计类在面向对象程序设计中,需要设计类的层次结构,从最初的抽象类出发,每一层派生类的的层次结构,从最初的抽象类出发,每一层派生类的建立都逐步地向着目标的具体实现前进。建立都逐步地向着目标的具体实现前进。