类的继承与派生谭浩强C.ppt
《类的继承与派生谭浩强C.ppt》由会员分享,可在线阅读,更多相关《类的继承与派生谭浩强C.ppt(67页珍藏版)》请在淘文阁 - 分享文档赚钱的网站上搜索。
1、类的继承与派生,前面我们主要讨论了面向对象程序设计的第一个重要机制-数据的封装与隐藏特性。而面向对象程序设计中另一个重要机制就是代码的可重用性,代码可重用性的特征实现的主要机制之一就是类的继承。类的继承机制自动为一个类提供来自于另一个类的操作和数据结构,这使得程序员只需在新类中定义已有类中没有的成分来建立新类。更为实际的意义就在于,类的继承机制给程序员提供了无限重复利用程序资源的一种有效途径。通过C+语言中的继承机制,可以扩充和完善旧的程序设计以适应新的需求,这样不仅可以节省程序开发的时间和资源,并且为未来程序设计增添了新的资源。,1基类和派生类,2单继承,3多继承,4虚基类,综上所述,理解继
2、承是理解面向对象程序设计所有方面的关键。所以,本章是整个面向对象程序设计中的重点内容。通过本章的学习,主要理解与掌握基类和派生类、单继承、多继承及虚基类的基本概念及其它们在面向对象程序设计中的基本应用。,基类和派生类,继承的机制提供了利用已有的数据类型来定义新的数据类型的途径。,所定义新的数据类型不仅拥有新定义的成员(数据成员与成员函数),而且还同时拥有已存在的成员。,我们将这种利用已知的类来定义新类的机制称之为类的继承。,我们称已存在的用来定义新类的类为基类,又称为父类。由已存在的类派生出的新类称之为派生类,又称为子类。,这样,派生类继承了它父类的属性和操作。同时,在子类中也可声明新的属性和
3、新的操作,剔除了那些不适合于其用途的继承下来的操作。这种机制,使得程序员可重用父类的代码,将注意力集中在为子类编写新的代码。,继承是我们理解事物,解决问题的方法。继承可帮助我们描述事物的层次关系,帮助我们精确地描述事物,帮助我们理解事物的本质。在解决某一问题时,只要弄清事物所处的层次结构,也就找到了对应的解决方法。,在C+语言中,一个派生类可以从一个基类派生,也可以从多个基类派生。从一个基类派生的继承称为单继承。从多个基类派生的继承称为多继承。单继承形成了类的层次,像一棵倒挂的树。多继承形成了一个有向无环图。如图所示。,基类和派生类,基类和派生类,一派生类的定义,单继承的定义格式如下:,cla
4、ss : ;,其中,是新定义的一个类的名字,它是从中派生的,并且按指定的派生的。,常使用下列三种关键字给予描述:,public:表示公有继承。,private:表示私有继承。,protected:表示保护继承。,这三种继承的意义,在后讨论。,基类和派生类,多继承的定义格式如下:,class : , ,. ;,二派生类的三种继承方式,公有继承(public)、私有继承(private)和保护继承(protected)是常用的三种继承方式。, 公有继承(public),公有继承的特点是基类的公有成员和保护成员作为派生类的成员时,它们都保持原有的状态,而基类的私有成员仍然是私有的。, 私有继承(pr
5、ivate),私有继承的特点是基类的公有成员和保护成员都作为派生类的私有成员,并且不能被这个派生类的子类所继承与访问。,基类和派生类, 保护继承(protected),保护继承的特点是基类的所有公有成员和保护成员都成为派生类的保护成员,并且只能被它的派生类成员函数或友元访问,而这种继承关系还可向其子类进行传递。基类的私有成员仍然是私有的。,为了进一步理解三种不同继承方式在其成员的可见性方面的区别,下面从三种不同角度进行讨论。, 基类成员对基类对象的可见性:在基类外部,通过基类对象对基类成员的可访问性。, 基类成员对派生类成员函数的可见性:派生类成员对继承下来基类成员的可访问性。, 基类成员对派
6、生类对象的可见性:在派生类外部,通过派生类对象对基类成员的可访问性。,对于公有继承方式:, 基类成员对基类对象的可见性:,公有成员可见,其它不可见。这里保护成员等同于私有成员。,基类和派生类, 基类成员对派生类成员函数的可见性:,公有成员与保护成员可见,而私有成员不可见。这里保护成员等同于公有成员。, 基类成员对派生类对象的可见性:,公有成员可见,其它成员不可见。,结论:在公有继承时,派生类的对象可以访问基类中的公有成员。派生类的成员函数可以访问基类中的公有成员和保护成员。,对于私有继承方式:, 基类成员对基类对象的可见性:,公有成员可见,其它成员不可见。, 基类成员对派生类成员函数的可见性:
7、,公有成员和保护成员可见,而私有成员是不可见的。, 基类成员对派生类对象的可见性,所有成员都是不可见的。,结论:在私有继承时,基类的成员只能由直接派生类访问,而无法再往下继承。,基类和派生类,对于保护继承方式与私有继承方式的情况基本相同。两者的区别就在于,基类的公有成员与保护成员在私有继承的方式下,在派生类中不允许再往下继承,而保护继承方式则允许继续往下传递。,可将可见性理解为可访问性。关于可访问性的另外一种规则中,称派生类对象对基类的访问为水平访问,称派生类的派生类对基类的访问为垂直访问。则上述讨论可总结如下:,公有继承时,水平访问和垂直访问对基类中的公有成员不受限制。,私有继承时,水平访问
8、和垂直访问对基类中的公有成员也不能访问。,保护继承时,对于垂直访问同于公有继承,对于水平访问同于私有继承。,结论:对于基类中的私有成员,只能被基类中的成员函数和友元函数所访问,不能被其它的函数访问。,三基类与派生类的关系,任何一个类都可以派生出一个新类,派生类也可作为基类再派生出新类。因此,基类与派生类的关系是相对而言的。,基类和派生类,这就意味着:一个基类可以是另一个基类的派生类,这样便形成了复杂的继承结构,出现了类的层次。一个基类派生出一个派生类,它又可作为另一个派生类的基类,则原来的基类为该派生类的间接基类。其关系如右图所示。,其中,类A是类C的间接基类,而类B是类A的直接派生类。,基类
9、与派生类之间的关系,可从以下几个方面进行理解:, 派生类是基类的具体化。,类的层次通常反映了客观世界中某种真实的模型。例如,定义输出设备为基类,而显示器、打印机等是派生类,它们的关系如下图所示。,基类和派生类,在这种情况下,不难看出:基类是对若干个派生类的抽象,而派生类是基类的具体化。基类抽取了它的派生类的公共特征,而派生类通过增加行为将抽象变为某种有用的类型。, 派生类是基类定义的延续,先定义一个抽象类,该基类中有些操作并未实现。然后定义非抽象的派生类,实现抽象基类中定义的操作,虚函数就属于此类情况。这时,派生类是抽象基类的实现。因此,将派生类看成基类定义的延续,是常用的方法之一。, 派生类
10、是基类的组合,在多继承时,一个派生类有多于一个的基类,这时派生类将是所有基类行为的组合。,派生类将其本身与基类区别开来的方法是添加数据成员和成员函数。因此,继承的机制将使得在创建新类时,只需说明新类与已有类的区别,从而大量原有的程序代码都可以复用,所以可以称类是“可复用的软件构件”。,单继承,在单继承中,每个类中可以有多个派生类,但是每个派生类只能有一个基类,从而形成树形结构,如下图所示。,单继承,一成员访问权限的控制,例5.1 分析下列程序中的访问权限,并回答所提出的问题。,#include class A public: void f1(); protected: int j1; priv
11、ate: int i1; ;,class B:public A public: void f2(); protected: int j2; private: int i2; ;,class C:public B public: void f3(); ;,问题:, 派生类B中成员函数f2()能否访问基类A中的成员:f1()、i1和j1?,答案:可访问f1()和j1,不可访问i1 。,单继承, 派生类B的对象b1能否访问基类A中的成员:f1()、i1和j1?,答案:可访问f1(),而不可访问i1和j1 。, 派生类C中成员函数f3()能否访问直接基类B中的成员:f2()和j2?能否访问间接基类A中
12、的成员f1()、j1和i1?,答案:f3()可以访问f2()与j2及间接基类中的f1()和j1,不可以访问i2和i1 。, 派生类C的对象c1能否访问直接基类B中的成员:f2()、i2和j2?能否访问间接基类A中的成员:f1()、j1和i1?,答案:可以访问f2()和f1(),其它的都不可访问 。,由此分析,可得出结论:在公有继承时,派生类的成员函数可访问基类中的公有成员和保护成员。派生类的对象仅可访问基类中的公有成员。,考虑:将程序中的两处继承方式的public改为private,上述的访问权限又如何变化?,单继承,例5.2 分析下列程序,指出错误之处,并给出纠正办法。,#include c
13、lass A public: void f(int i) coutiendl; void g() coutgn; ;,class B:A public: void h() couthn; A:f;/此语句的含意为:将类A中的成员名为f的成员作为B类公有成员. ;,单继承,void main() B d1; d1.f(6); d1.g(); d1.h(); ,g()在类A中为公有成员,继承到B类中为私有成员,故B类对象对其无访问权限。,说明:使用class关键字定义类时,缺省的继承方式是private。,例5.3 分析下列程序,指出错误之处,并给出纠正办法。,#include #include
14、class A public: A(const char *nm) strcpy(name,nm); private: char name80; ;,单继承,class B:public A public: B(const char *nm) :A(nm) void PrintName() const; ;,继承类的构造函数必须包含对基类对象的构造.,void B:PrintName() const coutname: nameendl; ,name为A类的私有成员,因此不允许被其它类的成员访问.,void main() B b1(Wang Li); b1.PrintName(); ,单继承,
15、二构造函数与析构函数,派生类的构造函数与析构函数如何定义及派生类对象的构造与析构是继承讨论中的主要问题,因此本节所讨论的课题是继承问题中的重点内容。, 构造函数,派生类对象的数据结构(属性)是由基类中说明的数据成员和派生类中说明的数据成员共同组成。因此,派生类对象中包含有由基类中说明的数据成员和操作所构成的封装体,称之为基类子对象,它必须由基类中的构造函数来完成对其的构造及初化的操作。,构造函数不能被继承,所以,派生类的构造函数必须通过调用基类的构造函数来初始化基类的子对象。因此,在定义派生类的构造函数时除了对自己的数据成员进行初始外,还必须负责调用基类构造函数使基类的数据成员得以初始化。如果
16、派生类中还有子对象时,还应包含对子对象初始化的构造函数。,派生类对象的构造,基类子对象构造与初始化,自身声明数据成员的初始化,基类构造函数,派生类构造函数,单继承,派生类构造函数的一般格式如下:,():(),(),. ,派生类构造函数的调用顺序如下:,基类的构造函数,调用顺序按照被继承时声明的顺序。,子对象类的构造函数(如果有),调用顺序按照定义顺序。,派生类构造函数 。,例5.4 分析下列程序的输出结果:,#include,单继承,class A public: A() a=0; coutA类默认构造函数被调用!n; A(int i) a=i; coutA类构造函数被调用!n; A() co
17、utA类对象被析构!n; void Print() const couta,; int Geta() return a; private: int a; ;,单继承,class B:public A public: B() b=0; coutB类默认构造函数被调用!n; B(int,int ,int); B() coutB类对象被析构!n; void Print() A:Print(); coutb,aa.Geta()endl; private: int b; A aa;/A类的子对象 ;,单继承,B:B(int i,int j,int k):A(i),aa(j) b=k; coutB类构造函
18、数被调用!n; ,void main() B bb2; bb0=B(1,2,5); bb1=B(3,4,7); for(int i=0;i2;i+) bbi.Print(); ,单继承,单继承,说明:,先创建两个对象元素的对象数组。调用两次类B的默认构造函数,每调用类B的默认构造函数,先调用两次类A的默认构造函数来创造基类子对象与aa子对象,于是出现前6行的输出结果。,在程序的两个赋值语句中,系统调用B类构造函数建立匿名对象,赋值后,再调用析构函数析构对象,由于对象析构顺序与构造顺序相反,所以出现两个调用派生类B的构造函数与析构函数的12行信息。,输出两个类B对象的数据成员值,占有两行信息。,
19、最后,程序结束时,要析构数组对象,因此,输出最后6行信息。,析构函数,我们知道,当对象的生命周期结束时,系统会自动调用析构函数完成对象的析构,对于派生类的对象也是如此。也就是说,当派生类对象的生命周期结束时,派生类的析构函数被执行。由于析构函数同样没有继承性,因此,在执行派生类的析构函数时,基类的析构函数也将被调用。在这里,我们关心的是执行顺序的问题。由于析构顺序与构造顺序相反,所以在执行析构函数时,先执行派生类的析构函数,再执行基类的析构函数。,单继承,例5.5 分析下列程序的运行结果。,#include class M public: M(int i=0,int j=0) m1=i; m2
20、=j; void print() coutm1,m2,; M() coutM类析构函数被调用!n; private: int m1,m2; ;,class N:public M public: N() n=0; N(int i,int j,int k):M(i,j),n(k) void print() M:print(); coutnendl; N() coutN类析构函数被调用!n; private: int n; ;,单继承,void main() N n1(5,6,7),n2(-2,-3,-4); n1.print(); n2.print(); ,单继承,派生类构造函数使用中应注意的问题
21、,派生类构造函数的定义中可以省略对基类构造函数的调用,其条件是在基类中必须有默认的构造函数或根本没有定义构造函数。如果是这样的话,则派生类不必负责调用基类的构造函数。,例5.6 分析下列程序的输出结果。,#include class A public: A(int i=0) a=i; void print() couta,; private: int a; ;,单继承,void main() B d1,d2(5),d3(4,5,6); d1.print(); d2.print(); d3.print(); ,class B:public A public: B() b1=b2=0; B(int
22、 i) b1=i; b2=0; B(int i,int j,int k):A(i),b1(j),b2(k) void print() A:print(); coutb1,b2endl; private: int b1,b2; ;,单继承,当基类的构造函数使用一个或多个参数时,则派生类必须定义构造函数,提供将参数传递给基类构造函数的途径。如果派生类构造函数的作用仅仅起到这样的作用,则其函数体可以为空。,例如有以下定义: class B public: B(int i,int j) b1=i; b2=j; /. private: int b1,b2; ;,单继承,class D:public B
23、public: D(int i,int j,int k,int l,int m):B(i,j),bb(k,l) d1=m; /. private: int d1; B bb; ;,分析:派生类D的构造函数中有5个参数,前两个参数传递给基类B的构造函数,接着的两个参数用于构造子对象同样传递给B类的构造函数,最后一个参数用于派生类D数据成员的初始化。,单继承,派生类生成过程, 吸收基类成员,在类的继承中,首先是派生类先将基类的成员全盘接收。这样,派生类实际上就包含了基类中除构造函数、析构函数之外的所有成员。问题是,尽管很多基类的成员,特别是非直接基类的成员,在派生类中很可能根本就不起作用,却也被继
24、承下来,在生成对象时也要占用内存空间,造成资源浪费。, 改造基类成员,对基类成员的改造包括两个方面,一是基类成员的访问控制问题,主要依靠派生类定义时的继承方式来控制。第二是对基类数据成员或成员函数的覆盖,就是在派生类中定义一个和基类数据成员或成员函数同名的成员,由于作用域不同,于是发生同名覆盖,基类中的成员就被替换成派生类中的同名成员。, 添加新的成员,派生类新成员的加入是继承与派生机制的核心,是保证派生类在功能上有所发展的关键。程序员可以根据实际情况的需要,给派生类添加适当的数据和函数成员,来实现必要的新增功能。在这里,特别强调的是,在派生过程中,基类的构造函数和析构函数是不能被继承下来的。
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 继承 派生 谭浩强
限制150内