510 likes | 750 Views
C++ 面向对象程序设计. 第八章 继承和派生. ⑴ 理解继承与派生的概念 ⑵掌握派生类的定义格式,理解派生类成员的来源 ⑶掌握三种不同继承方式对基类成员、派生类成员访问权限的影响 ⑷熟悉单一继承方式下派生类构造函数和析构函数的书写格式、执行顺序 ⑸了解多重继承方式下派生类构造函数和析构函数的书写格式、执行顺序 ⑹了解二义性问题产生的原因,掌握虚基类的定义及应用. 学习目标. 8.1 为什么要引入继承的概念.
E N D
C++面向对象程序设计 第八章 继承和派生
⑴理解继承与派生的概念 ⑵掌握派生类的定义格式,理解派生类成员的来源 ⑶掌握三种不同继承方式对基类成员、派生类成员访问权限的影响 ⑷熟悉单一继承方式下派生类构造函数和析构函数的书写格式、执行顺序 ⑸了解多重继承方式下派生类构造函数和析构函数的书写格式、执行顺序 ⑹了解二义性问题产生的原因,掌握虚基类的定义及应用 学习目标
8.1 为什么要引入继承的概念 • 面向对象程序设计十分强调软件的可重用性,其重要特征继承是软件复用的一种重要形式。在C++中,也通过继承机制来实现代码的可重用性。可以方便地利用一个已有的类建立新类,重用已有软件中的部分甚至很大的部分。它模拟客观事物发展是从简单到复杂和互相联系的规律。利用继承,我们可以推陈出新,易于扩展和完善已有的程序功能。在已有的类的基础上,可以派生一个新类。前者称为基类(base class)或父类,后者称为派生类(derived class)或子类。在类层次(class hierarchy)中,基类和派生类是相对的,从一个基类派生出来的类可以是另一个类的基类。
8.1.2 继承与派生的概念 保持已有类的特性而构造新类的过程称为继承。继承的目的是实现代码重用。在已有类的基础上新增自己的特性而产生新类的过程称为派生。派生的目的是当新的问题出现,原有程序无法解决(或不能完全解决)时,需要对原有程序进行改造。 说明: ⑴继承是面向对象程序设计方法的4个基本特征之一,是程序代码可重用性的具体体现。 ⑵在C++面向对象程序设计中,所谓类的继承就是利用现有的类创建一个新的类。新类继承了现有类的属性和行为。 ⑶为了使新类具有自己所需的功能,它可以扩充和完善现有类的属性和行为,使之更具体。 ⑷继承可分为单继承(single inheritance) 和多继承(multi-inheritance)。 ⑸微软基础类MFC就是通过类的继承来体现类的可重用性和可扩充性。
8.2 基类和派生类 8.2.1. 基类和派生类的概念 在继承关系中,新定义的类称为被继承的类的派生类或子类,而被继承的类称为新定义类的基类或父类。派生类继承了基类的所有成员(除构造函数和析构函数以外)。 对于一个派生类,可以同时有多个基类,这种情况称为多继承,这时的派生类同时得到了多个已有类的特征。一个派生类只有一个直接基类的情况,称为单继承。 一个派生类也可以作为另一个派生类的基类。
8.2.2. 派生类的定义(单继承) 一个没有利用继承的例子: class Student { int num; char name[30]; char sex; public: void display( ) //对成员函数display的定义 {cout<<"num: "<<num<<endl; cout<<"name: "<<name<<endl; cout<<"sex: "<<sex<<endl; } };
可以看出,很多是重复的地方,我们可以通过C++语言中的继承机制,可以扩充和完善旧的程序设计以适应新的需求。这样不仅可以节省程序开发的时间和资源,并且为未来程序增添了新的资源。可以看出,很多是重复的地方,我们可以通过C++语言中的继承机制,可以扩充和完善旧的程序设计以适应新的需求。这样不仅可以节省程序开发的时间和资源,并且为未来程序增添了新的资源。 class Studend1 { int num; //此行原来己有 char name[20]; //此行原来己有 char sex; //此行原来己有 int age; char addr[20]; public: void display( ) ; //此行原来己有 {cout<<"num: "<<num<<endl; //此行原来己有 cout<<"name: "<<name<<endl;//此行原来己有 cout<<"sex: "<<sex<<endl; //此行原来己有 cout<<"age: "<<age<<endl; cout<<"address: "<<addr<<endl;} };
可以充分利用继承的机制 class Student1: public Student //声明基类是Student { private: int age; //新增加的数据成员 string addr; //新增加的数据成员 public: void display_1( ) //新增加的成员函数 { cout<<"age: "<<age<<endl; cout<<"address: "<<addr<<endl; } };
类A派生类B:类A为基类,类B为派生类。 B A 新增加的成员数据和成员函数
class 派生类名:继承方式 基类名 { …… //派生类新增加的成员声明 }; 说明: ⑴继承方式决定了基类的成员在派生类中的访问权限。继承方式共有三种:public、private和protected(缺省值为private)。 ⑵在不涉及继承问题时,类中的成员有两种访问权限,即private和public。在涉及继承的场合,类中成员有三种访问权限,即:public (公有的)、private(私有的)、protected(保护的)。 如果不显式说明访问权限,隐含的访问权限是 private。 ⑶应该注意:继承方式会改变从基类继承过来的成员的访问权限。 ⑷虽然派生类继承了基类的所有成员,但为了不破坏基类的封装性,无论采用哪种派生方式,基类的私有成员在派生类中都是不可见的,即不允许在派生类的成员函数中访问基类的私有成员。
举例: class Employee:public Person //定义派生类,单继承 { char m_strDept[20]; public: void ShowMe() { cout<<m_strName<<"\t"<<m_nSex<<"\t"<<m_strDept<<"\n"; } }; class Person//定义基类 { protected: char m_strName[10]; char m_nSex[6]; public: void ShowMe() { cout<<m_strName<<"\t“ <<m_nSex<<"\n"; } };
8.3 三种派生方式 8.3.1 public派生 以公有继承方式创建的派生类对基类各种成员的访问权限如下: ⑴基类公有成员相当于派生类的公有成员,即派生类可以像访问自身的公有成员一样访问基类的公有成员。 ⑵基类保护成员相当于派生类的保护成员,即派生类可以像访问自身的保护成员一样访问基类的保护成员。 ⑶对于基类的私有成员,派生类内部成员无法直接访问。派生类的使用者也无法通过派生类对象直接访问基类的私有成员。
public派生 • 公有派生时,基类中所有成员在派生类中保持各个成员的访问权限。 • 基类:public: 在派生类和类外可以使用 • protected: 在派生类中使用 • private: 不能在派生类中使用
【例8-1】人员类(Person)及其子类雇员类(Employee)的定义及使用。【例8-1】人员类(Person)及其子类雇员类(Employee)的定义及使用。 • 代码见教材P84 程序说明:派生类Employee继承了基类Person中除构造函数和析构函数以外的所有成员。 对象emp的构造过程通过Employee的构造函数实现,而构造函数执行过程中调用了基类Register()成员函数。说明派生类自身可以访问基类公有成员。
8.3.2 private派生 • 以私有继承方式创建的派生类对基类各种成员的访问权限如下: • ⑴基类公有成员和保护成员相当于派生类的私有成员,派生类自身的成员函数可以访问它们。 • ⑵对于基类的私有成员,无论派生类内部成员或派生类的使用者都无法直接访问。
【例8-2】将【例8-1】改为私有派生。 • 程序见教材P87 • 程序运行结果: 张三 40 m 图书馆 2000 张三 调用基类 GetName() 返回值为: 张三 调用基类 GetSex() 返回值为:m 调用基类 GetAge() 返回值为:40
Person类 保护 Private派生 私有 公有 保护 公有 派生类的对象
8.3.3 protected派生 以保护继承方式创建的派生类对基类各种成员的访问权限如下: • ⑴基类公有成员和保护成员都相当于派生类的保护成员,派生类可以通过自身的成员函数或其子类的成员函数访问它们。 • ⑵对于基类的私有成员,无论派生类内部成员或派生类的使用者都无法直接访问。
8.4 三种派生方式的区别 • 采用public公有派生,基类成员的访问权限在派生类中保持不变,即基类所有的公有或保护成员在派生类中仍为公有或保护成员。public派生最常用,可以在派生类的成员函数中访问基类的非私有成员,可通过派生类的对象直接访问基类的公有成员。 • 采用private私有派生,基类所有的公有和保护成员在派生类中都成为私有成员,只允许在派生类的成员函数中访问基类的非私有成员。private派生很少使用。 • 采用protected保护派生,基类所有的公有和保护成员在派生类中都成为保护成员,只允许在派生类的成员函数和该派生类的派生类的成员函数中访问基类的非私有成员。
【例8-3】定义类Point,然后定义类Point的派生类Circle。【例8-3】定义类Point,然后定义类Point的派生类Circle。 #include <iostream.h> class Point //定义基类,表示点 { private: int x; int y; public: void setPoint(int a, int b){ x=a; y=b; }; //设置坐标 int getX(){ return x; }; //取得X坐标 int getY(){ return y; }; //取得Y坐标 };
class Circle:public Point //定义派生类,表示圆 { private: int radius; public: void setRadius(int r){ radius=r; }; //设置半径 int getRadius(){ return radius; }; //取得半径 int getUpperLeftX(){ return getX()-radius; }; //取得外接正方形左上角的X坐标 int getUpperLeftY(){ return getY()+radius; }; //取得外接正方形左上角的Y坐标 };
程序运行结果: X=200, Y=250,Radius=100 UpperLeft X=100, UpperLeft Y=350 void main() { Circle c; c.setPoint(200, 250); c.setRadius(100); cout<<"X="<<c.getX()<<", Y="<<c.getY()<<",Radius="<<c.getRadius()<<endl; cout<<"UpperLeft X="<<c.getUpperLeftX()<<", UpperLeft Y="<<c.getUpperLeftY()<<endl; }
程序说明: 派生类Circle通过public派生方式继承了基类Point的所有成员(除私有成员外所有成员的访问权限不变),同时还定义了自己的成员变量和成员函数。 若将类Circle的派生方式改为private或protected,则下述语句是非法的:c.setPoint(200, 250); 无论哪种派生方式,派生类都继承了基类的所有成员,包括私有成员。我们虽然不能在派生类Circle中直接访问私有数据成员x和y,但可以通过继承的公有成员函数getX()、getY()和setPoint()访问或设置它们。
8.5 派生类的构造函数和析构函数 • 基类的构造函数和析构函数不能被继承,在派生类中,如果要对派生类新增加的成员进行初始化,就必须定义派生类自己的构造函数。 • 在创建派生类对象过程中会先创建一个基类的隐含对象,从而使派生类对象可以访问属于隐含对象的相关成员。但是我们在使用派生类时只会说明要创建的派生类对象,而不可能明确说明需要同时创建一个隐含的基类对象。因此,派生类的构造函数要对派生类对象和隐含的基类对象的创建负责。 • 对所有从基类继承来的成员的初始化工作,还是由基类的构造函数完成,因此在定义派生类构造函数时,应对基类的构造函数所需要的参数进行设置。 • 同样,对派生类对象的清理工作也需要定义派生类自己的析构函数。
4.5.1 派生类的构造函数 • 派生类的构造函数的定义: 派生类名::派生类名(基类1形参,基类2形参,...,基类n形参,本类形参):基类名1(参数), 基类名2(参数), ...,基类名n(参数),对象数据成员的初始化 { 本类成员初始化赋值语句; };
说明: ⑴一般来说,基类成员的初始化由派生类的构造函数调用基类的构造函数来完成。 ⑵冒号后的列表称为成员初始化列表(initialization list)。表中,各项用逗号分开。每项对应于一个类,类名之后的圆括号标出该类的初始化参数。 ⑶派生类自己的初始化可在参数列表中完成,也可以在构造函数体内进行。
注意:除非基类有默认的构造函数,否则(即基类的构造函数带有参数)必须定义派生类构造函数,并采用显式调用方式。注意:除非基类有默认的构造函数,否则(即基类的构造函数带有参数)必须定义派生类构造函数,并采用显式调用方式。 派生类构造函数的执行次序: 首先,调用基类构造函数,调用顺序按照他们被继承时声明的基类名顺序执行; 其次,调用内嵌对象构造函数,调用次序为各个对象在派生类内声明的顺序; 最后,执行派生类构造函数体中的内容。
8.5.3 派生类的析构函数 • 析构函数的功能是在类对象消亡之前释放占用资源(如内存)的工作。由于析构函数无参数、无类型,因而派生类的析构函数相对简单。 • 派生类与基类的析构函数彼此独立,只作各自类对象消亡前的善后工作。因而在派生类中有无显式定义的析构函数与基数无关。派生类析构函数执行过程与构造函数执行过程相反。即当派生类对象的生存期结束时,首先调用派生类的析构函数,然后调用内嵌对象的析构函数,再调用基类的析构函数。
【例8-4】派生类构造函数和析构函数的执行。 #include <iostream.h> #include <string.h> class person { char m_strName[10]; int m_nAge; public: person(char * name,int age ) { strcpy (m_strName,name); m_nAge = age; cout<<"constructor of person"<<m_strName<<endl; } ~person() { cout<<"deconstrutor of person"<<m_strName<<endl; } };
class Employee:public person { char m_strDept[20]; person Wang; public: Employee(char * name,int age,char * dept,char * name1,int age1):person(name,age),Wang(name1,age1) { strcpy(m_strDept,dept); cout<<"constructor of Employee"<<endl;} ~Employee(){cout<<"constructor of Employee"<<endl; } };
void main() { Employee emp("张三",40,"人事处","王五",36); } • 程序运行结果: constructor of person张三 constructor of person王五 constructor of Employee constructor of Employee deconstrutor of person王五 deconstrutor of person张三 • 程序说明:从运行结果可以看出构造函数和析构函数的执行顺序。
8.6 多继承和虚基类 • 多继承是现实世界中的普遍现象。多继承是指派生类可以有多个基类,其中的几个基类可能有公共基类。没有公共基类的多继承比较简单,可以视为多个单继承的组合;在有公共基类的多继承中,可能出现二义性问题。
A B C 8.6.1 多继承的定义 class 派生类名:继承方式 基类名1,继承方式 基类名2,…,继承方式 基类名n { …… //派生类新增加的成员声明 }; 下图中,A和B是派生类C的两个基类,按以下格式定义派生类C: Class C:继承方式 A,继承方式 B { //C的类体 } 其中,继承方式可以是public, private 或protected。继承列表中,基类的排列先后次序不限。
【例8-5】多继承可以看成是单继承的扩展。 #include <iostream.h> class A { private: int a; public: void setA(int x){ a = x; } void howA(){cout<<"a="<<a<<endl;} }; class B { private: int b; public: void setB(int x){ b = x; } void showB(){cout<<"b="<<b<<endl;} };
class C:public A,private B //公有继承A,私有继承B { private: int c; public: void setC(int x,int y) { c = x; setB(y); //通过B类的成员函数setB()为B类的私有成员b赋值 } void showC() { showB(); //此处可以使用showB() cout<<"c="<<c<<endl; } };
void main() { C obj; obj.setA(53); obj.showA(); //输出a=53 obj.setC(55,58); obj.showC(); //输出b=58 c=55 } 程序运行结果: a=53 b=58 c=55 • 程序说明:因为类C私有继承类B,类B中的showB()已经改变访问控制为C类中的私有成员,因此,在main()函数中不可以使用obj.showB()。
8.6.2 多继承中的构造函数和析构函数 在创建派生类对象时,要初始化派生类和各个基类的数据成员。因此,需要调用各个构造函数。其次序是: • 按照继承列表中的基类排列次序(从左到右),先调用各基类的构造函数,对基类成员进行初始化。如果某个基类仍是一个派生类,则这个过程递归进行。 • 如果派生类还包括对象成员,则对对象成员的构造函数的调用,仍在初始化列表中进行。 • 最后调用派生类自己的构造函数。 • 当该对象消失时,析构函数的执行顺序与构造函数的执行顺序正好相反。 注意:构造函数(析构函数)是不被继承的,所以,一个派生类只能调用它的直接基类的构造函数。
【例8-6】多继承中的构造函数和析构函数的调用次序。【例8-6】多继承中的构造函数和析构函数的调用次序。 • 见教材P95页。
8.6.3 二义性与虚基类 • 问题1:当派生类继承了基类以后,派生类对象就拥有基类的所有的成员。那么,如果派生类中有和基类成员同名的成员时,派生类的成员将会覆盖基类的同名成员。那么如何访问基类中被覆盖的成员呢(解决同名的二义性)?可以通过在所访问的成员名前加上所属的类域来强制访问基类的成员。
【例8-7】强制访问基类成员 //e8_7.cpp #include<iostream.h> class Base1 { public: void print() { cout<<" Base1"<<endl; } }; class Base2 { public: void print() { cout<<" Base2"<<endl; } };
void main() { Derived d; d.print(); d.Base1::print(); d.Base2::print(); } 程序运行结果: Derived Base1 Base2 class Derived:public Base1,public Base2 { public: void print() { cout<<" Derived "<<endl; } };
问题2:如果Base1和Base2本身又是派生类,并且它们是从同一个基类派生出来的。如图4.3所示。那么,根据前述的继承规则,Derived就间接地继承了BaseBase两次,在创建Derived类对象时,将两次调用BaseBase的构造函数用来初始化BaseBase中的成员,也就是说BaseBase类中的所有成员在Derived对象中有两份。问题2:如果Base1和Base2本身又是派生类,并且它们是从同一个基类派生出来的。如图4.3所示。那么,根据前述的继承规则,Derived就间接地继承了BaseBase两次,在创建Derived类对象时,将两次调用BaseBase的构造函数用来初始化BaseBase中的成员,也就是说BaseBase类中的所有成员在Derived对象中有两份。 • 当一个类多次间接从一个类派生以后,这个类就保留多份间接基类的成员。这种情况有时是合理的,但大多数情况下,我们希望这个派生类只保留一份基类的成员。如何解决这种情况呢?
在C++中,如果在多条继承路径上有一个公共的基类,那么在这些路径中的某几条路径的汇合处,这个公共的基类就会产生多个实例。如果想使这个公共的基类只产生一个实例,可以采用虚拟继承(virtual inheritance ),将这个基类说明为虚基类(virtual base)。 • 例如定义一个Person类(人类),将一般的关于人的信息(如姓名、年龄、性别等)封装起来;然后从Person类派生出两个类Student类(学生类)和Teacher类(教师类);再由Student类和Teacher类派生出一个助教类TeachAssistant。这样,一个助教既具有教师的特性,又有学生的特性。但是助教本身是一个人(而不是两个人),所以TeachAssistant的对象中应该只能有一份Person的数据,如图4.4所示。
图8.4 虚基类 图8.3 BaseBase两次作为Derived的间接基类
如果在定义Teacher类和Student类时,说明是虚基继承Person类(在这种情况下,Person即为虚基类),那么就可以保证由Teacher类和Student类派生出来的其他类只继承这个虚基类一次。C++中的虚基类机制可以保证:当基类通过多条派生路径被派生类继承时,派生类只继承该基类一次,即派生类对象只调用一次基类的构造函数,来初始化基类中的成员,即只保存一份该基类的数据成员的值。如果在定义Teacher类和Student类时,说明是虚基继承Person类(在这种情况下,Person即为虚基类),那么就可以保证由Teacher类和Student类派生出来的其他类只继承这个虚基类一次。C++中的虚基类机制可以保证:当基类通过多条派生路径被派生类继承时,派生类只继承该基类一次,即派生类对象只调用一次基类的构造函数,来初始化基类中的成员,即只保存一份该基类的数据成员的值。 • 虚基类定义形式如下: class Person{ …… }; class Teacher:virtual public Person { …… }; class Student:virtual public Person { …… }; class TeachAssistant:public Teacher , public Stuent { …… }; • 也就是说,在定义派生类时,使用关键字virtual加到对应的基类的名字前,则该基类为虚基类。
一个派生类可以公有或私有继承一个或多个虚基类,关键字virtual和关键字public或private的相对位置无关紧要,但要放在基类名之前,并且关键字virtual只对紧随其后的基类名起作用。例如由虚基类A和虚基类C以及非虚基类B派生出类D的定义如下:一个派生类可以公有或私有继承一个或多个虚基类,关键字virtual和关键字public或private的相对位置无关紧要,但要放在基类名之前,并且关键字virtual只对紧随其后的基类名起作用。例如由虚基类A和虚基类C以及非虚基类B派生出类D的定义如下: class D:virtual public A , private B , virtual public C { //…… }; 例题见教材 P99页
【例8-8】 虚基类应用。 • 见教材P100页
本章小结 • 本章学习了类的继承特性。继承(inheritance)是面向对象程序设计方法的四个基本特征之一,是程序代码可重用性的具体体现。 • 继承是C++语言的重要概念,是实现代码重用的重要机制。类的继承,是新的类从已有类那里得到已有的特性;从已有类产生新类的过程就是类的派生。派生类同样也可以作为基类派生新的类,这样就形成了类的层次结构。类的派生实际是一种演化、发展过程,即通过扩展、更改和特殊化,从一个已知类出发建立一个新类。类的派生通过建立具有共同关键特征的对象家族,从而实现代码的重用。 • 派生新类的过程包括三个步骤:吸收基类成员、改造基类成员和添加新的成员。C++类中,派生类包含了基类中除构造函数和析构函数之外的所有成员。对基类成员的改造包括两个方面,一是基类成员的访问控制问题,依靠派生类定义时的继承方式来控制;二是对基类数据或函数成员的覆盖,对基类的功能进行改造。派生类新成员的加入是继承与派生机制的核心,是保证派生类在功能上有所发展的关键,可以根据实际需要给派生类添加适当的数据和函数成员,来实现必要的新增功能。