680 likes | 933 Views
第 8 章 继承和派生. 8.1 继承与派生 8.2 派生类对基类成员的访问 8.3 派生类的构造函数与析构函数 8.4 多重继承与虚基类. 第 8 章 继承和派生. 教学目标 : 1. 掌握 C++ 语言中类的继承机制、语法规则及子类对基类成员的覆盖; 2 . 了解单继承、多重继承等有关概念及其差别。 教学重点 : 1. 派生类对基类的继承本质; 2. 派生类构造函数和析构函数工作机制 ; 3. 虚基类的应用; 教学难点 : 1. 虚基类的应用。. 8.1 继承与派生. 8.1.1 基本概念
E N D
第8章 继承和派生 • 8.1 继承与派生 • 8.2 派生类对基类成员的访问 • 8.3 派生类的构造函数与析构函数 • 8.4 多重继承与虚基类
第8章 继承和派生 • 教学目标: 1. 掌握C++语言中类的继承机制、语法规则及子类对基类成员的覆盖; 2 .了解单继承、多重继承等有关概念及其差别。 • 教学重点: 1.派生类对基类的继承本质; 2.派生类构造函数和析构函数工作机制 ; 3.虚基类的应用; • 教学难点: 1.虚基类的应用。
8.1 继承与派生 8.1.1 基本概念 8.1.2 派生的定义与构成
8.1.1 基本概念 继承:面向对象程序设计中,可以在已有类的基础上定义新的类,而不需要把已有类的内容重新书写一遍,这就叫做继承。 基类: 已有类称为基类或父类, 派生类: 在已有类的基础上建立的新类称为派生类或导出类、子类。 基类和派生类是继承中的两个概念,被继承的类称为基类,继承的类称为派生类。
单继承: 如果一个对象从单个基类中继承了属性,就被称为单继承; 多重继承: 如果一个对象从多个基类中继承了属性,就被称为多重继承。 继承性允许一个类从其他类中继承属性, 从而实现代码重用。
基类1 基类2 …… 基类n 基类 派生类1 派生类2 派生类1 派生类2 一个基类可以直接派生出多个派生类 派生类可以由多个基类共同派生出来,称多重继承。 (b)单继承 (a)多重继承 图8.1 多重继承与单继承
多层次继承: 在派生过程中,派生出来的新类同样可以作为基类再继续派生出更新的类,依此类推形成一个层次结构。直接参与派生出某类称为直接基类,而基类的基类,以及更深层的基类称为间接基类。 类族: 同时一个基类可以直接派生出多个派生类。这样形成了一个相互关联的类族。如MFC就是这样的族类,它由一个CObject类派生出200个MFC类中的绝大多数。
8.1.2 派生类的定义与构成 派生类的定义: 一个派生类可以看作是对现有类的继承或扩展,派生类提供了扩展或定制基类特性的简单手段,不需要重新来创建基类本身。 实现继承的方法是类的派生,任何一个类都可以作为基类,从这个基类可以派生出多个类. 派生的类不仅具有基类的特征,而且还可以定义自己独有的特征。程序员可以通过类的派生来构造可重用的类库。
派生类的定义格式: class 派生类名:继承方式 基类名{ private: 成员表1;//派生类增加或替代的私有成员 public: 成员表2;//派生类增加或替代的公有成员 protected: 成员表3;//派生类增加或替代的保护成员 };//分号不可少 其中基类是已声明的类。 在派生类定义的类体中给出的成员称为派生类成员,它们是新增加成员,它们给派生类添加了不同于基类的新的属性和功能。派生类成员也包括取代基类成员的更新成员。 继承方式决定了子类对父类的访问权限,有3种继承方式: private、public和protected,默认为private,最常用的是public。
关于继承的几点说明: • ① 派生类自动具有基类的全部数据成员和成员函数;但是,派生类对基类成员的访问有所限制。 • ② 派生类可以定义自己的成员: 数据成员和成员函数。 • ③ 基类、派生类或父类、子类都是“相对”的。一个类派生出新的类就是基类。派生类也可以被其他类继承,这个派生类同时也是基类。 • 构造函数和析构函数是不能继承的,对派生类要重新定义构造函数和析构函数.
派生编程步骤: 吸收基类的成员 不论是数据成员,还是函数成员,除构造函数与析构函数外全盘接收 编制派生类时可分四步 改造基类成员 声明一个和某基类成员同名的新成员,派生类中的新成员就屏蔽了基类同名成员称为同名覆盖(override) 发展新成员 派生类新成员必须与基类成员不同名,它的加入保证派生类在功能上有所发展。 重写构造函数与析构函数
第二步中,新成员如是成员函数,参数表也必须一样,否则是重载。第二步中,新成员如是成员函数,参数表也必须一样,否则是重载。 第三步中,独有的新成员才是继承与派生的核心特征。 第四步 是重写构造函数与析构函数,派生类不继承这两种函数。不管原来的函数是否可用,一律重写可免出错。
例如: 基类可写为: class A{ //类成员列表 }; class B: public A { // //增加或替代的成员列表 }; class C: public B { //可扩展到任意级数 // //增加或替代的成员列表 };
8.2 派生类对基类成员的访问 • 继承方式:亦称为访问控制,是对基类成员进一步的限制。访问控制也是三种: • 公有(public)方式,亦称公有继承 • 保护(protected)方式,亦称保护继承 • 私有(private)方式, 亦称私有继承。 访问限定符两方面含义: (1) 派生类成员(新增成员)函数对基类(继承来的)成员的访问(调用和操作), (2)从派生类对象之外对派生类对象中的基类成员的访问。
(1) 公有继承: 派生时用public作继承方式。 • 基类的公有(public)成员被继承为公有的。 • 基类的私有(private)成员在派生类中不可见。 • 基类的保护(protected)成员被继承为保护的。
(2) 私有继承: 派生时用private作继承方式。 • 基类的公有段(public)成员被继承为私有的。 • 基类的私有段(private)成员在派生类中不可见。 • 基类的保护段(protected)成员被继承为私有的。
(3) 保护继承:派生时用protected作继承方式。 • 基类的公有段(public)成员被继承为保护的。 • 基类的私有段(private)成员在派生类中不可见。 • 基类的保护段(protected)成员被继承为保护的。
派生方式 基类中的访问限定 在派生类中对基类成员的访问限定 在派生类对象外访问派生类对象的基类成员 公有派生 public public 可直接访问 protected protected 不可直接访问 private 不可直接访问 不可直接访问 私有派生 public private 不可直接访问 protected private 不可直接访问 private 不可直接访问 不可直接访问 公有派生是绝对主流。
class A { //基类 private: int priA; protected: int proA; public: int pubA; };
class B : public A { //派生类 public: void fn() { int a; a = priA; //错误:不可访问 a = proA; //有效 a = pubA; //有效 } };
void main() { A a; //基类对象 a.priA = 1; //错误:不可访问 a.proA = 1; //错误:不可访问 a.pubA = 1; //有效 B b; //派生类对象 b.priA = 1; //错误:不可访问 b.proA = 1; //错误:不可访问 b.pubA = 1; //有效 }
8.3 派生类的构造函数与析构函数 • 8.3.1 派生类的构造函数 • 8.3.2 派生类的析构函数
8.3.1 派生类的构造函数 派生类构造函数: 在派生关系中,构造函数和析构函数是不能继承的,对派生类要重新定义构造函数和析构函数。 (1)一个派生类对象中也包含了基类数据成员的值,所以在生成一个派生类对象时,系统首先要通过派生类的构造函数调用基类中的构造函数,对基类成员初始化,然后对派生类中新增的成员初始化。 (2)也就是说,派生类的构造函数除了对新增成员进行初始化之外,还要调用基类的构造函数,对基类进行初始化。
派生类构造函数的定义: 派生类名::派生类名(参数总表):基类名1(参数名表1)《,基类名2(参数名表2),……,基类名n(参数名表n)》,《成员对象名1(成员对象参数名表1),……,成员对象名m(成员对象参数名表m)》{ ……//派生类新增成员的初始化; }//所列出的成员对象名全部为新增成员对象的名字 注意: 在构造函数的声明中,冒号及冒号以后部分必须略去。 所谓不能继承并不是不能利用,而是把基类的构造函数作为新的构造函数的一部分,或者讲调用基类的构造函数。基类名仅指直接基类,写了底层基类,编译器认为出错。 冒号后的基类名,成员对象名的次序可以随意,这里的次序与调用次序无关。
派生类构造函数各部分执行次序: 1.调用基类构造函数,按它们在派生类定义的先后顺序,顺序调用。 2.调用成员对象的构造函数,按它们在类定义中声明的先后顺序,顺序调用。 3.派生类的构造函数体中的操作。 注意: 在派生类构造函数中,只要基类不是使用无参的默认构造函数,都要显式给出基类名和参数表。 如果基类没有定义构造函数,则派生类也可以不定义,全部采用系统给定的默认构造函数。 如果基类定义了带有形参表的构造函数,派生类就应当定义构造函数。
8.3.2 派生类的析构函数 析构函数: 析构函数的功能是作善后工作。 只要在函数体内把派生类新增一般成员处理好就可以了,而对新增的成员对象和基类的善后工作,系统会自己调用成员对象和基类的析构函数来完成。 析构函数各部分执行次序与构造函数相反,首先对派生类新增一般成员析构,然后对新增成员对象析构,最后对基类成员析构。
【例8.1】由在册人员类公有派生学生类 【例8.1】由在册人员类公有派生学生类。我们希望基类和派生类共享相同的公有接口,只能采用公有派生来实现。 基类: class Person{ string IdPerson; //身份证号,18位数字 string Name; //姓名 Tsex Sex; //性别enumTsex{mid,man,woman}; int Birthday; //生日,格式1986年8月18日写作19860818 string HomeAddress; //家庭地址 public: Person(string, string,Tsex,int, string);//构造函数 Person(); //默认的构造函数 ~Person(); //析构函数
//接口函数: void SetName(string);//修改名字 string GetName(){return Name;}//提取名字 void SetSex(Tsex sex){Sex=sex;} //修改性别 Tsex GetSex(){return Sex;} //提取性别 void SetId(string id){IdPerson=id;}//修改身份证号 string GetId(){return IdPerson;}//提取身份证号 void SetBirth(int birthday){Birthday=birthday;} //修改生日 int GetBirth(){return Birthday;} //提取生日 void SetHomeAdd(string ); //修改住址 string GetHomeAdd(){return HomeAddress;}//提取住址 void PrintPersonInfo(); //输出个人信息 };
派生的学生类: class Student:public Person{ //定义派生的学生类 string NoStudent; //学号 course cs[30]; //30门课程与成绩 public: Student(string id, string name,Tsex sex,int birthday, string homeadd, string nostud); //注意派生类构造函数声明方式 Student(); //默认派生类构造函数 ~Student(); //派生类析构函数 SetCourse(string ,int); //课程设置 int GetCourse(string ); //查找成绩 void PrintStudentInfo(); //打印学生情况 }; struct course{ string coursename; int grade;}; 验证主函数
单继承中的构造函数和析构函数的应用说明: ① 派生类的构造函数的初始化列表中列出的均是直接基类的构造函数。 ② 构造函数不能被继承,因此派生类的构造函数只能通过调用基类的某个构造函数(如果有重载的话)来初始化基类子对象。 ③ 派生类的构造函数只负责初始化自己定义的数据成员。 ④ 先调用基类的构造函数,再调用派生类自己的数据成员所属类的构造函数,最后调用派生类的构造函数;派生类的数据成员的构造函数被调用的顺序取决于在类中声明的顺序。
单继承中的构造函数和析构函数的应用说明: ⑤ 析构函数不可以继承,不可以被重载,也不需要被调用。 ⑥ 派生类的对象的生存期结束时自动调用派生类的析构函数,在该析构函数结束之前再自动调用基类的析构函数;所以,析构函数的被自动调用次序与构造函数相反。
8.4 多重继承与虚基类 • 8.4.1 多重继承的二义性问题 • 8.4.2 虚基类的定义 • 8.4.3 虚基类的构造函数
椅子 床 沙发(单继承) 躺椅(多重继承) 两用沙发(多重继承) 图8.2 椅子,床到两用沙发 8.4.1 多重继承的二义性问题 多重继承: 由多个基类共同派生出新的派生类,这样的继承结构被称为多重继承或多继承.
在册人员 教职工(单继承) 学生(单继承) 兼职教师(单继承) 教师(单继承) 工人(单继承) 行政人员(单继承) 研究生(单继承) 在职研究生 (多重继承) 行政人员兼教师 (多重继承) 研究生助教 (多重继承) 派生出来的新类同样可以作为基类再继续派生出更新的类,依此类推形成一个层次结构。 图8.3 大学在册人员继承关系
二义性问题: 参见图7.3,比如行政人员兼教师,在其基类教师中有一个“教职工编号”,另一基类行政人员中也有一个“教职工编号”,如果只讲教职工编号,那么它是哪一个基类中的呢?这两者可能是一回事,但计算机系统并不这么认为。 进一步,如果“教职工编号” 是由两个基类“教师”和“行政人员”共同的基类“教职工”类继承来的,只有同一个标识符,也不能用改标识符来区分。 唯一标识问题: 通常采用作用域运算符“::”: 基类名::成员名; //数据成员 基类名::成员名(参数表); //函数成员
定义EGStudent类对象EGStud1,并假定派生全部为公有派生,而int No全为公有成员: EGStud1.No //在职学号 EGStud1.GStudent::No //研究生号 EGStud1.GStudent.Student::No //学生号 EGStud1.GStudent.Student. Person::No //身份证号 EGStud1.Employee::No //工作证号 EGStud1.Employee.Person::No //身份证号 两个身份证号从逻辑上讲应是一回事,但是物理上是分配了不同内存空间,是两个变量,请参见图7.4(b)。 图8.4(a)在职研究生派生类关系
Person Person成员 Student GStudent Student新成员 GStudent新成员 EGStudent Person成员 Person Employee Employee新成员 EGStudent新成员 图7.4(b)在职研究生派生类存储图 建议采用有确定字面意思的标识符,它可以被编译器简单区分出来。 如果class Person的身份证号标识为int IdPerson,则写为:EGStud1.GStudent::IdPerson EGStud1.Employee::IdPerson 不必标出那么多层次的类,但写EGStud1.IdPerson是错的。 作用域分辨符不能嵌套使用,如: EGStud1.GStudent::Student::No //学生号 EGStud1.GStudent::Student::Person::No //身份证号 是错误的。
注意: 一般数据成员总是私有成员,派生类对基类的访问只能间接进行。访问身份证号,应通过class Person中的公有成员函数(接口)GetNo()和SetNo()进行: EGStud1.Employee.Person::SetNo(int no); no=EGStud1.Employee.Person::GetNo();
【例8.2】由圆和高多重继承派生出圆锥 【例8.2】由圆和高多重继承派生出圆锥。 因为公有派生时,在派生类中不可以直接访问基类的私有成员,但可以直接访问基类的保护成员,当需要在派生类中访问基类的数据成员时,可以将它们定义为保护的,而不是私有的。 本例中类Circle为圆;类Line为高;类Cone为圆锥,由Circle和Line公有派生而来。在Cone类中,Circle和Line类的接口完全不变,可以直接调用,这就是公有派生的优点。在Cone的成员函数中可直接访问Circle和Line中的公有成员和保护成员。 圆类Circle定义 高类Line定义 圆锥类Cone定义 检证主程序:
8.4.2 虚基类的定义 虚基类的引入: 在多继承中,若在多条继承路径上有公共基类,这个公共基类便会产生多个副本。为了解决这个问题,把公共基类定义为虚基类。使用虚基类的继承称为虚拟继承。 虚基类是对派生类而言,虚基类本身的定义同基类一样,在定义派生类时声明该基类为虚基类即可,就是冠以关键字virtual。 例如, 在图7.4中,两个身份证号显然是不合理的。可以把class Person这个共同基类设置为虚基类,这样就仅有一个Person基类成员,从不同路径继承来的同名数据成员(身份证号)在内存中就是同一个数据,从而解决了多重继承的二义性问题。
虚基类定义: class派生类名:virtual 继承方式 基类类名{...}; class派生类名: 继承方式virtual 基类类名{...}; 注意: virtual 关键字只对紧随其后的基类名起作用: class Student:virtualpublic Person{...}; class Employee:virtual public Person{...};
Person Student GStudent EGStudent Person Employee 图8.5 采用虚基类后在职研究生类储存图 Person Student新成员 GStudent新成员 Person Employee新成员 Person成员 EGStudent新成员 虚拟继承: 虚拟继承 在Person的位置上放的是指针,两个指针都指向Person成员存储的内存。这种使用虚基类的继承称为虚拟继承,
8.4.3 虚基类的构造函数 虚拟继承的构造函数: 派生类名::派生类名(参数总表):基类名1(参数名表1)《,基类名2(参数名表2),……,基类名n(参数名表n)》,《成员对象名1(成员对象参数名表1),……,成员对象名m(成员对象参数名表m)》,底层虚基类名1(参数名表1)《,……, 底层虚基类名r(参数名表r)》{ ……//派生类新增成员的初始化 }; //所列出的成员对象名全部为新增成员对象的名字 在多层虚拟继承构造函数中,基类名不仅要列出直接基类,而且要列出底层虚基类,否则编译器认为出错。
构造函数执行次序: 在派生类对象的创建中: (1) 首先是虚基类的构造函数并按它们声明的顺序构造。 (2) 第二批是非虚基类的构造函数按它们声明的顺序调用。 (3) 第三批是成员对象的构造函数。 (4) 最后是派生类自己的构造函数被调用。
【例8.3】在采用虚基类的多重继承中,构造与析构的次序。【例8.3】在采用虚基类的多重继承中,构造与析构的次序。 class Dclass:public Bclass1,virtual Bclass3, virtual Bclass2{ Object object; public: Dclass(): object() ,Bclass2(),Bclass3(),Bclass1(){ cout<<"派生类建立!\n";} ~Dclass(){cout<<"派生类析构!\n";} }; int main(){ Dclass dd; cout<<“主程序运行!\n”;return 0; }
运行结果: Constructor Bclass3 //第一个虚拟基类,与派生类析构函数排列无关 Constructor Bclass2 //第二个虚拟基类 Constructor Bclass1 //非虚拟基类 Constructor Object //对象成员 派生类建立! 主程序运行! 派生类析构! deconstructor Object //析构次序相反 deconstructor Bclass1 deconstructor Bclass2 deconstructor Bclass3 //析构的次序与构造的次序相反。
【例8.4】虚基类在多层多重继承中的应用 ——在职研究生类定义。 以虚基类定义公有派生的学生类 以虚基类定义公有派生的研究生类 以虚基类定义公有派生的教职工类 多重继承的以虚基类定义公有派生的在职研究生类 对照图8.5,尽管Employee和Student的构造函数都包含Person的构造函数,但并未真正调用。唯一的一次调用是在EGStudent构造函数中。如是非虚基类,则有两次调用。
第8章 继承和派生 【例8.1】由在册人员类公有派生学生类 分析构造函数: Person::Person(string id, string name,Tsex sex,int birthday, string homeadd){ IdPerson=id; Name=name; Sex=sex; Birthday=birthday; HomeAddress=homeadd; } //作为一个管理程序,这个构造函数并无必要,因为数据总是另外输入的。仅为说明语法存在。
分析默认的构造函数: Person::Person(){ IdPerson="#";Name="#";Sex=mid; Birthday=0;HomeAddress="#"; } 分析析构函数: Person::~Person(){} //string内部动态数组的释放,由string自带的析构函数完成