C 应用与开发案例教程(中)ppt.ppt_第1页
C 应用与开发案例教程(中)ppt.ppt_第2页
C 应用与开发案例教程(中)ppt.ppt_第3页
C 应用与开发案例教程(中)ppt.ppt_第4页
C 应用与开发案例教程(中)ppt.ppt_第5页
已阅读5页,还剩249页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

C+应用与开发案例教程(中),第6章 继承与派生,6.1 概述,继承是C+语言的一种重要机制,该机制允许在既有类的基础上定义新的类,而不需要把既有类的内容重新书写一遍。继承是通过派生方式实现的。 一个基类可以派生出多个派生类,这种从一个基类中的继承叫单继承。如果一个派生类由多个基类派生出来,称为多继承。派生类也可以作为基类再派生出新的类,形成类的层次结构。 类有三种继承方式:公有继承(public)、私有继承(private)和保护继承(protected)。我们可以通过不同的继承方式限定成员的访问权限,以达到修改已有成员的目的。我们还可以通过同名覆盖、作用域限定符和虚基类的方法达到此目的。派生类继承了基类的所有成员,但构造函数和析构函数除外,因此,在派生类中,想要进行特别的初始化和清理工作时,就要加入新的构造函数和析构函数。,6.1 概述,多态性是面向对象程序设计中的又一重要特征,多态的实现可分为编译时的多态和运行时的多态。编译时的多态,指程序在编译过程中确定函数操作的具体对象,通过函数重载来实现;运行时的多态,是指程序在运行过程中才能确定函数操作的具体对象,通过虚函数来实现。虚函数为我们提供了一种更为灵活的多态性机制,它体现的是运行时的多态,它允许函数调用与函数体之间的关系在运行时才建立。虚函数是用virtual关键字声明的非静态成员函数。当将基类的同名函数定义为虚函数时,我们就可利用基类类型的指针访问该指针指向的派生类对象的同名原型函数,从而实现运行过程的多态。,6.2 派生类的概念,6.2.1 基类和派生类 继承性在客观世界中是一种常见的现象。例如:当一个小孩出生时,就从父亲和母亲那里继承了一定的特征。随着时间的推移和环境的变化,这个小孩逐渐有了自己的性格特征,因此这个小孩就具备了从父母那里继承来的以及自己所独有的特征的组合。 从面向对象程序设计的观点看,继承所表达的正是这样一种类与类之间的关系,这种关系允许在既有类的基础上创建新的类。在最简单的情况下,一个类B继承类A或者从类A派生出类B,通常类A称为基类(父类),类B称为派生类(子类)。这时,类B的对象具有类A对象的所有特性。也可以这样说,类B从类A派生出来。这意味着类B至少描述了与类A同样的接口,至少包含了同类A一样的数据,可以共享类A的成员函数。,6.2 派生类的概念,类B继承了类A(或称类A派生了类B),那么类A的公有段成员可以传递给派生类B,当作类B自己的成员。在创建派生类对象时,先要调用基类的构造函数,以便分配基类的公有段成员存储空间;实际上,由于调用了构造函数,一个派生类对象包含有一个基类对象,只是对于派生类而言,不能访问基类的私有段成员。 派生类的定义格式如下: class 派生类名:继承方式 基类名 /派生类成员定义 ,6.2 派生类的概念,在上面的格式中,派生类名是新定义的一个类名,它是从由“基类名”所标识的类派生而来的。这样定义的派生类继承了基类的除了构造函数和析够函数之外的所有成员,因此派生类对象由两部分组成:一部分是由基类继承的成员,另一部分是派生类新增加的自己特有的成员。 “继承方式”用于规定派生类中从基类继承到的那部分成员在派生类中的访问控制权限。继承方式可用下列3个关键字之一来指定:public:公有继承;protected:保护继承; private:私有继承。,6.2 派生类的概念,【例6-1】 #include class A private: int pri; public: int pub; void set_pri (int a) pri=a; void set_pub (int a) pub=a; ,void out_pri( ) coutpri” ”endl; ; class B:public A private: int pri1; public: int pub1; void set_pri1(int a) pri1=a; ,6.2 派生类的概念,void set_pub1(int a) pub1=a; void out ( ) coutpub” ”pri1” ”pub1endl; ;,void main( ) A objA; objA.set_pri(1); objA.set_pub(2); objA.out_pri(); B objB; objB.set_pri(3); objB.set_pub(4); objB.set_pri1(5); objB.set_pub1(6); objB.out_pri(); objB.out(); ,6.2 派生类的概念,程序的输出结果为: 1 3 4 5 6 该程序创建了两个对象objA(A类对象)和objB(B类对象)。尽管B类没有包含像out_pri( )这样的成员函数说明,对象objB仍可以调用基类的成员函数。使用了继承,B类的任何对象也是A类的对象。 该程序创建了两个对象objA(A类对象)和objB(B类对象)。尽管B类没有包含out_pri( )函数声明,但对象objB仍然可以调用out_pri( ),因为B类继承了A类的公有成员函数out_pri( )。,6.2 派生类的概念,6.2.2 继承方式 1公有继承 在上一小节中,我们用了公有继承的例子,这种类型的继承是最常用的。 对于公有继承来说,基类的公有段成员和保护段成员在派生类中的访问属性不变。派生类的其他成员可以直接访问继承来的这些成员。而外部使用者只能通过对象来访问其公有成员。基类的私有成员是不可访问的(友员除外),就是说,无论是派生类的成员还是派生类的对象都无法访问基类的私有成员。应注意的是:基类的保护成员只能被派生类的成员访问,不能被派生类的对象访问。,6.2 派生类的概念,【例6-2】 #include class A protected: int i,j; public: void get_ij( ); void show_ij( ) ;,class B: public A int k; public: int get_k( ); void make_k( ); ; class C: public B public: void f( ); ;,6.2 派生类的概念,这里,类B由基类A公有派生而来,则A公有段和保护段的成员在B中也是公有段和保护段的成员。同时,B又公有派生出类C,这时,B的公有段和保护段的成员(包括从A继承过来的成员)在C中也是公有段和保护段的成员。下面是各成员函数的实现代码:,6.2 派生类的概念,void A: get_ij( ) coutij; void A: show_ij( ) couti” ”j”n”; ,int B: get_k( ) return k; void B: make_k( ) k=I +j; void C: f( ) i=5; j=6; ,6.2 派生类的概念,可见,在B和C中都可以直接使用基类A的保护段中的成员i和j。下面的实现说明了怎样使用这些派生类。 void main( ) B objB; C objC; objB.get_ij( ); objB.show_ij( ); objB.make_k( ); coutobjB.get_k( )”n”; objC.f( ); objC.show_ij( ); ,6.2 派生类的概念,程序的输出结果为: Enter the two numbers:1 2 1 2 3 5 6 总之,一个派生类如果从基类公有派生出来,则基类成员的访问权限在派生类中保持不变。,6.2 派生类的概念,2. 私有继承 当类的继承方式为私有继承时,基类公有段成员和保护段成员在派生类中作为私有成员,派生类的其他成员可以直接访问它们。但是,在外部通过派生类的对象无法访问。基类的私有成员仍然不可访问(友员除外),即不允许派生类的成员函数访问基类的私有成员。 经过私有继承之后,所有基类的成员都成为派生类的私有成员,如果进一步派生的话,新的派生类的成员函数就不能访问已变成私有的基类的成员。修改前面公有派生的例子如下:,6.2 派生类的概念,【例6-3】 #include class A protected: int i, j; public: void get_ij( ); void show_ij( ); ;,class B: private A int k; public: int get_k( ); void make_k( ); ; class C: public B public: void f( ); ;,6.2 派生类的概念,这里,派生类B由基类A私有派生,A的公有段和保护段的成员在B中变成了私有段的成员。同时,C又由类B公有派生,则B的公有段和保护段的成员(注意:并不包括从A继承过来的成员,它们属于B的私有段)成为C的公有段和保护段的成员。下面是各成员函数的实现:,6.2 派生类的概念,void A: get_ij( ) coutij; void A: show_ij( ) couti” ”j”n”; int B: get_k( ), return k; void B: make_k( ) k=ij; void C: f( ) i=5; /错误 j=6; /错误 ,6.2 派生类的概念,类A的保护段成员i和j已经成了类B的私有段成员,因此,B的派生类C以及B的对象都不能访问A的保护段成员i和j、以及A的公有段成员函数get_ij( )和show_ij( )。下面的实现说明了这些派生类的使用。 void main( ) B objB; C objC; objB.get_ij( ); /错误,外部对象无法访问私有成员 objB.show_ij( ); /错误,外部对象无法访问私有成员 objB.make_k( ); coutobjB.get_k( )”n”; objC.show_ij( ); /错误,外部对象无法访问私有成员 ,6.2 派生类的概念,3. 保护继承 无论是公有继承,还是私有继承,派生类都不能直接访问基类的私有成员,要想访问某些私有成员只能通过调用基类的成员函数,这样显得很不方便。要想使用这些私有成员既便于派生类访问,又禁止外界对它的操作,可以把这些私有成员定义为保护成员。当派生类使用保护继承方式派生时,基类的公有成员和保护成员在派生类中具有保护成员访问属性。这样,派生类的其他成员函数就可以直接访问它们,但在类外部通过派生类的对象无法访问。而基类的私有成员仍然是不可访问的(友员除外)。,6.2 派生类的概念,对于私有继承和保护继承的直接派生类中,基类私有成员都是不可见的,基类保护成员和公有成员是可见的;对派生类对象而言,所有成员都不可见。两种继承方式的效果似乎完全相同。但是,如果派生类作为新的基类,继续派生时,二者的区别就出现了。 假设A类以私有继承方式派生B类,B类又派生C类,那么C类的成员函数和对象都不能访问间接从A类中继承来的成员。 如果A类以保护继承方式派生B类,那么A类中的公有和保护成员在B类中都是保护成员。B类再派生出C类后,A类中的公有和保护成员被C类继承后,有可能是保护的或者是私有的(由类C对类B的继承方式决定)。因此,C类的成员有可能可以访问间接从类A中继承来的成员。,6.2 派生类的概念,【例6-4】 #include class A int i; protected: int j; public: void get_ij( ); coutij; ,void show_ij( ); couti“ ”j”n”; ; class B:protected A int k; public: int get_k( ); return k; ,6.2 派生类的概念,void make_k( ); k=i+j; /错误,不能访问基类私有成员 ; class C: public B public: void f( ); i=5; /错误,不能访问基类私有成员 j=6; /正确 ,在派生类中是保护成员 ;,6.2 派生类的概念,6.2.3 调整访问声明 基类成员被派生类继承后,在派生类中的访问声明主要由派生类定义时的继承方式来决定。但是,在C+程序设计过程中,我们希望基类某些成员的访问声明以私有或保护继承方式时,在派生类中的访问声明不变,保持在基类中的访问声明,这可通过调整访问声明来实现。格式如下: 基类类名:基类保护段或公有段数据成员 基类类名:基类保护段或公有段成员函数名,6.2 派生类的概念,例如: class A int a; public: int b, c; int af( ); ; class D: private A int d; public: A: c; /调整对A: c的访问声明 int e; int df( ); ;,6.2 派生类的概念,类D从基类A私有派生,类A的所有公有段和保护段的成员都为类D的私有段,使用访问声明 A: c 可以将类B的公有段成员c在私有派生类D中显式声明为公有的,D的派生类可以访问它。由此可见,调整访问声明是私有派生的一种补充。对访问声明的调整要注意以下几点:,6.2 派生类的概念,1调整时不能说明任何类型。 class A int a; public: int b, c; int af( ); ; class D: private A int d; public: int A: c; /错误 ;,6.2 派生类的概念,2. 对访问声明的调整仅用于派生类中恢复名字的访问权限, 不允许在派生类中降低或提升基类成员的可访问性。即:基类中的公有段成员或保护段成员在派生类中仅能说明为相对应的公有段成员或保护段成员;基类的私有成员不能用于访问声明的调整。 从类的封装性的角度来看,这一限制是可以理解的。因为如果派生类能将其基类的私有成员用访问声明提高其访问权限而成为公有段的成员,这就破坏了类的封装性。,6.2 派生类的概念,class A int a; protected: int b; public: int c; ; class D:private A protected: A: :b; A: :c /错误,不能降低基类成员的可访问性 public: A: :c; A: :a; /私有成员不能用于访问声明的调整 ;,6.2 派生类的概念,3对重载函数名的访问声明将调整基类中具有该名的所有函数的访问权限。由于调整访问声明仅仅是恢复名字的访问,对于重载函数名,它的访问声明将使所有同名的重载函数的访问权限都得到调整。 class A public: f( ); f (int); ; class B: private A public: A: :f; /使A: :f( )和A: :f (int)在B中都是公有的 ;,6.2 派生类的概念,其中,A: :f( )表示访问声明仅仅调整名字,该函数名不带任何参数和类型;同时,它使得A: :f( )和A: :f (int)在类B中都处于公有段。 class A private: f (int); public: f( ); ; class B: private A public: A: :f; /错误,访问声明具有二义性,不能调整其访问 ;,6.2 派生类的概念,同时也意味着,如果派生类和基类有同名的成员,则不可调整基类成员的访问。 class A public: void f( ); ; class B: private A public: void f( ); A: :f; /错误,f的二次声明,不能调整访问 ;,6.2 派生类的概念,6.2.4 类层次中的访问规则 1在派生类中对基类成员的覆盖 C+允许派生类重新定义基类的成员。如果派生类定义了与基类同名的成员,称派生类的成员覆盖了基类的同名成员。如果要在派生类中使用基类的同名成员,可以显式地使用如下格式: 类名: :成员 来调用基类的成员。,6.2 派生类的概念,引用被覆盖的基类同名成员时,应使用类名限定符加以限定。当派生类与基类有同名成员时,引用不同的派生类对象中的这些成员时,还需指明对象名。 应当注意的是,不管是公有派生还是私有派生都不影响派生类对基类的静态成员的访问,但访问静态成员时,必须用“类名: :成员”显式地说明。例如:,6.2 派生类的概念,class A public: static void sa( ); /静态成员 void fa( ); ; class B: private A ; /全私有派生 class C: public B void fc( ) A: :sa( ); /正确 fa( ); /错误 sa( ); /错误 ;,6.2 派生类的概念,2. 基类和派生类的赋值兼容规则 对于基类对象和派生类对象,在公有派生条件下,C+语言允许派生类对象到基类对象的自动转换,这通常称为赋值兼容规则。 赋值兼容规则:当派生类从基类公有继承时,允许以下4种派生类对象到基类对象的自动转换。 规则1:可以用派生类对象为基类对象赋值; 规则2:可以用派生类对象初始化基类引用对象; 规则3:可以把指向派生类对象指针赋给基类对象的指针; 规则4:可以把派生类对象的地址赋给基类对象的指针。 每个派生类对象包含有一个基类对象,因此,上述规则不难理解。,6.2 派生类的概念,6.2.5 派生类的构造函数和析构函数 派生类继承了基类的所有成员,但基类中的构造函数和析构函数是不能被继承的。当我们想对派生类中新添加的成员进行初始化时,就必须按实际需要添加新的构造函数。如果要对从基类继承下来的成员进行初始化,则还要由基类的构造函数完成。当需要传递参数给基类构造函数时,必须为派生类建立一个构造函数,并由它来传递基类的构造函数所需的参数。 在C+中,派生类构造函数的声明为: 派生类构造函数(参数总表):基类名(参数表),对 象成员1(对象成员参数表), 对象成员n(对象成员参数表) ,6.2 派生类的概念,若基类使用缺省的构造函数或不带参数的构造函数,则在派生类中定义构造函数时可省略该基类名。如果基类定义了带有参数的构造函数,则派生类就应该定义构造函数,以保证对调用基类构造函数的对象进行初始化。派生类构造函数的调用顺序是: 1. 先祖先(基类),调用基类的构造函数。 2. 再客人(对象成员),调用成员对象的构造函数。 3. 后自己(派生类本身)。调用派生类的构造函数。,6.2 派生类的概念,【例6-5】 #include class Base public: Base( ) cout”Base created”endl; class Derived: public Base ,public: Derived( ) cout”Derived created”endl; ; void main( ) Derived a; ,6.2 派生类的概念,程序的输出结果为: Base created Derived created 从输出结果可以看出:先执行基类的构造函数,再执行派生类的构造函数。 另一方面,执行析构函数时,先执行派生类的析构函数再执行基类的析构函数。原因是显而易见的,对基类的破坏隐含了对派生类的破坏,所以派生类的析构函数必须先执行。,6.3 多继承,6.3.1 多继承的概念 前面介绍的派生类只有一个直接基类,这种继承称为单继承。C+允许一个派生类具有两个或两个以上基类,这种继承称为多继承。多继承在实际中也是非常有用的。常见的例子是软件系统的windows风格用户界面设计。软件系统的windows风格用户界面包括窗口、尺寸框、横向滚动条、纵向滚动条以及各种类型的按钮。我们可以先分别设计出窗口类、尺寸框类、横向滚动条类、纵向滚动条类以及各种按钮类,然后设计用户界面类。用户界面类把窗口类、尺寸框类、横向滚动条类、纵向滚动条类以及各种按钮类作为基类,通过多继承产生。,6.3 多继承,定义具有两个以上基类的派生类与定义单基类的派生类的形式相似,只要将继承的多个基类用逗号分隔即可。多继承方式派生类的定义格式为: class 派生类名:继承方式1 基类名1,继承方式2 基类 名2, /派生类成员的定义 ; 对每个基类可以用不同的继承方式,缺省继承方式为private。例如: class C: public A, B /类C公有继承类A,私有继承类B ;,6.3 多继承,在多重继承中,公有派生、私有派生和保护派生对于基类成员在派生类中的可访问性与单继承的规则相同。 在多重继承中,派生类的构造函数与单继承下派生类构造函数相似,它必须负责该派生类所有基类构造函数以及对象成员(如果有的话)构造函数的调用。同时,派生类的参数必须包含完成所有基类、对象成员以及派生类中新增数据成员初始化所需的参数。派生类构造函数执行顺序是: 1所有基类的构造函数;多个基类构造函数的执行顺 序取决于定义派生类时所指定的顺序,与派生类构造函数中所定义的成员初始化列表的参数顺序无关; 2对象成员的构造函数; 3派生类本身的构造函数。 多继承的析构函数与单继承的一样,析构函数的调用顺序正好与构造函数的相反。,6.3 多继承,6.3.2 虚基类 1虚基类的概念 派生类及其基类可用一有向无环图表示,其中的箭头表示“由派生而来”。考虑下面的例子:,6.3 多继承,class A public: int a; ; class B: public A ;,class C: public A ; class D: public B,public C public: void f (int i) a=i; ;,6.3 多继承,这里,类A两次成为类D的间接基类。这就意味着D类对象中有两个A对象: 由类B继承的A和类C继承的A。如下图所示: A A B C D 图6-1 在D: :f (int i)函数中,a有两个拷贝(B路径继承来的a和C路径继承来的a),因此语句a=0具有二义性,它是将B: :a置为0,还是将C: :a置为0?,6.3 多继承,如果希望间接基类A与其派生类的关系是如下图所示: A B C D 显然,目前多继承的方法不能描述这种情况,需要新的描述方法显式地指明间接基类与其派生类的这种单拷贝关系。C+提供了这种描述手段:它将A说明为B和C的虚基类;当在多条继承路径上有一个公共的基类(如本例的A),在这些路径中的某几条路径汇合处(如本例的D),这个公共基类就会产生多个实例,可以将这个公共基类说明为虚基类。它仅是简单地在继承的基类前加关键字virtual,例如把上例改为:,6.3 多继承,class A public: int a; ; class B: virtual public A ;,class C: virtual public A ; class D: public B,public C public: void f (int i) a=i; ;,6.3 多继承,这时D类对象中只有A的一个拷贝,因而函数D:f(int)中的语句a=i,没有二义性。 此例中,对于类D而言,类A是类C的虚基类,而是类B的真基类;但对于类C而言,类A仍是类C的真基类,虚基类只是个相对的概念。 如果把上例稍稍修改一下: class D: public C,public B 则对于类D而言,类A是类B的虚基类,而是类C的真基类。,6.3 多继承,一个派生类的对象的地址可以直接赋给虚基类的指针。例如: D obj1; A *ptr= 将产生编译错误。,6.3 多继承,2. 虚基类对象的初始化 加入虚基类后,它的初始化在语法上与一般多继承的初始化是一样的,但在调用构造函数的顺序上有点差别。 (1)先调用虚基类构造函数,然后调用非虚基类的构造函数; (2)当同一层有多个虚基类,按照它们的说明顺序调用它们的构造函数; (3)当虚基类是由非虚基类派生时,则先调用基类构造函数,再调用派生类构造函数。,6.3 多继承,class X: public Y,virtual public Z X one; 将产生如下调用次序: Z( ) Y( ) X( ) 这里Z是X的虚基类,故先调用Z的构造函数,再调用Y的构造函数,最后才调用派生类X自己的构造函数。,6.3 多继承,class base1 ; class base2 ; class level1:public base2,virtual public base1 ; class level2: public base2,virtual public base1 ; class toplevel: public level1,virtual public level2 ; toplevel view;,6.3 多继承,类等级关系如下图所示: base2 base1 base2 level1 level2 toplevel1 图6-2,6.3 多继承,当建立对象view时,将产生如下调用次序: level2( ): base1( ) base2( ) level2( ) level1( ): base2( ) level1( ) toplevel( ): toplevel( ),6.3 多继承,toplevel有两个基类:一个是虚基类level2,另一个是非虚基类level1。根据规定:应先执行level2的构造函数;level2也有两个基类,一个是虚基类base1,另一个是非虚基类base2。应先执行base1的构造函数,再执行base2的构造函数,最后执行level2的构造函数。toplevel然后执行level1的构造函数,而level1又有两个基类,base1是虚基类,无需再执行其构造函数,base2是非虚基类,因此要先执行base2的构造函数,然后执行level1的构造函数。最后执行toplevel的构造函数。上例中,对于toplevel的对象而言,base1是level1的虚基类。 注意:虚基类和非虚基类在使用上是不同的。,6.4 多态性与虚函数,在讲述多态性和虚函数之前,先介绍基类的指针。 6.4.1 指向基类对象的指针指向派生类对象 指向基类和指向派生类的指针变量是相关的。假设A是基类,B是从A公有派生出来的派生类,在C+中,任何被说明为指向A的指针也可以指向B。例如: A *p; /指向类型A的对象的指针 A objA; /类型A的对象 B objB; /类型B的对象 p= /p指向类型B的对象 利用指针p,可以访问从基类A继承的成员,但B自己定义的成员不能用p访问(除非用了显式类型转换把A转换成B)。,6.4 多态性与虚函数,任何声明为指向基类的指针,它可以指向它的公有派生类对象,这种指针只能直接访问那些从基类继承来的成员,不能直接访问公有派生类中的新添成员。如果派生类是以私有方式派生的,则基类的指针不能指向派生类对象。 如果我们想用基类的指针调用派生类的特定成员,则可以将基类指针显式转换为派生类指针来实现。 一个指向基类的指针可用来指向从基类公有派生的任何对象,这一事实是非常重要的,是C+实现运行时多态性的关键途径。,6.4 多态性与虚函数,6.4.2 多态性 多态性是面向对象程序设计的重要特征。它是指同样的消息被不同类型的对象接收时会产生不同行为。多态的实现可分为编译时的多态和运行时的多态:编译时的多态,指程序在编译过程中确定函数操作的具体对象,通过函数重载来实现,重载是多态性的一种简单形式;运行时的多态,是指程序在运行过程中才能确定函数操作的具体对象,通过虚函数实现(下节介绍)。,6.4 多态性与虚函数,1.编译时的多态性 编译时的多态是在程序编译过程中就确定同名操作的具体操作对象,即决定调用哪个同名函数。编译时的多态性可以通过重载函数来实现。有关一个类中函数重载的问题,我们已经在前面介绍过。下面是一个基类成员函数在派生类中重载(也就是实现编译时的多态性)的例子。 2.运行时的多态性 在实际情况中,许多对象以及对对象的操作往往不能再编译时就确定下来,它们需要在程序的运行过程中确定,这就是运行时的多态性。解决这个问题就是采用动态绑定,具体是用虚函数来实现。在下一节中具体地介绍虚函数。,6.4 多态性与虚函数,6.4.3 虚函数 虚函数是在基类中被冠以virtual的成员函数,它提供了一种接口界面,虚函数可以在一个或多个派生类中被重新定义,但要求在派生类中重新定义时,虚函数的函数原型,包括返回类型、函数名、参数个数、参数类型的顺序,必须完全相同。,6.4 多态性与虚函数,【例6-6】 #include class base protected: int x; public: base (int a) x=a; void who() cout”base”xendl; ;,class first_d: public base public: first_d (int a): base (a) void who( ) cout”first derivation”endlxendl; ;,6.4 多态性与虚函数,class second_d: public base public: second_d (int a): base (a) void who( ) coutsecond derivationendlxendl; ; 建立了一个类等级,两个派生类中都重新定义了基类的成员函数who( )。下面是这个类等级的使用:,6.4 多态性与虚函数,void main( ) base *p; base base_obj(1); first_d first_obj(2); second_d second_obj(3); p= ,6.4 多态性与虚函数,程序的输出结果为: base 1 base 2 base 3 指向基类的指针p,不管是指向基类的对象base_obj还是指向派生类的对象first_obj和second_obj,p-who( )调用的都是基类定义的who( )版本。这说明,通过指针引起的普通成员函数的调用,仅仅与指针(或引用)的类型有关,而与此刻正在指向什么对象无关。因为基类的指针仅能访问派生类中继承的基类成员,而不能访问派生类自己的成员。在这种情况下,必须显示地用 first_obj.who( );和second_obj( );,6.4 多态性与虚函数,这样才能调用类first_d和类second_d中定义的who( )的版本。其本质的原因在于普通成员函数的调用是在编译时静态区分的。 如果随着p所指向的对象的不同,p-who( )能调用不同类中who( )的版本,这样就可以用一个界面p- who( )访问多个实现版本:base中的who( )、first_d中的who( )和second_d中的who( ),这在编程时非常有用。实际上,这表达了一种动态的性质,函数调用p-who( )依赖于运行时p所指向的对象,虚函数提供的就是这种解释机制,如果在base中将成员函数who( )说明为虚函数,则修改上述程序为:,6.4 多态性与虚函数,【例6-7】 #include class base protected: int x; public: base (int a) x=a; virtual void who( ) /说明为 虚函数 cout”base”xendl; ;,class first_d: public base public: first_d (int a): base(a) void who( ) cout”first derivation”endlxendl; ;,6.4 多态性与虚函数,class second_d: public base public: second_d (int a): base(a) void who( ) coutsecond derivationendlxendl; ;,void main( ) base *p; base base_obj(1); first_d first_obj(2); second_d second_obj(3); p= ,6.4 多态性与虚函数,程序的输出结果为: base 1 first derivation 2 second derivation 3 这里,语句p-who( )出现了3次,由于p所指向的对象不同,每次出现都执行了who( )的不同实现版本。基类的虚函数who( )定义了一种接口,在派生类中为此接口定义了不同的实现版本,由于虚函数的解释机制,实现了“单界面、多实现版本”的思想。这种在运行时刻将函数界面与函数的不同实现版本进行匹配的过程,称为动态绑定,也称为运行时的多态性。,6.4 多态性与虚函数,用虚函数实现运行时多态性的关键之处是:必须用指向基类的指针访问虚函数。尽管可以像调用其他成员函数那样显示地用对象名来调用一个虚函数,但只有在同一个指向基类的指针访问虚函数时,运行时多态性才能实现。由于p指向的对象不同,因此调用了who( )的3个不同实现版本,这时,称为函数who( )具有虚特性。,6.4 多态性与虚函数,基类函数具有虚特性的条件是: 1在基类中,将该函数说明为虚(virtual)函数; 2定义基类的公有派生类; 3在基类的公有派生类中一模一样地重载该虚函数; 4定义指向基类的指针变量,它指向基类的公有派生 类的对象。,6.4 多态性与虚函数,注意:在一个派生类中重新定义基类的虚函数是函数重载的另一种形式。但它不同于一般的函数重载。当重载一般的函数时,函数的返回类型和参数表可能是不相同的,仅函数名要求相同。但重载一个虚函数时,要求函数名、返回类型、参数个数。参数类型和顺序是完全相同的。 如果函数原型不同,仅函数名相同,C+认为这是一般的函数重载,此时虚特性丢失。例如:,6.4 多态性与虚函数,class base public: virtual void vf1( ); virtual void vf2( ); virtual void vf3( ); void f( ); ;,class derived:public base public: void vf1( ); /具有虚特性 void vf2 (int); /一般函数重载, 参数不同,虚特性丢失 char vf3( ); /错误:仅返回类型 不同 void f( ); /一般的函数重载非虚 函数的重载 ;,6.4 多态性与虚函数,void g( ) derived d; base *bp- /调用base: :f( ) ,6.4 多态性与虚函数,在派生类derived中的函数vf1( )与基类base中的虚函数vf1( )具有完全相同的函数原型,故保持了虚特性;而函数vf2(int)与基类中的虚函数vf2( )参数不同,仅函数名相同,这只是一般函数的重载,其虚特性丢失;函数char vf3( )同基类的虚函数void vf3( )相比较,仅返回类型不同,目前的C+实现认为这是错误的;函数f( )仅仅是基类非虚函数f( )的重载。 由于vf1( )保持了虚特性,vf2( )丢失了虚特性,因此,在进行函数调用时,结果不一样。,6.4 多态性与虚函数,在函数g( )中,语句bp-vf1( ),调用的derived:vf1( )。在派生类derived中,函数vf1定义为虚函数,虚函数调用的解释依赖于调用它的对象类型,bp虽然是指向基类的指针,但此刻指向的是派生类对象d,因此,该语句等价d.vf1( );另外一条语句bp-f( ),调用的却是base: :f()。 函数f( )在基类和派生类中均已定义,且函数原型相同,但它是一个非虚函数,非虚函数调用的解释仅依赖于表示调用它的对象的指针或引用类型。bp被声明为指向基类的指针,非虚函数的调用仅仅依赖bp是指向基类的指针,而不在乎bp此刻是否正在指向派生类的对象,它调用的是base( );同样地,语句bp-vf2( )调用的是base: :vf2( )。,6.4 多态性与虚函数,虚函数的这种特性使派生类和虚函数成为许多C+程序设计的关键,因为基类可以使用虚函数提供一个界面,这是该类的所有公有派生类都具有的共同界面,但派生类可以定义自己的实现版本,而且虚函数调用的解释依赖于调用它的对象类型,指向基类对象的指针指向不同派生类的对象,就能访问虚函数的不同实现版本。,6.4 多态性与虚函数,虚函数必须是类的成员函数。不能将虚函数说明为全局(非成员的)函数,也不能说明为静态成员函数。不能将友员说明为虚函数,但虚函数可以是另一个类的友员。 一旦一个函数被说明为虚函数,不管经历了多少派生类层,都将保持其虚特性。 当一个派生类没有重新定义虚函数时,则使用其基类的虚函数版本。因此,在使用时要记住继承的层次性。,6.4 多态性与虚函数,6.4.4 纯虚函数及抽象类 基类往往表示一些抽象的概念。例如,shape是一个基类,它表示具有形状的东西,从shape可以派生出封闭图形和非封闭图形两个派生类,封闭图形又可以派生出椭圆形、多边形等。这个类等级的基类shape体现了一个抽象的概念,在shape中定义一个求面积的函数显然是无意义的,但可以将其说明为虚函数,提供各派生类一个公共的界面,并由各派生类提供求面积函数的各自版本。在这种情况下,基类的有些虚函数没有定义是很正常的,但是要求派生类必须重新定义这些虚函数,以使派生类有意义。为此,C+引入了纯虚函数的概念。,6.4 多态性与虚函数,纯虚函数是一个在基类中说明的虚函数,它在该基类中没有定义,要求任何派生类都必须定义自己的版本。为说明一纯虚函数,应使用如下格式: virtual type func_name (参数表)=0; 这里,type是函数的返回类型,func_name是函数名。,6.4 多态性与虚函数,如果一个类至少有一个纯虚函数,那么就称该类为抽象类。抽象类机制支持一般概念的表示。例如上面谈到的形状类shape是一般的概念,可以表达为抽象类,它有许多具体的变种,如圆形和方形才是具体可用的类。抽象类也可用于定义接口,由派生类提供各种实现。抽象类只能用作其他类的基类,抽象类不能建立对象。抽象类不能用作参数类型、函数返回类型或显式转换的类型。但可以声明抽象类的指针和引用。例如:,6.4 多态性与虚函数,class point ; class shape point center; public: point where( ) return center; ,void move (point p) center=p; draw( ); virtual void rotate (int)=0; /纯虚函数的定义 virtual void draw( )=0; /纯虚函数的定义 ;,6.4 多态性与虚函数,shape x; /错误:抽象类不能建立对象 shape *p; /可以声明抽象类的指针 shape f( ); /错误:抽象类不能作为返回类型 void g (shape); /错误:抽象类不能作为参数类型 shape /可以声明抽象类的引用,6.4 多态性与虚函数,从基类继承来的纯虚函数,在派生类中仍是纯虚函数。例如: class ab_circle: public shape int radius; public: void rotate (int) ;,6.4 多态性与虚函数,由于shape: :draw( )是一个纯虚函数,缺省的ab_circle: :draw( )也是一个纯虚函数,这时ab_circle仍为抽象类。要使ab_circle类为非抽象的,必须如下说明: class ab_circle: public shape int radius; public: void rotate (int) void draw( ) ;,6.4 多态性与虚函数,6.4.5 构造函数与虚析构函数 1. 构造函数 因为在派生类中构造函数是不能继承的,也没有重定义的必要。在构造函数中调用虚函数将破坏动态绑定逻辑。下面的例子说明了这样动态绑定逻辑。,6.4 多态性与虚函数,【例6-8】 #include class base protected: int x; public: base (int m) x=m+1; print( ); virtual void print( ) cout”The virtual function in base is called!”endl;,coutxendl; ; class derive: public base private: int y; public: derive (int m): base(m) y=m; print( ); ,6.4 多态性与虚函数,virtual void print( ) cout”The virtual function in derive is called!”endl; coutyendl; ; void main( ) derive obj(10); ,6.4 多态性与虚函数,程序的输出结果为: The virtual function in base is called! 11 The virtual function in derive is called! 10 程序从创建派生类的对象开始执行,在执行中,先要调用基类的构造函数。此时派生类的创建过程尚未完成,只能

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论