39
第5第 第第第第第第第 第第第第第第第第第第第第第第第第第第第 第第 第第第 传统 第第第 第第 第第第第第第第第第第第第第第第第第第第第第第第第第第第第第第第 ,一。 第第第第第第第第第第第第第第第 第 第第第第第第第 第第第第第第 传统一 第第第第第第第第第第第第第第第第第第第第第第第 第第第第 第第第第第第第 第第第第第第第第第第第第第第第第第第第第第第第第第第第第第 ,, 第第第第第第第第第第第第 第第第 C++ 第第第第第第第第第第第第第第第第第 第第第第第

第 5 章 派生类与继承性

  • Upload
    ataret

  • View
    91

  • Download
    0

Embed Size (px)

DESCRIPTION

第 5 章 派生类与继承性. 新的软件总是在原先开发的基础上进行扩充。对于传统的程序员而言,唯一的方法便是改写或重写这些早先定义好的代码。这样的程序设计方法使得所设计出来的程序无法重复使用,是传统程序设计中的一个严重缺点。. 面向对象程序设计利用面向对象中继承的概念与方法,用户不必改动原来的程序,通过继承不仅可以使用原有的数据类型,还可以定义新的数据类型以补充原有数据类型的不足,这就是 C++ 中通过派生类与继承性来实现软件设计的可重用性。. 5.1 派生类的概念和定义. - PowerPoint PPT Presentation

Citation preview

Page 1: 第 5 章  派生类与继承性

第 5 章 派生类与继承性 新的软件总是在原先开发的基础上进行扩充。对于传统的程序员而言,唯一的方法便是改写或重写这些早先定义好的代码。这样的程序设计方法使得所设计出来的程序无法重复使用,是传统程序设计中的一个严重缺点。 面向对象程序设计利用面向对象中继承的概念与方法,用户不必改动原来的程序,通过继承不仅可以使用原有的数据类型,还可以定义新的数据类型以补充原有数据类型的不足,这就是 C++ 中通过派生类与继承性来实现软件设计的可重用性。

Page 2: 第 5 章  派生类与继承性

5.1 派生类的概念和定义5.1.1 派生和继承的特性 派生和继承是一个非常自然的属性,人类社会和动植物界都具有典型的派生和继承特性,人们对事物的分类描述也是如此。见图 5.1 。 在派生和继承体系或分类描述体系中,整个体系具有金字塔结构,最高层是最普遍、最一般的,每一层都比它的上一层更具体,低层含有高层的特性,同时又与高层有细微的差别。 因此,在 C++ 的类定义中,应该遵循这种规律:更高层的类具有更普遍的特征,更低层的类具有更具体的特征。

Page 3: 第 5 章  派生类与继承性

5.1.2 派生类的一般定义格式 在 C++ 中,派生类的一般定义格式为: class 派生类名: <access> 基类类名 { ... // 本派生类新定义的成员 };

上述定义中派生类是新定义的类,基类是已有的类,基类类名前的 <access> 称为访问描述符,它可以是下面三个关键字的任何一个: private, pr

otected, public ,分别称为私有派生、保护派生和公有派生。基类可以有多个,用逗号隔开,这种情况称为多重继承。

Page 4: 第 5 章  派生类与继承性

访问描述符: 若为 private, 称为私有派生,基类的所有公有段和保护段的成员都成为派生类的私有段成员;

程序 cpp5-1a (p124-126) 。

注意,基类的私有段成员无论采用什么样的访问描述符都不能成为派生类的任何成员。

若为 public ,称为公有派生,那么基类的所有公有段成员都成为派生类的公有段成员,基类保护段的成员都成为派生类的保护段成员。

若为 protected, 称为保护派生,基类的所有公有段和保护段的成员都成为派生类的保护段成员;

Page 5: 第 5 章  派生类与继承性

5.1.3 派生类新定义的成员与继承来的 成员的关系 在上述派生类定义格式的 { } 中,你也可以定

义完全属于本类的新成员,其定义方法与前一章介绍的类的定义方法完全一样。

实际上,当你采用私有派生或保护派生时,你必须在该派生类的公有段中至少新定义一个成员函数,以便你能从外界使用该派生类,否则该派生类永远也不能被使用,因为他从基类继承来的成员都是对外界不能使用的私有成员或保护成员。例如,可以把上面的 showa() 函数的内容改为如下:

Page 6: 第 5 章  派生类与继承性

void sona::showa() { set(); show(); }

然后在 main 函数中通过语句“ son1.showa

();” 而达到在外界使用类 sona 的私有成员函数 sh

ow() 的目的。 在派生类中,自己新定义的成员与从基类继承来的成员两者是有一定差别的。实际上,只有新定义的成员才真正属于自己的,而继承来的成员只是可以当作成员来使用,但并不真正属于自己,它仍属于基类。这种差别具体体现在以下两个方面:

Page 7: 第 5 章  派生类与继承性

① 当在派生类中新定义了一个与继承来的成员同名的成员时,派生类默认使用的成员是新定义的成员,继承来的同名成员被隐藏起来,但它仍然存在。

② 继承来的成员函数可以使用基类的私有数据,但新定义的成员函数不能使用基类的私有数据。

若要在派生类中使用基类的同名成员,必须显式地用基类类名和作用域运算符来指定: 基类类名 :: 成员

Page 8: 第 5 章  派生类与继承性

例如,在上面的程序的派生类 sonc 定义中,把新定义的成员函数 showc 改为 show ,内容不变,然后在 main 函数中把语句“ son3.showc();”

改为下面二个语句:

The sonc data:protected money: 500public height: 160The man data:private age: 20protected money: 500public height: 160

该两句的运行结果为:

son3.show(); son3.man::show();

程序 cpp5-1b (p126) 。

Page 9: 第 5 章  派生类与继承性

5.1.4 派生类中的静态成员 派生类可访问基类的任何静态成员,但在访问时必须用“类名 :: 成员”显式地说明。

class B { public: static void f() { cout<<"B::f()\n";} voig g() { cout<<"B::g()\n";} };

class D: private B { };class DD: public D{public: void h() { B::f(); // 正确 f(); // 错误 g(); // 错误 }

};程序 cpp5-2 (p127) 。

Page 10: 第 5 章  派生类与继承性

5.2 访问控制 派生类继承了基类的全部数据成员和除了构造、析构函数之外的全部成员函数,但是这些成员在派生类中的访问属性在派生的过程中是可以调整的,继承方式控制了基类中具有不同访问属性的成员在派生类中的访问属性。

类的继承方式有 public 、 protected 和 private 三种,不同的继承方式,导致原有具有不同访问属性的基类成员在派生类中的访问属性也有所不同。

Page 11: 第 5 章  派生类与继承性

5.2.1 公有继承 当类的继承方式为公有继承时,基类的 public和 protected 成员的访问属性在派生类中不变,而基类 private 成员不可访问。即基类的 public和 protected 成员作为派生类的 public 和 protected 成员,派生类的其他成员可以直接访问。

例 5.1 (p128)

在这个例子中,由基类 A 派生出新的 B 类。 B类继承了 A 类的成员,也就继承了基类的所有属性,实现了代码的重用,同时,通过新增成员,加入了自身的独有特征,达到了程序的扩充。

Page 12: 第 5 章  派生类与继承性

5.2.2 私有继承 当类的继承方式为私有继承时,基类 public 和protected 成员都以 private 成员身份出现在派生类中,而基类 private 成员不可访问。即基类的 public 和 protected 成员被继承后作为派生类的私有成员,派生类的其他成员可以直接访问它们,但是在类外部通过派生类的对象无法访问。

经过私有继承之后,所有基类的成员都成为派生类的私有成员,如果进一步派生的话,基类的成员就无法在新的派生类中被访问。因此,一般情况下私有继承的使用比较少。 例 5.3(p133) 。

Page 13: 第 5 章  派生类与继承性

5.2.3 保护继承 保护继承中,基类 public 和 protected 成员都以 protected 成员身份出现在派生类中,而基类 private 成员不可访问。即基类的 public 和 protected 成员被继承以后作为派生类的保护成员,派生类的其他成员可以直接访问它们,但在类外部通过派生类的对象无法访问。

在类的多层次继承关系中,保护成员为共享访问与成员隐蔽之间找到一个平衡点,既能实现成员隐蔽,又能方便继承,实现代码的高效重用和扩充。

Page 14: 第 5 章  派生类与继承性

5.2.4 访问声明

当采用私有派生或保护派生时,从基类继承来的成员都是对外界不能使用的私有成员或保护成员。这种“一刀切”的做法有时候会带来许多不便。有时我们希望基类的某一个成员在私有派生或保护派生后能变成公有成员,以便能被外界所使用。

通过访问描述符,不同的继承方式导致基类的成员在派生类中对类外来说具有不同的访问属性(对类内成员函数来说是相同的) 。

Page 15: 第 5 章  派生类与继承性

为了满足这种需要, C++ 提供了一种调节机制,称为访问声明,它可以改变该成员的访问权限。访问声明的定义形式是在派生类的公有段中作如下声明: 基类类名 :: 成员 例如: class B { private: int a; public: int b,c; void bf(){...} };

class D:private B{private: int d;public: B::c; // 访问声明 void df(){...}};

Page 16: 第 5 章  派生类与继承性

void main(){ B bobj; bobj.b=5; bobj.bf(); cout<<"B::b="<<bobj.b<<"\n"; D dobj; dobj.c=10; dobj.df(); cout<<"D::c="<<dobj.c<<"\n";}

对访问声明的使用需要注意以下几点:

(1) 访问声明仅仅调整成员的访问权限,不可改变其数据类型,即不能为它说明类型。如以下语句是错误的: int B::c;

程序 cpp5-3

Page 17: 第 5 章  派生类与继承性

(2) 访问声明仅用于派生类中调整成员的访问权限,不允许在派生类中降低或提升基类成员的可访问性,即只有基类中的公有段成员和保护段成员才能在私有派生类中被声明为公有段成员或保护段成员,或在保护派生类中被声明为公有段成员。基类的私有成员不能用于访问声明 , 如: B::a; // 错误

(3) 对重载函数名的访问声明将使所有同名的重载函数的访问权限都得到调整。这一条若与 (2)

同时起作用,将使具有不同访问权限的重载函数名不能用于访问声明。

Page 18: 第 5 章  派生类与继承性

例如下列访问声明是错误的: class X { private: int f(int); public: void f(); }; class Y: private X { public: X::f; // 错误,重载函数 f 具有不同的访问权限 };

Page 19: 第 5 章  派生类与继承性

同时也意味着,如果在派生类中新定义了一个与从基类继承来的成员同名的成员,则不能调整它的访问权限。例如下面的访问声明是错误的:

class X{ public: void f();};

class Y: private X{ public: void f(int); X::f; // 错误,函数 f 具有不同的访问权限};

Page 20: 第 5 章  派生类与继承性

5.3 派生类的构造函数和析构函数 派生类不能继承基类中的构造函数。在建立一个类等级后,我们通常创建某个派生类的对象来使用这个类等级,包括隐含地使用基类的数据和函数。如果基类的构造函数缺省或没有参数或有默认参数值,则其派生类可以不需要构造函数。

但是,基类往往有构造函数或有参数化的构造函数,当创建一个派生类对象时,怎样调用基类的构造函数对基类数据初始化?

Page 21: 第 5 章  派生类与继承性

显然,需要提供一种初始化机制使得在创建派生类对象时,能够通过访问基类的构造函数来初始化基类的数据。

C++ 在派生类的构造函数中提供这种初始化基类的机制,它允许在多重继承中所有基类的构造函数的参数在派生类的构造函数的参数中给出。 派生类构造函数的定义为: 派生类名 (总参数表 ):基类名 (参数表 ), 对象成员(参数表){ … // 派生类自己新定义数据成员的初始化 };

Page 22: 第 5 章  派生类与继承性

这种初始化机制与第 3 章中的类类型的数据成员初始化方式是一致的。“:”后面是基类的构造函数和参数表以及对象成员和参数表。这里,总参数表包括基类的参数表、对象成员的参数表和自己新定义数据成员的参数表三个方面的初始化数据。

例 5.7 ,我们在一个基类 A 中定义了一个数据成员 x 和一个有一个参数的构造函数,在一个派生类中又定义一个对象成员 a 和一个数据成员 d ,因此在派生类的构造函数中就必须给出总共 3 个参数。

Page 23: 第 5 章  派生类与继承性

当在 main 函数中定义一个派生类的对象时就必须同时给出 3 个初始化数据,如: B b(17,2,-5);

派生类构造函数执行时遵循先基类、再对象成员,后自己新定义的数据成员的顺序。如果基类使用缺省构造函数或不带参数的构造函数,那么派生类构造函数定义中“:”后面的“基类名(参数表)”一项可以省去,但是派生类构造函数执行时仍然隐含地调用基类的构造函数。 当程序结束退出执行析构函数时,先执行派生类的析构函数,再执行基类的析构函数。

程序 cpp5-4 ( 例 5.7 p140-141) 。

Page 24: 第 5 章  派生类与继承性

5.4 多重继承与虚基类5.4.1 多重继承的概念 继承是实现软件重用的重要方法。当继承不断地扩展时,继承既可以是多个层次的,也可以有多个父类,这就是多重继承。

所谓多重继承,就是某一派生类具有多个直接基类。因此,它的声明和定义格式与单一继承的不同之处在于有多个基类类名,这些基类类名之间用“,”隔开,并要求这些基类类名不能两两相同,即一个类不能被多次说明为一个派生类的直接基类。

Page 25: 第 5 章  派生类与继承性

例如,有 3 个类 A 、 B 、 C : class A { ... }; class B { ... }; class C {…};

若类 D 欲同时继承 A 、 B 和 C ,则其表示方法为: class D: public A, public B, public C { ... };表示派生类 D 同时公有继承继承基类 A 、 B 和 C 。 C++ 规定,若省略对基类的访问描述符,则默认为 private 。所以上述 B 和 C 之前的 public不能省略。

Page 26: 第 5 章  派生类与继承性

5.4.2 多重继承下派生类的构造函数 多重继承下派生类的构造函数设计必须负责所有基类中的成员初始化,参数个数应同时考虑多个基类初始化所需要的参数数目,调用基类的构造函数的顺序取决于定义派生时所指明的基类顺序,而不是在派生类构造函数的“:”后所指定的初始化表的顺序。

多重继承必须考虑以下两个问题: (1) 是来源于不同基类的成员可能重名的问题 ;

(2) 是同一个基类在多层次继承下多次成为某一派生类的间接基类的问题,此问题又称为虚基类。

Page 27: 第 5 章  派生类与继承性

5.4.3 多重继承下的二义性1. 调用不同基类的相同成员时出现二义性

class A{ public: void f(){cout<<"I am A::f()\n";} };class B{public: void f(){cout<<"I am B::f()\n";}

void g(){ }};

void main(){ C c; c.f(); // 访问不明确}

class C:public A, public B{public: void g(){ } void h(){ }}; 程序 cpp5-5

Page 28: 第 5 章  派生类与继承性

对于二义性问题的解决,可以有二种选择:

第一,利用作用域运算符“ ::” 指明要调用的成员函数所属的类范围。

第二,在派生类中重新定义一个同名成员,使得该成员可隐藏基类中同名称的成员。

例如,上面派生类 C 的 f() 函数可定义如下: void C:: f()

{ A::f(); // 指定调用类 A 的 f 函数 B::f(); // 指定调用类 B 的 f 函数 }

程序 cpp5-5

Page 29: 第 5 章  派生类与继承性

在 C++ 中,一个类不能被多次说明为一个派生类的直接基类,但在一个多层次的多重继承体系中可以多次成为一个派生类的间接基类。这样就会造成一个类在一个派生类中有多个拷贝。

例如,我们定义了一个类 A ,类 B1 和类 B2 都从类 A 公有派生,而类 C 是从类 B1 和类 B2 多重继承的,因此在类 C 中就间接地有了类 A 的两份拷贝。

2. 访问共同基类的成员时出现二义性

程序 cpp5-6A (p144) 。

Page 30: 第 5 章  派生类与继承性

5.4.4 虚基类 为了解决上述成员模棱两可的问题, C++ 引人了虚基类的概念。虚基类是一个基类虽被多次的继承,但是各派生类却共用该基类的一份拷贝,这非常像类中的静态成员。

要定义某一基类为虚基类,只须在派生类定义时,在各派生类名和“:”后加关键字 virtual 即可。例如,我们把上例中的类 B1 和类 B2 的定义改为: class B1: virtual public A{};

class B2: virtual public A{};

那么再运行该程序 ( 程序 cpp5-6B) ,就没有错误。

Page 31: 第 5 章  派生类与继承性

把一个基类定义为虚基类后,所要考虑的主要问题是虚基类的初始化顺序问题。虚基类的初始化与多继承的初始化在语法上是一样的,但隐含的构造函数的调用次序有点差别。 虚基类构造函数的调用次序是这样规定的: ① 虚基类的构造函数在非虚基类之前调用;

② 若同一层次中包含多个虚基类,虚基类构造函数按它们说明的次序调用; ③ 若虚基类由非虚基类派生,则遵守先调用基类构造函数,再调用派生类构造函数的规则。

程序 cpp5-7

Page 32: 第 5 章  派生类与继承性

5.5 类类型转换 在 C++ 中,系统提供了对预定义类型的隐式转换和显式转换机制。对于类类型,是否也存在一种类型转换机制,使得类对象之间能进行类型转换?

在 C++ 中,类被视为用户定义的类型,可以像系统预定义类型一样进行类型转换。一般来说,类类型的转换是由单一参数的构造函数和类型转换函数来实现的,前者用于将一般类型的数据转换为类类型的数据,后者将类类型的数据转换为一般类型的数据。

Page 33: 第 5 章  派生类与继承性

5.5.1 通过单一参数的构造函数将一般类型转换为类类型

当类中定义了一需要单个参数的构造函数时,便提供了一个将一般数据类型的数值或变量转换为类类型,并赋值给该类对象的方法。换句话说,只需一个参数的构造函数提供了类型转换的功能。

当程序中提供了单个参数的构造函数后,编译系统若发现程序中有需要使用类型转换的地方,便会自动地调用该构造函数,以完成类型转换工作,而并不需要用户特别指明。

Page 34: 第 5 章  派生类与继承性

下面的例子中类 time 用来记录越野赛跑者在到达各站所用去的时间。在类 time 中一共定义了四个构造函数,使得用户能够以不同的时间单位来记录时间。程序 cpp5-8(p146-147) 。

上例中“ time(long s);” 就是单参数的构造函数,它既能用于定义对象时的初始化工作,也能用于把一个整数赋给一个对象,如下所示: time p1(13521);

p1=22030;

因此,该构造函数就是实现把整型值转换为类类型time 的功能函数。

Page 35: 第 5 章  派生类与继承性

5.5.2 通过类型转换函数将类类型转换为一般类型

要将类类型的数据转换为其他类型的数据,需要借助于一种特殊的方法—类型转换函数。

类型转换函数提供了方法使用户可以明确地将用户定义的类类型转换为系统预定的数据类型,如: int i=(int)p1;

或 int i=int(p1);

上述两句都是把类类型 p1 转换为整型。

Page 36: 第 5 章  派生类与继承性

当编译系统发现等号的左边为一整型变量而右边为类类型的数据(对象)时,便会去寻找类中是否定义这样的类型转换函数,若有便调用之,以完成转换。

类型转换函数的声明格式为: operator 类型名(); 类型转换函数的定义格式为:

类型转换函数是类的成员函数,不能被定义为友元函数。类型转换函数的样子与运算符函数非常类似,都是以 operator 为关键字的。

Page 37: 第 5 章  派生类与继承性

类名 ::operator 类型名 () { ...... return ( 类型名 ) 类的数据成员;或 return 类型名 ( 类的数据成员 ); }

类型转换函数没有参数,也没有返回类型,但这个函数体内必须有一条返回语句,返回一个具有所要类型的数据。由于类型名已告诉了编译系统所要返回的类型,因此无须再指明其返回值类型。

Page 38: 第 5 章  派生类与继承性

下面看一个圆的实例程序 cpp5-9(p148) 。

上例中,定义了三个类型转换函数,分别用于将 circle 类对象数据转换为 int 型、 long 型和double 型。因此,在 main 函数中,同样是在“ =” 右边的“ c1” ,而左边的变量却是不同类型不同含义的,因而实际的结果是类型转换函数自动找出对象中的某个数据成员,然后转换类型后赋给“ =” 左边的变量。

转换函数用于将类类型数据转换成其他类型的数据,因此必须为类的成员函数,且没有参数,所以类型转换函数也不能被重载。

Page 39: 第 5 章  派生类与继承性

作业3.1 定义一个哺乳动物 Mammal 类,由此派生出狗 Dog 类和猫 Cat 类。3.2 定义一个人 Person 类,由此派生出教师 Teach

er

类和学生 Student 类。 两题均要求基类中有数据成员和构造函数,派生类中有新的数据成员和构造函数。