42
第 13 第 第第 第 13 第 第第 13.1 第第第 13.2 第第第第 13.3 第第第 13.4 第第第

第13章

Embed Size (px)

DESCRIPTION

- PowerPoint PPT Presentation

Citation preview

Page 1: 第13章

第 13章 继承

第 13 章 继承

• 13.1 派生类

• 13.2 多基派生

• 13.3 虚基类

• 13.4 虚函数

Page 2: 第13章

第 13章 继承

13.1 派生类■ 派生类 ( 也称子类 ) 是 c++ 提供继承的基础,也是对原有的类进行

扩充和利用的一种基本手段。一个类可以继承另一个类的属性。其中被继承的类叫做基类( Base class ) , 继承后的类叫做派生类( Derived clas )。

■ 13.1.1 派生类声明■ 1 . public 派生与 private 派生■ C++ 允许程序员用下面的格式实现继承:

Page 3: 第13章

第 13章 继承

■ class 标识符 2 :派生方式 标识符 1■ {■ private :■ 私有成员声明语句;■ public :■ 公有成员声明语句;■ } ;■ 这里,标识符 1 标识一个已存在的类,被称为基类;标识符 2 标识

的就是要定义的新类,被称为派生类。通过继承机制,在派生类中可以定义基类中没有的私有成员和公有成员。派生方式指 privat

e 派生或 public 派生,分别被称为私有派生和公有派生。■ 这两种派生方式的特点如下:

Page 4: 第13章

第 13章 继承

■ ( 1 )使用 public 派生,基类的所有成员在派生类中的访问权限不变。

■ ( 2 )使用 private 派生,基类的所有成员在派生类中的访问权限都是私有的,即基类中的 public 成员相当于派生类中的 private 成员,而基类中的 private 成员在派生类中不可见。

■ 在例 13.1 中我们将对前面定义过的 product 类进行 public 派生和 pr

ivate 派生。

Page 5: 第13章

第 13章 继承

■ 【例 13.1 】■ ① private 派生■ #include <iostream.h>■ #include <string.h>■ #include "ex121.cpp"■ class fuproduct : private product■ {■ char factory ; // 生产厂家■ public :■ fuproduct() ; // 派生类构造函数■ {■ ┆■ }

Page 6: 第13章

第 13章 继承

■ void pdput() ;■ } ;■ fuproduct pdput()∷■ {■ product pdput()∷ ;■ cout<<"factory : "<<factory<<endl ;■ }■ void main()■ {■ fuproduct a("xing1" , "70-1" , 9000 , "anhui") ; // 出错■ a.pdput() ;■ }

Page 7: 第13章

第 13章 继承

■ ② public 派生:■ #include <iostream.h>■ #include <string.h>■ #include "ex121.cpp"■ class fuproduct : public product■ {■ char factory ;■ public :■ fuproduct() ;■ {■ ┆■ }■ void pdput() ;■ } ;

Page 8: 第13章

第 13章 继承

■ void fuproduct pdput()∷■ {■ product pdput()∷ ;■ cout<<"factory : "<<factory<<endl ;■ }■ void main()■ {■ fuproduct a("xing1" , "70-1" , 9000 , "anhui") ;■ a.pdput() ;■ }■ 输出结果为:■ pname : xing1■ pnod : 70-1■ num : 9000■ factory : anhui

Page 9: 第13章

第 13章 继承

■ 由此可见两种派生方式的不同。当然在第二个程序的主函数中,加入语句:

■ a.product pdput()∷■ 也是正确的,因为它可以引用基类的公有成员。■ 在派生类和基类中可以声明同名的成员,例如在上例中 product 和

fuproduct 类中都声明了公有函数 pdput() ,则我们在主函数中执行语句:

■ a.pdput() ;■ 将导致 fuproduct pdput()∷ 成员函数被调用,因为对象 a 的类型为 f

uproduct ,编译器先在 fuproduct 类中查找 pdput 操作,在没有找到时,才到它的基类 product 类中进行查找。如果想调用基类中的成员函数,则可以采用“基类名∷基类成员函数名”进行成员名限定,例如在前面的程序中,如果想要调用基类 product 中的成员函数,可以用下面的语句来实现:

Page 10: 第13章

第 13章 继承

■ a.product pdput()∷ ;■ 这里 fuproduct pdput()∷ 成员函数的实现不能被简单地写为:■ void fuproduct pdput()∷■ { ■ pdput() ;■ }■ 这将导致对 fuproduct pdput()∷ 函数的调用,但这是一个无限递归

调用。■ 2 . protected 成员与 protected 派生■ 如果想要做到基类成员只能被有派生血缘关系的成员访问,而不

被无血缘关系的对象成员访问,用前两种派生方式是无法办到的。因为基类成员中的私有成员是别的类(包括派生类)成员不能访问的,而基类中的公开成员在 public 派生时,不仅可以由派生类对象成员访问,也可以由外部函数(无血缘)访问。在 private 派生时,基类中的公开成员虽然

Page 11: 第13章

第 13章 继承

■ 只允许派生类对象中的成员访问,不允许外部函数访问,可是再派生出下一级时,基类的所有成员将都被私有化,其他类成员也不可再访问。实现只允许有派生血缘关系的对象成员访问的方法,是在基类中使用具有另一种访问属性的成员── protected 成员。

■ protected 成员是一种具有血缘关系内外有别的成员。它对派生对象而言,是公开成员,可以访问;对血缘外部而言,与私有成员一样被隐藏。

■ 在类的派生中,除了允许使用 public 和 private 两种派生方式外,C++ 还允许使用 protected 派生方式。使用 protected 派生,基类的保护成员和公有成员在派生类中的访问权限都降一级,即基类中的 public 和 protected 成员分别相当于派生类中的 protected 成员和private 成员。

Page 12: 第13章

第 13章 继承■ 13.1.2 友元与继承■ 前面我们讲了三种派生方式的特点,可以看出不管是按哪一种方

式派生,基类的私有成员在派生类中都是不可见的。如果一个派生类要访问基类中声明的私有成员,可以将这个派生类声明为基类的友元。如:

■ class Mount■ {■ friend class Mountain ;■ private :■ int i , j ;■ public :■ void show() ;■ {■ ┆■ }

Page 13: 第13章

第 13章 继承

■ } ;■ class Mountain : public Mount■ {■ public :■ void putnum() ;■ } ;■ void Mountain putnum()∷■ {■ i = 9 ;■ j = 8 ;■ }■ 成员函数 Mountain putnum()∷ 访问了基类中声明的私有成员,这

是正确的,因为类 Mountain 是类 Mount 的友元类。但是,由于友元函数没有 this 指针,所以不能直接引用基类中声明的非静态成员。

Page 14: 第13章

第 13章 继承

■ 13.1.3 派生类的构造函数与析构函数■ 构造函数与析构函数不能继承,所以,一个派生类的构造函数必

须通过调用基类的某个构造函数来初始化基类子对象。如在声明派生类 fuproduct 对象时,系统先通过派生类的

■ fuproduct a("xing1" , "70-1" , 9000 , "anhui") ;■ 构造函数调用基类的构造函数,对基类的成员进行初始化,然后

再对派生类的新增成员进行初始化。声明格式如下:■ x x(parX1∷ , parX2 ,…, parx1 , parx2 ,… ) : X(parX1 ,

parX2 ,… ) ■ 其中, X 为 x 的基类名, x 为派生类名, parX1 , parX2 ,…为

基类构造函数参数, parx1 , parx2 ,…为派生类构造函数参数。

Page 15: 第13章

第 13章 继承

■ 当一个派生类对象撤消时,先撤消派生类自身,再撤消其成员,最后撤消基类。

■ 这里要顺便提及的一点是,任何类的构造函数都不可以是私有的,否则它将既不可被外部访问,又不可被派生类成员访问,使所在类因无法初始化而既不能生成对象,也不能派生新的类。但是,构造函数可以是保护 (protected) 的。这时它虽不可被外部访问,但都可以被派生类成员访问。所有的构造函数都是保护属性的类,虽不能由其生成对象,但可以由其派生出新的类,这种能用以派生新类,却不能创建自己的对象的类称为抽象类

Page 16: 第13章

第 13章 继承

13.2 多基派生■ 前面所讲的继承结构被称为单一继承或单继承,在这种继承结构

中,派生类只有一个直接基类。当一个派生类具有多个基类时,将继承这些基类的部分代码,这种派生方式称为多基继承或多重继承。多基继承被视为对单继承的扩展,派生类结构形式为:

■ class 派生类名:派生方式 1 基类名 1 ,派生方式 2 基类名 2 ,…,派生方式 n 基类名 n

■ {■ 数据成员和成员函数声明;■ } ;

Page 17: 第13章

第 13章 继承

■ 例 13.3■ class prime1■ {■ public :■ void f() ;■ } ;■ class prime2■ {■ protected :■ int i ;■ public :■ void g() ;■ } ;

Page 18: 第13章

第 13章 继承

■ class second : private prime1 , public prime2■ {■ public :■ void h()■ {■ f() ; // 出现错误■ g() ;■ a = 0 ;■ }■ } ;■

Page 19: 第13章

第 13章 继承

■ void main()■ {■ second a ;■ a.f() ;■ g() ;■ h() ;■ }■ 多基派生的构造函数的一般形式为:■ D D(∷ 参数表 1 ,参数表 2 ,…,参数表 n) ■ : B1( 参数表 1) , B2( 参数表 2) ,…, Bn( 参数表 n) ■ { ■ … // 函数体 ■ }

Page 20: 第13章

第 13章 继承

■ 在此构造函数中, B1 , B2 ,… Bn 是 D 的多重基类的构造函数名,有时可以将这些基类的构造函数名省略,只要圆括号中的参数与派生类构造函数参数表中的顺序一致即可。

Page 21: 第13章

第 13章 继承

13.3 虚基类■ 前面我们讲了多基派生,一个派生类可以有多个基类,但在多基

派生时有可能出现这样的情况:■ 如图 13-1 所示,类 classB 和类 classC 都是类 classA 的派生类,而

类 classB 与类 classC 又派生出类 classD 。这时出现了一个公共基类 classA ,则在类 classD 中会出现公共基类 classA 的多个拷贝。如果想得到公共基类的一个拷贝,虚基类可以解决这个问题。定义格式如下:

■ class 派生类名: virtual 派生方式 基类名

Page 22: 第13章

第 13章 继承

■ 【例 13.4 】■ class A■ {■ protected :■ int a ;■ public :■ void f() ;■ } ;■ class B : virtual public A■ {■ protected :■ int b ;■ } ;

Page 23: 第13章

第 13章 继承

■ class C : virtual private A■ {■ protected :■ int c ;■ } ;■ class D : virtual public B , public C■ {■ int d ;■ public :■ void g() ;■ } ;■ 由于在类 D 的对象中只存在一个类 A 的子对象,所以下面的语句

都是正确的:

Page 24: 第13章

第 13章 继承

■ 由于在类 D 的对象中只存在一个类 A 的子对象,所以下面的语句都是正确的:

■ D d ;■ int x = d.a ;■ int y = d.B a∷ ;■ int z = d.C a∷ ;

Page 25: 第13章

第 13章 继承

13.4 虚函数■ 虚函数是重载的另一种表现形式。这是一种动态的重载方式,它

提供了一种更为灵活的多态性机制。虚函数允许函数调用与函数体之间的联系在运行时才建立,也就是运行才决定如何动作,既动态联编。

■ 13.4.1 方法的多态性与虚函数■ 多态性是人类处理问题过程中一种普遍的规则。处理问题要求

“原则性”与“灵活性”。例如,“ study” 的原则是:■ 情况不明时:调查;■ 问题不懂时:学习;■ 得不出结论时:研究。■ 此外,还需要根据“ study” 的进展,灵活地采用不同的对

Page 26: 第13章

第 13章 继承

■ 在程序中,也需要多态性。这种多态性可以简单地概括为“一个接口,多种方法”。前面已介绍的重载就是一种简单的多态性:一个函数名 ( 接口 ) 对应了几种不同的函数原型 ( 方法的特征 ) 。多态性通过聚束 (binding ,也称匹配或联编 ) 实现。聚束是把函数调用与适当的函数码相对应的活动。前面介绍的函数重载中,函数名相同,但函数原型不同,对具体的调用语句,编译器在编译时就能根据函数原型提供的特征 ( 参数类型、个数 ) ,确定与哪一种函数体相匹配,这种聚束方式称为静态聚束,相应的多态性称为静态多态性。在前面的讨论中,我们还看到了另外一种多态性,在一个层次类的不同类中,可以用同一个名字命名语义相似的方法函数,而在各自的类中给予不同的定义,以形成不同的版本。如对一个点、一个圆、多个不同大小与形状的图形,计算面积的方法是不相同的。那么,如何能在一个统一的接口 ( 相同的原型 )

下,根据所创建的具体对象实现聚束呢 ?

Page 27: 第13章

第 13章 继承

■ 这种聚束方式不可能在编译时静态地进行,而必须在程序运行过程中进行动态聚束。在 C++ 中,动态多态性通过虚函数机制来实现。

■ 虚函数是用关键字 virtual修饰的某基类中的 protected 或 public 成员函数。它可以在派生类中重定义,以形成不同的版本。只有在程序执行过程中,依据指针具体指向哪个类对象,或依据引用哪个类对象,才能确定激活哪一个版本,实现动态聚束

Page 28: 第13章

第 13章 继承

■ 【例 13.6 】■ #include <iostream.h> class Point■ { private : float x , y ; public : Point(){

} Point(float i , float j)■ { ■ x = i ; y = j ;■ }■ virtual float area() // 声明为虚函数■ {■ return 0.0 ;■ }■ } ;

Page 29: 第13章

第 13章 继承

■ const f1oat Pi = 3.141593 ; class Circle : public Point ■ { private■ float radius ;■ public :■ Circle(float r)■ { radius = r ; } float area() // 虚函

数的再定义 {■ return Pi radius radius ;■ }■ } ;

Page 30: 第13章

第 13章 继承■ void main()■ {■ Point pp ; // 指向基类的指针■ Circle c(5.4321) ;■ pp= &c ;■ cout<<pp->area()<<endl ; // 调用虚函数■ }■ 输出结果为:■ 92.70121■ 上面两个程序中定义的 pp 都是既指向基类 Point 又指向派生类 Cir

cle 的指针,但运行结果却不相同。在例 13.5 中,没有使用虚函数,编译器对重载函数 area() 进行静态聚束,按照指针 pp 声明的类型,决定调用 Point 类中的方法 area() 。例 13.6 中,将 area() 声明为虚函数,编译器对其进行动态聚束,按照实际对象 c 调用了 Circle 中的方法 area() 。使 Point 类中的 area() 与 Circle 类中的 area() 有一个统一的接口。

Page 31: 第13章

第 13章 继承

■ 虚函数声明与重定义的一般规则是:■ ( 1 )在基类中,用关键字 virtual 可以将其 public 或 protected 部

分的成员函数声明为虚函数,如: virtual float area(){…}■ ( 2 )一个虚函数是属于它所在的类层次结构的,而不是只属于

某一个类,只不过它在该类层次结构中的不同类中具有不同的形态。

■ ( 3 )如果派生类中没有对基类中说明的虚函数进行重定义,则它继承基类中的虚函数。

■ ( 4 )由于有些编译器不能正确处理虚函数,因此要避免把构造函数声明为虚函数。

■ ( 5 )虚函数只能是类的成员函数,不能把虚函数声明为静态的或全局的;也不能把友元声明为虚函数。但是虚函数可以是另一个类的友元函数。

Page 32: 第13章

第 13章 继承

■ 13.4.2 虚函数的访问■ 1 .用基指针访问与用对象名访问■ 把一个函数声明为虚函数,保证了在这个函数被指向基类的指针

(或引用)调用时,■ C++ 系统对其进行动态聚束,向实际的对象传递消息,如例 13.6

中的语句:■ Point pp ;■ …■ pp = &c ;■ pp->area() ;■ 但是,通过一个对象名访问虚函数时, C++ 系统将使用静态聚束。

Page 33: 第13章

第 13章 继承

■ 2 .用 this 指针访问■ 由类的成员函数访问该类层次中的虚函数,要使用 this 指针。■ 【例 13.9 】■ #include <iostream.h>■ c1ass A■ {■ public :■ virtual void v1(){cout<<" v1 is called in A.\n" ; a1() ; }■ void a1(){cout<<" al is called in A.\n" ; v2() ; }■ virtual void v2(){cout<<" v2 is called in A.\n" ; }■ } ;■ class B : public A

Page 34: 第13章

第 13章 继承

■ {■ public :■ void b1(){cout<<" b1 is called in B.\n" ; v2() ; }■ virtual void v2(){cout<<" v2 is called in B.\n" ; }■ } ;■ void main()■ {■ A a ;■ a.v1() ;■ cout<< " Ok!\n" ;■ B b ;■ b.v1() ;■ }

Page 35: 第13章

第 13章 继承

■ 输出结果为:■ v1 is called in A. (a.v1() 调用 )■ a1 is called in A .■ v2 is called it A .■ Ok !■ v1 is called in A . (b.v1() 调用 )■ al is Called in A . v2 is Called in B .■ 在本例中, v1() 是通过静态聚束实现的,因为它是由对象名引用

的; v2() 是由 a1() 中的 this 指针引用的,即相当于如下形式的 a1

() :■ A a1() { ∷■ cout<<" al is called in A.\n" ; this->v2() ; }

Page 36: 第13章

第 13章 继承

■ 这里, this 是一个指向基类 A 的指针。由于 v2 () 是一个虚函数,因此 C++ 系统要通过动态聚束实现,具体取决于所引用的对象。在本例中,是对象 a ,则对 A v2()∷ 聚束;是对象 b ,则对 B v2∷()聚束。

■ 3 .用构造函数/析构函数访问■ 构造函数与析构函数是特殊的方法函数。它们访问虚函数时, C+

+ 采用静态聚束。

Page 37: 第13章

第 13章 继承

■ 13.4.3 纯虚函数与抽象类■ 当在基类中不能为虚函数提供有实际意义的定义时,可以将这个

虚函数声明为纯虚函数。声明纯虚函数的形式为:■ virtual 类型 函数名(参数列表) = 0 ;■ 如例 13.6 中 point 类中的声明语句改为■ virtual float area() = 0 ;■ 时, Point 类中的函数 area() 就称为纯虚函数。纯虚函数不能被直

接调用,仅起提供一个与派生类相一致的接口作用,用作派生类中的具有相同名字的函数存放处。包含纯虚函数的类与在前面介绍过的把所有构造函数都声明为 protected 成员的类一样,也称为抽象类。一个抽象类只能作为基类来派生新类,不能生成抽象类的对象。因此,当 Point 被声明为抽象类后,不能生成 point 的对象了。即语句

Page 38: 第13章

第 13章 继承

■ point p ;■ 是错误的。但可以声明一个指向抽象类的指针,如:■ point pp ;■ 纯虚函数不可以被继承。当基类是抽象类时,在派生类中必须给

出基类中纯虚函数的定义,或在该类中再声明其为纯虚函数。只有在派生类中给出了基类中所有纯虚函数的实现时,该派生类就不再成为抽象类。抽象类仅充当一个统一的接口作用。

Page 39: 第13章

第 13章 继承

■ 13.4.4 多基派生中虚函数的二义性■ 前面介绍了多基派生中的多条路径具有公共基类时,在这多条路径的汇合处就会因对公共基类产生多个拷贝而产生同名函数调用的二义性。消除这个二义性的办法是把公共基类定义为虚基类,使由它派生的多条路径的汇聚处只产生一个拷贝。

■ 多基派生中,虚函数在上述情况下也会出现■ 类似的二义性

Page 40: 第13章

第 13章 继承

■ 【例 13.12 】■ 分析图 13-4 所示的类层次结构。花括号中为它们各自重定义的虚

函数。 它的定义如下: /*file's name is " ex133.hpp" */

■ #include <iostream.h>■ c1ass base■ {■ public :■ virtual void a(){cout<<" a() in base\n" ; }■ virtual void b(){cout<<" b() in base\n" ; }■ virtual void c(){cout<<" c() in base\n" ; }■ virtual void d(){cout<<" d() in base\n" ; }■ virtual void e(){cout<<" e() in base\n" ; }■ virtual void f(){cout<<" f() in base\n" }■ } ;

Page 41: 第13章

第 13章 继承

■ class A : public base■ {■ public :■ virtual void a(){cout<<" a() in A\n" ; }■ virtual void b(){cout<<" b() in A\n" ; }■ virtual void f(){cout<<" f() in A\n" ; }■ } ;■ class B : public base■ {■ public :■ virtual void a(){cout<<" a() in B\n" ; } ■ virtual void b(){cout<<" b() in B\n" ; }■ virtual void c(){cout<<" c() in B\n" ; }■ } ;

Page 42: 第13章

第 13章 继承

■ class C : public A , public B■ {■ public : virtual void a(){cout<<" a() in C\n" ; }

virtual void d(){cout<<" d() in C\n" ; } ■ } ;■ 对这种类层次结构,不能使用指向公共基类 base 的指针来访问多条路径汇聚处的派生类 C 的对象。即语句: C cc ;

■ base pbase = &cc ; // 将 base 类指针指向 C 类对象■ 是错误的。只可以使用指针指向类层次结构中无分支的某条路径

上的某个类,如用指向 A 或指向 B 的指针来访问派生类 C 的对象。