590 likes | 838 Views
面向对象的 C++ 程序设计 基础. 第 4 章 继承与派生. 本章主要内容. 派生类的声明方式与构成 派生类成员的访问属性 派生类的构造函数和析构函数 多继承与虚基类. 派生类的声明方式与构成. 继承与派生的概念. 继承 (inheritance) 是面向对象的基本特性,即从已有的类那里得到已有的特性或类的定义; 从已有的类产生新类的过程就是派生 (derivation); 已有的类称为基类或父类,产生的新类称为派生类或子类;. 学生. 研究生. 大学生. 中学生. 小学生. 高中生. 初中生. 学生的分类层次图.
E N D
面向对象的C++程序设计基础 第4 章 继承与派生
本章主要内容 • 派生类的声明方式与构成 • 派生类成员的访问属性 • 派生类的构造函数和析构函数 • 多继承与虚基类
派生类的声明方式与构成 继承与派生的概念 • 继承(inheritance)是面向对象的基本特性,即从已有的类那里得到已有的特性或类的定义; • 从已有的类产生新类的过程就是派生(derivation); • 已有的类称为基类或父类,产生的新类称为派生类或子类;
学生 研究生 大学生 中学生 小学生 高中生 初中生 学生的分类层次图 继承与派生的图示
继承与派生的目的 • 继承的目的:实现代码重用。 • 派生的目的:当新的问题出现,原有程序无法解决(或不能完全解决)时,需要对原有程序进行改造。
派生类生成过程 • 三个步骤: (1)吸收基类成员(除构造函数和析构函数外,新类将基类的所有成员全部接受)。因此要慎重选择基类,尽量使继承来的成员发挥作用; (2)改造基类成员(在新类中定义继承方式来控制继承来成员的访问权限;在新类中定义与基类中同名的数据或函数,即“同名覆盖”,实现以新成员取代基类的成员); (3)添加新的成员,包括数据和函数(例如构造函数和析构函数); • 派生类是基类定义的延续。
基类 派生类 Person类 string name; int age; char sex; void display() Student类 string name; int age; char sex; void display() int num; int class; void display() 数据成员 继承 成员 函数 新增 基类和派生类的关系 派生类的构成
继承方式 • 三种继承方式 • 公有继承 • 私有继承 • 保护继承 • 不同继承方式的影响主要体现在: • 派生类成员对基类成员的访问权限 • 通过派生类对象对基类成员的访问权限
基类与派生类的对应关系 • 单继承 • 派生类只从一个基类派生。 • 多继承 • 派生类从多个基类派生。 • 多重派生 • 由一个基类派生出多个不同的派生类。 • 多层派生 • 派生类又作为基类,继续派生新的类。
派生类的声明方式 • 派生类的声明形式与普通类的声明形式基本相同; • 只是必须在类声明的头部指明它的基类以及继承方式,如public、private或者protected; • 继承方式必须且只能选择一个,缺省默认为private; • 单继承和多继承的定义形式;
1、单继承定义格式 class <派生类名>:<继承方式> <基类名> { <派生类新定义成员> //隐藏基类的成员 //代码重用 }; 例: class A {// A类的成员 }; class B: public A {// B类的成员 };
2、多继承定义格式 class <派生类名>:<继承方式1><基类名1>, <继承方式2><基类名2>… { <派生类新定义成员> }; 例 class A {// A类的成员 }; class B {// 类B的成员 }; class C: public A, private B {// 类C新添加的成员 };
小结 • 理解继承的概念 • 理解派生的概念 • 掌握单继承和多继承的定义方式
派生类成员的访问属性 • 基类的成员可以有public(公有)、protected(保护)和private(私有)三种访问属性; • 在派生类中,从基类继承来的成员,其访问属性由继承方式控制; • 不同的继承方式使得具有不同访问属性的基类成员在派生类中的访问属性也不同:包括派生类的新增成员对继承来的基类成员的访问,以及派生类外部,通过派生类对象对继承来的基类成员的访问。
(一) 公有继承(Public) • 基类的私有成员在公有继承后,在派生类中变得不可访问,仍是基类的私有成员; • 基类公有成员的访问权,在定义派生类时,定义的继承方式不同,权限也不同; • 基类的公有成员和保护成员在公有继承的派生类中,其访问属性保持不变; • 派生类对象只能访问从基类继承的公有成员。 • 例:一个公有继承的例子。 • 私有成员的不可访问性体现了基类的数据封装性,保护了基类的私有成员。
(二)私有继承(Private ) • 对于基类的私有成员,派生类及派生类的使用者都无权访问。无论是公有继承还是私有继承,派生类都不能直接访问基类的私有成员,只能通过调用基类的成员函数来访问它; • 基类的公有成员和保护成员在私有继承后,都以私有成员身份出现在派生类中,派生类的其它成员可以直接访问它们,但其外部对象无法访问; • 私有继承时派生类对象不可访问从基类继承来的任何成员。 • 例:私有继承的例子。 • 私有继承后,中止了基类功能的继续派生,比较少用。
(三)保护继承(Protected) • 当派生类使用保护继承方式派生时,基类的公有成员和保护成员在派生类都具有保护成员访问属性; • 保护派生类的成员函数可以访问基类的公有和保护成员,但不能访问基类的私有成员; • 在继承关系中, protected访问属性的成员只能被派生类的成员函数访问; • 保护派生类对象只能访问派生类自己的公有成员,不能访问基类的任何成员; • 要想使基类的私有成员既便于派生类访问,又禁止外界对它的操作,可以把私有成员定义为保护成员。 • 例4.4.3:一个保护继承的例子。
派生类如何选用继承方式? • 公有继承:公有继承中,基类的特性在派生类对象中能够完整地体现,基类的公有成员在派生类中仍然是公有的,可以象使用基类对象一样使用这个派生类对象; • 私有继承:私有继承后,基类的特性在派生类对象中完全体现不出来,隐藏在派生类对象背后,基类在派生类中所起的作用相当于派生类的一个私有对象成员,这个对象成员可以为派生类发挥作用; • 保护继承:它将基类的公有成员和保护成员都变成派生类的保护成员,使派生类的派生类可以访问它们。
A B C 多级派生 多层派生时的访问属性 • 类A为基类,类B是类A的派生类,类C是类B的派生类,则类C也是类A的派生类; • 类B称为类A的直接派生类,类C称为类A的间接派生类; • 类A是类B的直接基类,是类C的间接基类。 • 例4.4.4多层派生的访问属性。
多层派生时的访问属性 class C:protected B //protected方式 { public: void fc(); private: int c1; }; class A //基类 { public: int a1; void fa(); protected: int a2; private: int a3; }; class B:public A //public方式 { public: int b1; protected: void fb(); private: int b2 };
例 • 设计一个大、中学生和教师的管理程序。教师包括编号、姓名、职称、教研室数据的输入输出;大学生包括编号、姓名、性别、班号、英语、高等数学和数据结构3门课程成绩的输入输出并计算平均分;中学生包括编号、姓名、性别、班号、英语、数学和语文3门课程成绩的输入、输出并计算平均分。
类person 类teacher 类student 类unstudent 类mistudent 例类的继承关系
小结 • 掌握继承的几种方式:public继承、private继承和protected继承下基类成员在派生类中的访问属性的差别。
派生类的构造函数和析构函数 继承时的构造函数 • 派生类不继承基类的构造函数; • 对派生类成员初始化时需要新的构造函数,完成继承来的基类数据成员的初始化和派生类新添数据成员的初始化; • 继承的基类数据成员初始化要调用基类的构造函数,由派生类的构造函数为其设定参数(即传递参数给基类的构造函数),派生类的构造函数还要完成对新添数据成员的初始化;
派生类构造函数的构造条件 • 如果基类没定义构造函数,派生类也可以不定义;如果基类定义了带参数的构造函数,派生类必须定义新的构造函数,并为基类构造函数传递参数。 • 在两种情况下,必须定义派生类的构造函数: • 派生类本身需要构造函数; • 在定义派生类对象时,其相应的基类对象需调用带有参数的构造函数。
简单派生类的构造函数 • 简单的派生类只有一个基类,只有一级派生,且派生类的数据成员不包含其它类的对象; • 简单派生类的构造函数的语法形式: 派生类名(总参数表):基类名(参数表) { 派生类中新增成员的初始化; } :前是派生类构造函数的主干,总参数表包含基类和派生类数据成员初始化需要的所有参数。
简单派生类的构造函数 • 建立一个派生类对象时,构造函数的执行顺序是: (1) 调用基类的构造函数; (2) 执行派生类的构造函数体中的内容。
简单派生类的构造函数例子 • 例简单派生类的构造函数。 • 派生类构造函数名Student1后面括号内的参数表中包括参数类型和参数名(如int n); • 基类构造函数名后括号内的参数表只有参数名而不包括参数类型(即n, nam),其实这不是定义基类的构造函数,而是调用基类的构造函数,参数(n,nam)是实参; • 建立对象时指定的3个实参,顺序传递给基类和派生类的构造函数。
有子对象的派生类的构造函数 • 如果一个类的数据成员包含其它类的对象,则该对象就称为子对象(sub-object); • 子对象的初始化, 是在建立派生类对象时通过调用派生类的构造函数实现; • 有子对象的派生类的构造函数的语法形式: 派生类名::派生类名(总参数表):基类名(参数表), 子对象名(参数表) { // 派生类中新增成员的初始化; }
有子对象的派生类的构造函数 • 参数总表:包括初始化基类数据、新增对象数据及新增一般成员所需的全部数据; • 基类名、子对象名之间的次序可以任意,编译系统是根据相同的参数名(而不是根据参数的顺序)来确立它们的传递关系的。但是习惯上一般先写基类构造函数; • 建立派生类对象时,构造函数的执行顺序是: (1) 调用基类的构造函数; (2) 调用子对象的构造函数; (3) 执行派生类的构造函数体中的内容。
含子对象的派生类构造函数例子 • 例包含子对象的派生类的构造函数。 • 其中Student1构造函数的参数表给出了基类、子对象、新增成员初始化的所有参数,根据基类和子对象构造函数调用的实参名来决定使用哪个参数。
多层派生时的构造函数 • 多层派生的层次结构:一个派生类还可以作为基类,派生出新的类,依次继续,形成派生的层次结构。 • 例 多层派生类的构造函数。 • 多层派生时,构造函数的定义只需写出直接基类的构造函数; • 先初始化基类Student的数据成员num和name; • 再初始化Student1的数据成员age; • 最后再初始化Student2的数据成员score。
派生类构造函数的特殊形式 (1) 派生类的构造函数可以是空函数; (2) 若在基类中没定义构造函数或定义了无参构造函数,则派生类构造函数的定义就不写基类的构造函数; (3) 若在基类和子对象类型的声明中都没有定义带参数的构造函数,而且不需要对派生类自己的数据成员初始化,则可以不显式地定义派生类构造函数; (4) 若在基类或子对象类型的声明中定义了带参数的构造函数,则必须显式地定义派生类的构造函数,并在派生类构造函数中写出基类或子对象类型的构造函数及其参数表; (5) 若在基类中既定义了无参的构造函数,又定义了有参的构造函数(构造函数重载),则定义派生类构造函数时,既可以包含基类构造函数及其参数,也可以不包含基类构造函数。
派生类的析构函数 • 析构函数也不被继承,派生类自行声明。在派生类中是否要定义析构函数与它所属的基类无关; • 派生类析构函数的定义与无继承关系的类的析构函数定义相同,它只负责清理新增的非对象成员; • 系统会自动调用基类及对象成员的析构函数来对基类和对象成员进行清理,但执行次序和构造函数正好相反(先派生类,再基类对象,后基类)。 • 例给出下列程序的输出结果。
小结 • 掌握派生类构造函数的定义 • 掌握构造派生类对象时,构造函数的执行顺序
base1 base2 base3 derive1 derive2 derive3 derive4 多继承与虚基类 声明多继承的方法 • 一个派生类由多个基类派生而来,这种情况称为多继承。
声明多继承的方法 定义多继承派生类时,要指出它的所有基类名及继承方式; 声明形式: class 派生类名:继承方式1 基类名1,继承方式2 基类名2,… 继承方式n 基类名n { 派生类新增成员 }; 多继承中,基类成员在派生类中的可访问性与单继承的规则相同。
多继承派生类的构造函数 • 多继承派生类的构造函数定义与单继承相似,但几个基类的构造函数之间用“,”分隔。形式如下: • 派生类名::派生类名(参数总表):基类1(参数表1),…基类n(参数表n),对象成员1(参数表1),…对象成员n(参数表n) { //派生类新增成员的初始化语句; } • 派生类构造函数要完成新增构造函数的调用以及所有基类构造函数的调用; • 参数总表包含其后的各个分参数表,即构造函数的参数必须包含完成所有基类初始化以及子对象初始化所需要所有参数,即参数总是分配给后面的参数表1、参数表2,……。
多继承构造函数的执行顺序 • 多继承构造函数的执行顺序按派生类定义时从左到右的顺序; • 先祖先(基类),调用基类构造函数,调用顺序按照它们被继承时说明的顺序(从左向右); • 再客人(成员对象),调用子对象的构造函数,调用顺序按照它们在类中说明的顺序; • 后自己(派生类),执行派生类构造函数体中的内容 ; • 注意:在多个基类之间严格按照派生类定义时从左到右的顺序排列先后。
例 多继承构造函数举例 #include <iostream> using namecpace std; class B1 //基类B1,构造函数有参数 {public: B1(int i) {cout<<"constructing B1 "<<i<<endl;} }; class B2 //基类B2,构造函数有参数 {public: B2(int j) {cout<<"constructing B2 "<<j<<endl;} }; class B3 //基类B3,构造函数无参数 {public: B3(){cout<<"constructing B3 *"<<endl;} };
class C: public B2, public B1, public B3 { public: //派生类的公有成员 C(int a, int b, int c, int d): B1(a),memberB2(d),memberB1(c),B2(b) {} private: //派生类的私有对象成员 B1 memberB1; B2 memberB2; B3 memberB3; }; void main() { C obj(1,2,3,4); } 运行结果: constructing B2 2 constructing B1 1 constructing B3 * constructing B1 3 constructing B2 4 constructing B3 * 43
例 多继承构造函数举例 • C(int a,int b,int c,int d):B1(a),memberB2(d),memberB1(c),B2(b) { } • 构造函数的参数表中给出了基类及成员对象所需的全部参数;在冒号之后调用各个基类及子对象的构造函数; • 基类构造函数的执行顺序按照它们在派生类中声明继承的顺序(B2,B1,B3 );子对象构造函数的执行顺序也按照其在派生类中声明的顺序( B1,B2,B3 ) 。
例 多继承构造函数举例 • 需要注意: 1、在类C的构造函数中,没有列出全部基类和成员对象,因B3类只有默认构造函数,不需传递参数,故基类B3以及B3类成员对象memberB3不必列出; 2、在类C的构造函数中,基类名和成员对象名的顺序可随意。派生类构造函数体为空,实际上它只起到传递参数和调用基类和子对象构造函数的作用。
多继承的析构函数 • 析构函数也不被继承,派生类自行声明; • 派生类是否需要析构函数只决定于自身类是否需要,与基类无关; • 声明方法与一般(无继承关系时)类的析构函数相同。 • 不需要显式地调用基类的析构函数,系统会自动隐式调用。 • 析构函数的执行顺序与构造函数的执行顺序正好相反。 • 例4.6.2 多继承派生类的构造函数与析构函数。
例: 派生类析构函数举例 #include <iostream> using namecpace std; class B1 //基类B1声明 { public: B1(int i) {cout<<"constructing B1 "<<i<<endl;} ~B1() {cout<<"destructing B1 "<<endl;}}; class B2 //基类B2声明 {public: B2(int j) {cout<<"constructing B2 "<<j<<endl;} ~B2() {cout<<"destructing B2 "<<endl;}}; class B3 //基类B3声明 {public: B3(){cout<<"constructing B3 *"<<endl;} ~B3() {cout<<"destructing B3 "<<endl;}};
class C: public B2, public B1, public B3 {public: C(int a, int b, int c, int d): B1(a),memberB2(d),memberB1(c),B2(b){} private: B1 memberB1; B2 memberB2; B3 memberB3; }; void main() { C obj(1,2,3,4); } 48
运行结果 constructing B2 2 constructing B1 1 constructing B3 * constructing B1 3 constructing B2 4 constructing B3 * destructing B3 destructing B2 destructing B1 destructing B3 destructing B1 destructing B2
多继承的二义性 • class A • { public: • void setA(int a); • int get(); • private: • int a;}; • class B • { public: • void setB(int b); • int get(); • private: • int b;}; • class C:public A,public B • { public: • void setC(int c); • int getC(); • private: • int c;}; • 解决办法:同名覆盖 • class A • { public: • void setA(int a); • int get(); • private: • int a;}; • class B • { public: • void setB(int b); • int get(); • private: • int b;}; • class C:public A,public B • { public: • void setC(int c); • int get(); • private: • int c;}; • 调用不同基类的同名成员时可能出现二义性。 • C obj; obj.get();有二义性 • 可改为obj.A::get();