53
1 继继inheritance 继继继继继继继继继继继继继继继继继继继继继 继继继继 继继继继继继继继继继继继继继继继继继继继继继继 继继继继继 ,,。 继继继继继 继继 继继继继继 继继继继继继继继继继继继继继继继继继继 ,。。 继继继继继继继继继继 继继继第第第 第第第 9.1 第第第第第第 9.2 第第 9.3 第第第第 第第第第第

第九章 继承性

  • Upload
    raiden

  • View
    37

  • Download
    0

Embed Size (px)

DESCRIPTION

第九章 继承性. 继承( inheritance ) 机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能。 这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构。 体现了由简单到复杂的认识过程 。. 9.1 基类和派生类 9.2 单一继承 9.3 多重继承 小结与作业. 9.1 继承性 9.1.1 继承和派生的概念. 层次概念 是计算机的重要概念。通过 继承 ( inheritance )的机制可对类( class )分层,提供类型 / 子类型的关系。 - PowerPoint PPT Presentation

Citation preview

1

继承( inheritance )机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能。这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构。体现了由简单到复杂的认识过程。

第九章 继承性

9.1 基类和派生类

9.2 单一继承9.3 多重继承

小结与作业

2

9.1 继承性9.1.1 继承和派生的概念

  层次概念是计算机的重要概念。通过继承( inheritance )的机制可对类( class )分层,提供类型 / 子类型的关系。

C++ 通过类派生( class derivation )的机制来支持继承。被继承的类称为基类( base class )或超类( superclass ),新产生的类为派生类( derived class )或子类( subclass )。

基类和派生类的集合称作类继承层次结构( hierarchy )。

如果基类和派生类共享相同的公有接口,则派生类被称作基类的子类型( subtype )。

派生反映了事物之间的联系,事物的共性与个性之间的关系。 派生与独立设计若干相关的类,前者工作量少,重复的部分可以从基类继承来,不需要单独编程。

3

单一继承: 若某派生类只有一个基类,则称该派生类为单一继承的派生类。如由“汽车”类派生出的“小汽车”类、“大卡车”类均是单一继承。多重继承: 若某派生类有 2 个或更多个基类,则称该派生类为多重继承的派生类。“儿子”有“父亲”、“母亲” 2 个基类,他既继承了“父亲”的特性,又继承了“母亲”的特性,属于多重继承。 为了便于理解,我们用家族的名词来描述基类与派生类,一个基类得出有多个派生类,称为“一父多子”,属单一继承;一个派生类由多个基类派生,称为“一子多父”,属多重继承。 对于多层派生来说,一个派生类的基类为直接基类,而基类的基类为间接基类。

9.1.2 单一继承和多重继承

4

基 类1

基 类2

…… 基 类n

派生类 1 派生类 2

基类

派生类 1 派生类 2

( a )多重继承(一子多父)

( b )单一继承(一父多子)

一个基类可以直接派生出多个派生类

派生类可以由多个基类共同派生出来,称多重继承。

9.1.2 单一继承和多重继承

单一继承和多重继承示意图:

5

9.1.3 派生类的定义格式

由基类派生出派生类的定义的一般形式为:class 派生类名 : 访问限定符 基类名 1, 访问限定符 基类名 2,……{ private: 成员表 1 ; // 派生类增加或替代的私有成员 public: 成员表 2 ; // 派生类增加或替代的公有成员 protected: 成员表 3 ; // 派生类增加或替代的保护成员} ; // 分号不可少

其中基类 1 ,基类 2 ,……是已声明的类。只有一个基类时,是单一继承,上述给出的形式是多重继承。 在派生类定义的类体中给出的成员称为派生类成员,它们可以是新增加成员,它们给派生类添加了不同于基类的新的属性和功能,当派生类成员名与基类成员名相同时,派生类成员将取代基类成员,成为更新成员。

6

编制派生类时可分四步

吸收基类的成员

改造基类成员

发展新成员

重写构造函数与析构函数

9.1.3 派生类的定义格式

不论是数据成员,还是函数成员,除构造函数与析构函数外全盘接收

声明一个和某基类成员同名的新成员 , 派生类中的新成员就屏蔽了基类同名成员称为同名覆盖( override )

派生类新成员必须与基类成员不同名,它的加入保证派生类在功能上有所发展。

7

9.1.3 派生类的定义格式

上面的步骤就是继承与派生编程的规范化步骤。第二步中,新成员如是成员函数,参数表也必须一样,否则是重载。第三步中,独有的新成员才是继承与派生的核心特征。第四步是重写构造函数与析构函数,派生类不继承这两种函数。不管原来的函数是否可用,必须一律重写可免出错。

9.1.4 基类成员在派生类中的访问权限 访问控制,亦称为继承方式,是对基类成员进一步的限制。访问控制也是三种:公有( public )方式,亦称公有继承保护( protected )方式,亦称保护继承私有( private )方式, 亦称私有继承。

8

9.1.4 基类成员在派生类中的访问权限

访问限定符有两方面含义:派生类成员(新增成员)函数对基类(继承来的)成员的访问(调用和操作),和从派生类对象之外对派生类对象中的基类成员的访问。

公有派生是绝对主流。

不可直接访问 protected protected

不可直接访问 privateprivate

不可直接访问 protectedpublic publicprivateprotected

privateprotectedpublic 基类权限

继承方式

9

【例 9.1 】分析程序,指出访问权限的错误。

class Base{public:int b1;protected:int b2;private:int b3;};class D1:public Base{public:void fun( ){b1=11;b2=12;b3=13;}};class D2:private Base{public:void fun( ){b1=21;b2=22;b3=23;}};class D3:protected Base{public:void fun( ){b1=31;b2=32;b3=33;}};class D11:public D1{public:void fun( ){b1=111;b2=112;b3=113;}};class D22:public D2{public:void fun( ){b1=221;b2=222;b3=223;}};class D33:public D3{public:void fun( ){b1=331;b2=332;b3=333;}};void main( ){ D11 d1;d1.b1=1;d1.b2=2;d1.b3=3; D22 d2;d2.b1=4;d2.b2=5;d2.b3=6; D33 d3;d3.b1=7;d3.b2=8;d3.b3=9;}上述程序中有 16 处错误 ! 注意,类的公有成员函数可调用类中的私有

成员,但不可调用基类中的私有成员。使用对象只能调用本类中的公有成员。

10

9.2 单一继承9.2.1 单一继承程序举例

【例 9.2 】公有继承例子#include<iostream.h>class A // 类 A 有哪些成员?{public:void fun1( );protected:int a1;private:int a2;};class B:public A // 类 B 有哪些成员?{public:void fun2( );protected:int b1;private:int b2;};class C:public B // 类 C 有哪些成员?{public:void fun3( );};.........void main( ){ C c;...........}注意程序中对象 c 有哪些可访问成员?

可访问 fun1() 和 fun2()

11

【例 9.3 】私有单一继承程序举例。#include<iostream.h>

class A{public:void fa(int i){cout<<i<<endl;}

void ga( ){cout<<"A.\n";} };

class B:A{public:void hb( ){ cout<<"B.\n";}

A::fa; };

//B 继承 A ,用 class 说明,不指明权限的一定是私有void main( )

{ B b;b.fa(5);b.ga();b.hb( ); }// 红色表示的,有错误。私有继承将基类的公有、保护成员变为私有,成员函数

可访问这些成员,但对象不可访问;间接私有继承(二次私有继承)后,原来的成员都不可访问。

12

【例 9.4 】保护单一继承程序举例。#include<iostream.h>#include<string.h>class A{public:A(const char*name1){strcpy(name,name1);} protected:char name[80]; };class B:protected A{public:B(const char*nm):A(nm){ } // 派生类的构造函数 void Print( ){cout<<"name:"<<name<<endl;}};void main( ){B b("wang");b.Print( );} 保护继承将基类的公有、保护变为保护,成员函数可访问,

但对象不可访问;间接保护继承(二次保护继承)后,仍是这样。

输出结果:

name:wang

13

派生类的构造函数不能从基类中继承,需重新定义,形式为:派生类名 :: 构造函数名(参数表) : 成员初始化表{ ……// 函数体中主要对派生类新增成员进行初始化}成员初始化列表包括:基类构造函数、子对象的构造函数、常成员初始化、其他初始化项等,执行顺序为:基类构造函数、子对象的构造函数、其他初始化项、派生类构造函数体。

在构造函数的声明中,冒号及冒号以后部分必须略去。 析构函数也不能继承,派生类的析构函数将调用基类的析构函数,执行顺序与构造函数的执行顺序相反:派生类析构函数、子对象的析构函数、基类析构函数。

9.2.2 单一继承派生类的构造函数与析构函数

14

9.2.2 单一继承派生类的构造函数与析构函数

* 在派生类构造函数中,只要基类不是使用缺省构造函数都要显式给出基类名和参数表。如果基类没有定义构造函数,则派生类也可以不定义,全部采用系统给定的缺省构造函数。如果基类定义了带有形参表的构造函数时,派生类就应当定义构造函数。

15

9.2.2 单一继承派生类的构造函数与析构函数

析构函数的功能是作善后工作的。 只要在函数体内把派生类新增一般成员处理好就可以了,而对新增的成员对象和基类的善后工作,系统会自己调用成员对象和基类的析构函数来完成。 析构函数各部分执行次序与构造函数相反,首先对派生类新增一般成员析构,然后对新增对象成员析构,最后对基类成员析构。

16

【例 9.5 】分析程序,掌握派生类构造函数及析构函数的用法。

#include<iostream.h>class A{ public:A( ){a=0;cout<<" 隐含构造函数。 A\n";} A(int i){a=i;cout<<" 构造函数。 A\n";} ~A(){cout<<" 析构函数。 A\n";} void Print( ){cout<<a<<',';} int Geta( ){return a;} private:int a;};class B:public A{ public:B(){b=0;cout<<" 隐含构造函数。 B\n";} B(int i,int j,int k); ~B(){cout<<" 析构函数。 B\n";} void Print(){A::Print();cout<<b<<','<<aa.Geta()<<endl;} private:int b; A aa;};B::B(int i,int j,int k):A(i),aa(j),b(k){cout<<" 构造函数。 B\n";}void main(){ B bb[2];bb[0]=B(8,3,9);bb[1]=B(17,-18,19); for(int i=0;i<2;i++)bb[i].Print();}

17

输出结果:隐含构造函数。 A

隐含构造函数。 A

隐含构造函数。 B

生成并初始化

对象 bb[0]

隐含构造函数。 A

隐含构造函数。 A

隐含构造函数。 B

生成并初始化

对象 bb[1]

构造函数。 A

构造函数。 A

构造函数。 B

生成并初始化

临时对象 B(8,3,9)

析构函数。 B

析构函数。 A

析构函数。 A

释放

临时对象 B(8,3,9)

构造函数。 A

构造函数。 A

构造函数。 B

生成并初始化

临时对象 B(17,-18,19)

析构函数。 B

析构函数。 A

析构函数。 A

释放

临时对象 B(17,-18,19)

8,9,3

17,19,-18执行 for循环

析构函数。 B

析构函数。 A

析构函数。 A

释放

对象 bb[1]

析构函数。 B

析构函数。 A

析构函数。 A

释放

对象 bb[0]

18

【例 9.6 】运行程序,观察结果,掌握派生类构造函数的用法。

#include<iostream.h>class A{ public:A( ){a=0;cout<<" 隐含构造函数。 A\n";} A(int i){a=i;cout<<" 构造函数。 A\n";} ~A(){cout<<" 析构函数。 A\n";} void Print( ){cout<<a<<',';} private:int a;};class B:public A{ public:B(){b1=b2=0;cout<<" 隐含构造函数。 B\n";} B(int i){b1=0;b2=i;cout<<" 单参数构造函数。 B\n";} B(int i,int j,int k):A(i),b1(j),b2(k){cout<<" 三参数构造函数。 B\n";} ~B(){cout<<" 析构函数。 B\n";} void Print(){A::Print();cout<<b1<<','<<b2<<endl;} private:int b1,b2; };void main(){ B b1,b2(15),b3(11,12,13); b1.Print(); b2.Print(); b3.Print();}

19

输出结果:隐含构造函数。 A

隐含构造函数。 B生成并初始化

对象 b1

隐含构造函数。 A

单参数构造函数。 B生成并初始化

对象 b2

构造函数。 A

三参数构造函数。 B生成并初始化

对象 b3

0,0,0

0,0,15

11,12,13

分别执行b1.Print(); b2.Print(); b3.Print();

析构函数。 B

析构函数。 A释放对象 b3

析构函数。 B

析构函数。 A释放对象 b2

析构函数。 B

析构函数。 A释放对象 b1

20

【例 9.7 】运行程序,观察结果,掌握派生类构造函数的特点。

#include<iostream.h>class B{ public:B(){b1=b2=0; cout<<" 隐含构造函数。 B\n";} B(int i,int j){b1=i;b2=j;cout<<" 构造函数。 B\n";} ~B(){cout<<" 析构函数。 B\n";} void Print( ){cout<<b1<<','<<b2<<endl;} private:int b1,b2;};class D:public B{ public:D(){d=0;cout<<" 隐含构造函数。 D\n";} D(int,int,int,int,int); ~D(){cout<<" 析构函数。 D\n";} void Print(){B::Print();bb.Print();cout<<d<<endl;} private:int d; B bb; };D::D(int i,int j,int k,int l,int m):B(i,j),bb(k,l),d(m){ cout<<" 五元构造函数。 D\n"; }void main(){ D d1(11,12,13,14,15),d2; d1.Print(); d2.Print(); }

21

输出结果:

构造函数。 B

构造函数。 B

五元构造函数。 D

生成并初始化

对象 d1

隐含构造函数。 B

隐含构造函数。 B

隐含构造函数。 D

生成并初始化

对象 d2

11,12

13,14

15

执行d1.Print();

0,0

0,0

0

执行d2.Print();

析构函数。 D

析构函数。 B

析构函数。 B

释放

对象 d2

析构函数。 D

析构函数。 B

析构函数。 B

释放

对象 d1

22

【例 9.8 】分析程序结果,掌握基类和派生类同名函数的用法。

#include<iostream.h>class M{ public:void Set(int i,int j){m1=i; m2=j;} void Print(){cout<<m1<<','<<m2<<endl; } protected:int m1,m2; };class N:public M{ public:void Set( ){n=m1*m2; } void Print(){cout<<n<<endl; } protected:int n; };void main(){ M m; m.Set(2,3); m.Print( ); N n; n.M::Set(5,8); n.M::Print( ); n.Set( ); n.Print( ); }

注意,同名函数使用时的区别!

输出结果:

2,3

5,8

40

23

9.2.3 子类型和赋值兼容规则

1. 子类型和类型适应B 类具有 A 类的行为(使用成员函数对成员进行操

作),则称 B 类是 A 类子类型。若 B 类是 A 类的子类型,则称 B 类型适应于 A 类型,

且 B 类的对象可以当作 A 类型的对象来使用。“ 白马”和“马”均是抽象的概念(类或类型),一

匹具体的马就是“马”这个类的一个对象。显然,“白马”是“马”的子类型,“白马”适应于“马”,一匹具体的“白马”可以当作一匹具体的“马”来使用。

一般来说,公有继承的派生类是其基类的子类型。子类型不具有对称性,即若 B 类是 A 类的子类型,则

A 类不一定是 B 类的子类型。

24

9.2.3 子类型和赋值兼容规则

2. 公有继承和赋值兼容规则赋值兼容是指在一定条件下,不同类的对象可以

赋值。有以下几种情况:① 子类型对象可以赋值给其父类型对象,反之不

成立。不过父类对象接受的仅仅是子类继承的父类中的成员,对于子类中新增加的或更新的成员是不能接受的,下述两种情况也一样。

② 父类型对象可以引用子类型对象,反之不成立。

③ 父类型对象指针可以指向子类型对象,反之不成立。

25

【例 9.9 】赋值兼容的例子。#include<iostream.h>class M{ public:M( ){m=0;} M(int i){m=i;} void Print( ){cout<<m<<endl;} int Getm( ){return m;} private:int m; };class N:public M{ public:N( ){n=0;} N(int i,int j):M(i),n(j){ } void Print(){cout<<n<<','; M::Print( ); } private:int n; };void fun(M&p){cout<<p.Getm( )-5<<','; p.Print( ); }void main(){ M m(17),q; N n(13,18); n.Print( ); q=n; q.Print( ); M *pm=new M(16); N*pn=new N(15,19); pm=pn; pm->Print( ); fun(*pn); N n1(21,34); M &rm=n1;n1.Print( );}

注意,不同对象(指针)之间的赋值!

26

【例 9.10 】分析程序结果,掌握赋值兼容和保护成员的使用。

#include<iostream.h>class B{ public:void Print(){cout<<b<<endl; } protected:void Setb(int i){b=i*i; } private:int b; };class D1:public B{ public:void Setd1(int i){d1=i;Setb(d1); } void Print(){cout<<d1<<endl; } private:int d1; };class D2:public B{ public:void Setd2(int i){d2=i;Setb(d2); } void Print(){cout<<d2<<endl; } private:int d2; };void main(){ B b; D1 d1; D2 d2; d1.Setd1(7); d1.Print( ); d2.Setd2(5); d2.Print( ); b=d1; b.Print( ); B &rb=d2; rb.Print( ); } 注意,主程序中决不能增加 b.Setb(2);!对一个类来说,保护与私有是一样

的!

27

9.3 多重继承

如有以下定义:class B1{...}; class B2{...}; class B3{...};class D:public B1, public B2, public B3{...};类 D就是由类 B1 、类 B2 、类 B3 公有派生

的派生类,或者说,类 D多重继承类 B1 、B2 、 B3

28

9.3.1 多重继承派生类的构造函数和析构函数

多重继承派生类的构造函数、析构函数不能直接继承,构造函数的定义格式为:

派生类构造函数名 ( 参数表 ): 成员初始化列表{ 派生类构造函数体 }成员初始化列表包括各个基类构造函数、子对象的类构

造函数、其他初始化项。构造函数执行时,首先按照派生类定义时的基类顺序依

次执行所有的基类构造函数,其次执行子对象类的构造函数,最后执行派生类构造函数的函数体。

派生类的析构函数中包含有其所有基类的析构函数,析构函数的执行顺序与构造函数正好相反。没有定义构造函数和析构函数时,系统提供默认的。

29

【例 9.11 】多重继承派生类定义的例子

#include<iostream.h>class B1{public:void Setb1(int i){b1=i;}void Printb1(){cout<<"b1="<<b1<<endl;} private:int b1; };class B2{public:void Setb2(int i){b2=i;} void Printb2(){cout<<"b2="<<b2<<endl;} private:int b2; };class D:public B1,B2 //因为用 class 定义, B2 的继承权限隐含私有{public:void Setd(int i,int j){d=i;Setb2(j);} void Printd(){Printb2();cout<<"d="<<d<<endl;} private:int d; };void main(){D objd; objd.Setb1(16); objd.Printb1( ); objd.Setd(24,76); objd.Printd( );}注意,类 D 有公有成员函数: Setb1 、 Printb1 、 Setd 、 Printd 有私有成员函数: Setb2 、 Printb2 ,私有数据成员: d请问:通过类 D 的对象是否可以操作类 B1 、 B2 的私有数据成员?

30

【例 9.12 】多重继承派生类的构造函数及不同基类中含有同名函数的例子

#include<iostream.h>class B1{public:B1(int i){b1=i;} int Getd( ){return b1;} protected:int b1;};class B2{public:B2(char i){b2=i;} char Getd( ){return b2;} protected:char b2;};class D:public B1,public B2 {public:D(int,char,double); double Getd( ){return d;} private:double d;};D::D(int i,char j,double k):B1(i),B2(j){d=k;}void main( ){B1 b1(18); B2 b2('G'); D d(15,'H',6.56); cout<<"b1="<<b1.Getd()<<"\nb2="<<b2.Getd()<<"\n\nD:b1="; cout<<d.B1::Getd()<<"\nDb2="<<d.B2::Getd()<<"\nD:d="<<d.Getd()<<endl; B1 *ptrb1=&d; B2 rb2=d; // 兼容赋值 cout<<"\nb1="<<ptrb1->Getd()<<"\nb2="<<rb2.Getd()<<endl; }注意,类 D 有 4 个公有成员函数: Getd( 继承 B1) 、 Getd( 继承 B2) 、 G

etd ( 新增加 ) 、 D( 构造函数 ) 有 2 个保护数据成员: b1 、 b2 。有 1 个私有数据成员: d

31

【例 9.13 】多重继承派生类构造函数、析构函数执行顺序验证

#include<iostream.h>class B1{public:B1(int i){b1=i;cout<<" 构造 B1.b1="<<b1<<endl;} void Print(){cout<<b1<<endl;} ~B1(){cout<<" 析构 B1.b1="<<b1<<endl;} private:int b1; };class B2{public:B2(int i){b2=i; cout<<" 构造 B2.b2="<<b2<<endl;} void Print(){cout<<b2<<endl;} ~B2(){cout<<" 析构 B2.b2="<<b2<<endl;} private:int b2; };class B3{public:B3(int i){b3=i; cout<<" 构造 B3.b3="<<b3<<endl;} void Print(){cout<<b3<<endl;} ~B3(){cout<<" 析构 B3.b3="<<b3<<endl;} private:int b3; };

32

【例 9.13 】多重继承派生类构造函数、析构函数执行顺序验证

class B4{public:B4(int i){b4=i; cout<<" 构造 B4.b4="<<b4<<endl;} void Print(){cout<<b4<<endl;} ~B4(){cout<<" 析构 B4.b4="<<b4<<endl;} private:int b4; };class D:public B3,public B1,public B4{public: D(int d1,int d2,int d3,int d4,int d5):B1(d1),B3(d3),B4(d4),b2(d5) {d=d2;} void Print() {B1::Print();cout<<d<<endl;B3::Print();B4::Print();b2.B2::Print();} private:int d; B2 b2;};void main(){ D d(11,12,13,14,15);d.Print(); }注意:构造函数的执行顺序是按照派生类定义的顺序进行的。

33

9.3.2 多重继承的二义性程序设计语言严防语法的二义性,即同一个语句或语法单元可能出

现不同的含义,称为二义性。实际上,计算机在翻译时是不会出现二义性的,二义性实际是程序员在编程时理解上的二义性。多重继承中理解上容易产生二义性的有以下两种情况:

1. 调用不同基类中的相同成员名2. 调用共同基类的成员一个派生类有多个直接基类,而这多个基类又是由一个基类派生的,

即派生类的多个父类拥有共同的父类(相对于派生类来说,应称为祖父类)。如类 A 有父类 B 、 C 、 D ,而 B 、 C 是类 E 的子类,则类 E 是类 A 的共同基类。

解决情况 1. 的方法是在成员名前增加类名及作用域运算符,如类 A的基类 B 、 C 中均有成员 f 在 A 中可被调用,调用 B 中 f 时,应写为 B::f ,调用 C 中 f 时,写成 C::f 。

解决情况 2. 的方法有两种措施,其一与上述方法相同,即成员限定法,在 A 中调用 E 中的成员 h 时,不写 E::h,而写成 B::h 或 C::h,因为 A中使用的 h 是通过 B 或 C 间接继承来的。

其二是采用虚基类 ( 后面介绍)。

34

【例 9.14 】解决不同基类中同名成员二义性的例子

#include<iostream.h>class A {public:void f( ){cout<<"A::f( ).\n";}};class B {public:void f( ){cout<<"B::f( ).\n";} void g( ){cout<<"B::g( ).\n";}};class C:public A,public B {public:void g( ){cout<<"C::g( ).\n";} void h1( ){A::f( );cout<<"C::h1( ).\n";} void h2( ){B::f( );cout<<"C::h2( ).\n";}};void main( ){ C c; c.A::f( ); c.B::f( ); c.B::g( ); c.g( ); c.h1( ); c.h2( ); }注意,类 C 有公有成员函数: f(从 A 继承 ) 、 f(从 B 继承 ) 、 g

(从 B 继承 ) 、 g(自身新建 ) 、 h1 (自身新建 ) 、 h2 (自身新建 )等 6 个,同名成员必须区别,继承来的要注明成员的定义范围,如果不指明范围,一定是派生类中新定义的,否则编译将出错。

35

【例 9.15 】解决共同基类产生二义性的例子

#include<iostream.h>class A{public:A(int i){a=i;cout<<" 构造 A\n";} void Print(){cout<<a<<',';} ~A(){cout<<" 析构 A\n";} private:int a;};class B1:public A{public:B1(int i,int j):A(i){b1=j;cout<<" 构造 B1\n";} void Print(){A::Print();cout<<b1<<',';} ~B1(){cout<<" 析构 B1\n";} private:int b1; };class B2:public A{public:B2(int i,int j):A(i){b2=j;cout<<" 构造 B2\n";} void Print(){A::Print();cout<<b2<<',';} ~B2(){cout<<" 析构 B2\n";} private:int b2; };class C:public B1,public B2{public:C(int i,int j,int k,int l,int m):B1(i,j),B2(k,l){c=m;cout<<" 构造 C\n";} void Print(){B1::Print();B2::Print();cout<<c<<endl;} ~C(){cout<<" 析构 C\n";} private:int c; };void main( ){ C c(16,19,23,25,38);c.Print();} 注意,类 C 的公有成员函数 Print 有 5 个,从 A 中间接继承来 2 个,一个由 B1继承来,一个由 B2 继承来。若增加语句 c.A::Print( ); 则出现歧义错误。另外,从运行结果可以看出,类 A 的构造函数被调用 2 次。

36

【例 9.16 】成员限定法选择不同基类中的同名函数,解决二义性的例子

#include<iostream.h>class A{ public:char *Return( ){return "A.\n"; } };class B:public A{ public:char *Return( ){return "B.\n"; } };class C:public B{ public:char *Return( ){return "C.\n"; } };class D:public C{ public:char *Return( ){return "D.\n"; } };void main(){ D d; cout<<d.A::Return( ); cout<<d.B::Return( ); cout<<d.C::Return( ); cout<<d.Return( );}注意, Return 函数的具体表示方法。

37

9.3.3 虚基类

将公共基类说明成虚基类,可以避免派生类构造时,公共基类的构造函数被多次调用的情况,而且在初始化派生类对象时内存中只保存一份公共基类的成员。

说明虚基类的方法是: class 派生类名 virtual 继承方式 虚基类名 使用虚基类的派生类的构造函数中成员初始化列表中应专门

列出虚基类的构造函数: 派生类构造函数名 ( 参数表 ): 若干基类构造函数,子对象

构造函数,虚基类构造函数 { 派生类构造函数体 }只在创建派生类对象时调用一次虚基类构造函数,而在基类

构造函数中不再调用虚基类构造函数,用以保证虚基类成员只初始化一次。

38

【例 9.17 】在【例 9.15 】中使用虚基类解决共同基类二义性

#include<iostream.h>class A{public:A(int i){a=i;cout<<" 构造 A\n";} void Print( ){cout<<a<<',';} ~A( ){cout<<" 析构 A\n";} private:int a;};class B1:virtual public A{public:B1(int i,int j):A(i){b1=j;cout<<" 构造 B1\n";} void Print( ){A::Print( );cout<<b1<<',';} ~B1( ){cout<<" 析构 B1\n";} private:int b1; };class B2:virtual public A{public:B2(int i,int j):A(i){b2=j;cout<<" 构造 B2\n";} void Print( ){A::Print( );cout<<b2<<',';} ~B2( ){cout<<" 析构 B2\n";} private:int b2; };

39

【例 9.17 】在【例 9.15 】中使用虚基类解决共同基类二义性

class C:public B1,public B2{public:C(int i,int j,int k,int l,int m):B1(i,j),B2(k,l),A(i) { c=m;cout<<" 构造 C\n"; } void Print( ) {B1::Print( );B2::Print( );cout<<c<<endl;} ~C( ){cout<<" 析构 C\n";} private:int c; };void main( ){ C c(16,19,23,25,38);c.Print( );c.A::Print( );}

注意,与例 15比较类 C 的公有成员函数 Print 只有 4 个,从A 中间接继承来的只有 1 个,即由 B1 和 B2 继承来的是一个。若增加语句 c.A::Print( ); 则不会出现歧义错误。另外,从运行结果可以看出,类 A 的构造函数被调用 1 次。

40

【例 9.18 】运行程序,分析结果,注意虚基类的作用

#include<iostream.h>class A{public:A(const char*s){cout<<s<<endl;} };class B:virtual public A{public:B(const char*s1,const char*s2):A(s1) {cout<<s2<<endl;} }; class C:virtual public A{public:C(const char*s1,const char*s2):A(s1) {cout<<s2<<endl;} }; class D:public B,public C{public: D(const char*s1,const char*s2, const char*s3,const char*s4): B(s1,s2),C(s1,s3),A(s1){ cout<<s4<<endl;} };void main( ){ D *ptr=new D("class A","class B","class C","class D"); delete ptr;}注意,构造函数的执行顺序为: 虚基类 A 、基类 B 、基类 C 、派生类 D

41

9.3.4 多重继承应用举例 【例 9.19 】计算 4 类人员月收入的程序 面向对象设计过程: 一、设计类(对象) 1.顶层类(对象):职工(编号,姓名,薪金,数据初始化

方法) 2. 、二层类(对象):技术员(小时工资)、总经理(固定工资)、销售员(销售额提成)

3. 、三层类(对象):销售经理(固定工资+销售额提成) 每个类(对象)均有数据初始化方法、计算薪金方法、显示

数据方法等(公有成员函数) 二、研究类(对象)的层次关系 显然,二层类(对象)可以从“职工”类(对象)继承许多信息,“销售经理”又可以从二层类(对象)中的“总经理”和“销售员”中继承,这样一来,相对于“销售经理”来说,“职工”是公共基类,可以设计成虚基类。

42

【例 9.19 】源程序 ( 定义“职工”类 )

#include<iostream.h>class Employee{public: Employee( ) {cout<<"编号: ";cin>>no; cout<<"姓名: ";cin>>name; salary=0;} protected: int no; char name[20]; double salary;};

43

【例 9.19 】源程序 ( 定义“技术员”类 )

class Technician:public Employee{public: Technician( ){hourlyrate=150;} void pay( ) {cout<<"\n技术员: "<<name<<" 本月工作时数: "; cin>>workhours; salary=hourlyrate*workhours;} void display( ) {cout<<"技术员 :"<<name<<" 编号 :"<<no; cout<<" 月薪 :"<<salary<<endl; } private: double hourlyrate; int workhours;};

44

【例 9.19 】源程序 ( 定义“销售员”类 )

class Salesman:virtual public Employee{public: Salesman ( ){commrate=0.07;} void pay( ) {cout<<"\n销售员: "<<name<<"月销售量 :"; cin>>sales; salary=sales*commrate;} void display( ) { cout<<"销售员 :"<<name<<"编号 :"; cout<<no<<"月薪 :"<<salary<<endl; } protected: double commrate,sales;};

45

【例 9.19 】源程序 ( 定义“总经理”类 )

class Manager:virtual public Employee{public: Manager( ){ monthlypay=8500; } void pay( ){ salary=monthlypay; } void display( ) {cout<<"经理 :"<<name<<" 编号 :"<<no; cout<<" 月薪 :"<<salary<<endl; } protected: double monthlypay;};

46

【例 9.19 】源程序 ( 定义“销售经理”类 )

class Salesmanager:public Manager,public Salesman{public: Salesmanager( ) { monthlypay=4000; commrate=0.005; } void pay( ) { cout<<"\n销售经理 :"<<name<<"部门月销售量 :"; cin>>sales; salary=monthlypay+sales*commrate;} void display( ) {cout<<"销售经理 :"<<name<<" 编号 :"<<no; cout<<" 月薪 :"<<salary<<endl; }};

47

【例 9.19 】源程序 ( 定义主函数 )void main( ){ cout<<"创建经理信息 :\n"; Manager m1; cout<<"创建技术员信息 :\n"; Technician t1; cout<<"创建销售员信息 :\n"; Salesman s1; cout<<"创建销售经理信息 :\n"; Salesmanager sm1; m1.pay( ); m1.display( ); t1.pay( ); t1.display( ); s1.pay( ); s1.display( ); sm1.pay( ); sm1.display( );} 观察本程序运行结果,体会面向对象编程的思想

48

【例 9.20 】计算日期和时间的程序

面向对象设计过程: 一、设计类(对象) 1.“日期”类(年、月、日等数据,设置日期、输出日期等方法)

2.“ 时间”类(时、分、秒等数据,设置时间、输出时间等方法)

3.“ 时间日期”类,有时间和日期的所有数据,并增加日期和时间的联合输出方法

二、研究类(对象)的层次关系 显然,“时间日期”类,可由“日期”、“时

间”两个类多重继承得来。

49

【例 9.20 】源程序 ( 定义“日期”类 )#include<iostream.h>#include<stdio.h>typedef char string80[80];class Date{public: Date( ){ } Date(int y,int m,int d){ SetDate(y,m,d); } void SetDate(int y,int m,int d) { Year=y;Month=m;Day=d; } void GetStringDate(string80 &Date)//此处 Date 不是类名 , 是字符串

名 { sprintf(Date,"%d,%d,%d",Year,Month,Day); // 上述函数将年月日的具体数据作为字符串输出到字符数组 Date 中 } protected: int Year,Month,Day;};

50

【例 9.20 】源程序 ( 定义“时间”类 )

class Time

{public:

Time( ){ }

Time(int h,int m,int s){ SetTime(h,m,s); }

void SetTime(int h,int m,int s)

{ Hours=h;Minutes=m;Seconds=s; }

void GetStringTime(string80 &Time)

{ sprintf(Time,"%d,%d,%d",Hours,Minutes,Seconds); }

protected:

int Hours,Minutes,Seconds;

};

51

【例 9.20 】源程序 ( 定义“时间日期”类 )

class TimeDate:public Date,public Time{public: TimeDate( ){ } TimeDate(int y,int mo,int d,int h,int mi,int s): Date(y,mo,d),Time(h,mi,s) { } void GetStringDT(string80 &DTstr)

{ sprintf(DTstr,"%d/%d/%d;%d:%d:%d",Year,Month, Day,Hours,Minutes,Seconds);

}

};

52

【例 9.20 】源程序 ( 定义主函数 )void main( ){ TimeDate date1,date2(2003,2,26,9,11,30); string80 DemoStr; // 定义了一个字符数组,即字符串 date1.SetDate(2000,3,24); date1.SetTime(12,45,15); date1.GetStringDT(DemoStr); cout<<"The date1:"<<DemoStr<<endl; date1.GetStringDate(DemoStr); cout<<"The date1:"<<DemoStr<<endl; date1.GetStringTime(DemoStr); cout<<"The date1:"<<DemoStr<<endl; date1.GetStringDT(DemoStr); cout<<"The date1:"<<DemoStr<<endl; }注意,程序中用到库 stdio.h 中的函数 sprintf(字符串 ,输出格式 ,输出项列表 )的作用是将输出内容输出到字符串中。

53

小结与作业 继承是代码复用技术的体现 派生类由基类派生 派生类接收基类中的所有成员,并可能增加新成员 派生类继承的方式不同,基类成员在派生类中的地位不同 继承分单一继承、多重继承,继承又可以多层继承(即间

接继承) 继承造成的同名现象容易引起二义性,二义性的处理靠成

员限定法和虚基类法。 对成员的访问控制分对类中成员函数的访问权限的控制和

对对象的访问权限的控制 子类型的对象可以赋值给其父类型的对象,但是父类型对

象仅仅接受子类型继承父类型的部分成员。 作业:第 9 章后的所有习题