1、第1章 面向对象程序设计概述近年来, 面向对象程序设计的思想已经被越来越多的软件设计人员所接受。它是在吸收结构化程序设计的一切优点的基础上发展起来的一种新的程序设计思想。这种新的思想更接近人的思维活动, 人们利用这种思想进行程序设计时, 能够很大程度地提高编程能力, 减少软件维护的开销。面向对象系统最突出的特点是封装性、 继承性和多态性。11什么是面向对象程序设计 在软件设计和实现中, 传统的被人们广泛使用的方法是面向过程的程序设计方法。在讨论面向对象程序设计之前, 我们需要讨论一下面向过程的程序设计。111面向过程程序设计的基本概念面向过程的程序设计思想的核心是功能的分解: 第一步 要做的工
2、作就是将问题分解成若干个称为模块的功能块;第二步 根据模块功能来设计一系列用于存储数据的数据结构;第三步 编写一些过程(或函数)对这些数据进行操作。显然, 这种方法将数据结构和过程作为两个实体来对待, 其着重点在过程。设计人员首先考虑如何将功能分解, 在每一个过程中又要着重安排程序的操作序列, 但同时程序员在编程时又必须时时考虑数据结构, 因为毕竟要将操作作用于数据上。数据结构和过程的这种分离, 给软件人员造成沉重的负担。例如, 我们不可能要求数据结构始终没有变化, 且不说在软件维护的时候数据结构有可能发生变化, 就是在软件开发的过程中也不能保证数据结构不变化。面向过程的程序设计的缺点之一就是
3、一旦数据结构需要变更的时候, 必须修改与之有关的所有模块。因此, 面向过程的程序的可重用性差, 维护代价高。 下面, 我们举一个实例来进一步讨论面向过程的程序设计方法。 考虑一个银行系统。该系统允许顾客开设不同类型的银行账户, 其中包括现金账户、 支票账户和贷款账户, 同时允许顾客存款、 取款和转账。根据面向过程的程序设计方法, 首先我们将银行系统分解成三个模块分别负责存款、 取款和转账三项工作。这三个过程是MakeDeposit、 WithDraw和Transfer。接着, 建立一个简单的数据结构: struct account char name; /* 姓名 */ unsigned lo
4、ng accountld; /* 账号 * float balance /* 余额 */ float interestYTD * 年利息 * char accountType *账户类型(现金、 支票和贷款) * ; 然后, 对每个过程按照一定的操作顺序编写程序。我们来分析一下, 程序员所关心的是否与顾客一致。( 1) 对于一个顾客来说, 她最关心的是账户中还有多少钱, 利息是多少, 钱存在银行是否安全, 而不会对存钱和取钱的程序执行的过程感兴趣。对她而言, 只是需要知道存、 取款手续, 并按照这些手续去做就足够了。( 2) 程序设计人员所关注的是如何写存、 取款的代码, 如何在已经建立的数据
5、结构中填写数据并管理它们。可见, 程序员与顾客关心的事情是不一致的, 这是由于数据结构与”过程”的分离造成的。再进一步分析, 顾客与她们的银行账户有没有特殊的关系。在软件系统中, 由于顾客只不过是一串字符和数字, 因而不用考虑到底是谁拥有该账户, 账户里的内容是什么, 而银行账号也只不过是一个整数。 另外, 由于数据结构与”过程”的分离, 程序员能够非常方便地修改账目, 因为对她来说, 只不过是在修改数据结构中的数字, 实际上她可能经过修改数据而取走顾客的钱。 最后, 我们考虑如果数据结构发生了一些变化会产生什么样的结果。对于accountType数据项, 原来只有现金账户、 支票账户和贷款账
6、户三种取值, 程序只能识别这三种取值。由于账户类型的不同, 我们在编写存款等过程时, 将以不同的操作序列与之对应(在同一个过程中分情况处理)。对现金账户、 支票账户和贷款账户的处理不会完全相同。假设现在需要增加一种账户类型退休账户, 后果如何?原来的程序肯定出错。因为在原来的程序中我们只考虑了以上三种账户的情况, 对于新增加的账户类型, 原来的程序不会处理。也就是说, 每增加一种新的账户类型, 都必须重新编写程序代码, 可见其维护软件的开销是相当大的。 上述这些问题的出现都是由于面向过程程序设计的解决方法的着重点在功能, 而我们经过分析发现, 数据对于客户(特别是顾客)似乎更重要。在这里, 程
7、序员关心的是如何做(how to do), 而顾客则关心的是做什么(what t0 do), 这是由于过程和数据的分离造成的。使用面向对象的程序设计技术是解决这些问题的最好方法。112面向对象程序设计的基本概念 在面向对象的程序设计中, 着重点在那些将要被操作的数据, 而不是在实现这些操作的过程。数据构成了软件分解的基础, 而不是功能。我们首先要分析顾客在账户(数据)中要做什么, 然后提供相应的操作, 更重要的是不能将数据和相应操作看成两个分离的实体, 而是要把它们作为一个完整的实体来对待。数据与定义在它上面的用户需要的操作构成一个整体。同时, 数据本身不能被外部程序和过程直接存取。如果想修改
8、银行账户中的数据, 惟一的办法是在该数据上提供修改操作, 这些修改操作是以用户应得到的利益为根据。当我们把对银行账户的操作定义在数据上, 银行账户就是一个类, 称为银行账户类。作为其实例, 我们能够建立许多具体的银行账户, 而每一个具体的银行账户就是银行账户类的一个对象。 现在, 我们给面向对象程序设计下一个定义。面向对象程序设计是一种新的程序设计范型。面向对象程序的主要结构特点是: 第一, 程序一般由类的定义和类的使用两部分组成, 在主程序中定义各对象并规定它们之间传递息的规律; 第二, 程序中的一切操作都是经过向对象发送消息来实现的, 对象接收到消息后, 启动有关方法完成相应的操作。 面向
9、对象程序设计的最大优点就是软件具有可重用性。当人们对软件系统的要求有所改变时, 并不需要程序员做大量的工作, 就能使系统做相应的变化。类与对象是面向对象程序设计中最重要的概念, 也是一个难点, 想要掌握面向对象程序设计的技术, 首先就要很好地理解这两个概念。12对象与类121对象与类的概念 在现实世界中, 人们是如何认识”对象”和”类”的。在日常生活中对象就是我们认识世界的基本单元, 它能够是人, 也能够是物, 还能够是一件事。整个世界就是由形形色色的”对象”构成的。例如一辆车、 一个球、 一个小学生、 一次演出。对象既能够很简单, 也能够很复杂, 复杂的对象能够由若干简单的对象构成。 对象是
10、现实世界中的一个实体, 其特性是: (1)每一个对象必须有一个名字以区别于其它对象; (2)用属性(或叫状态)来描述它的某些特征; (3)有一组操作, 每一个操作决定对象的一种行为。 在日常生活中, ”类”是对一组具有共同的属性特征和行为特征的对象的抽象。例如, 由一个个的人构成人类, 而一个人是人类的一个实例。 类和对象之间的关系是抽象和具体的关系。类是对多个对象进行综合抽象的结果, 对象又是类的个体实物, 一个对象是类的一个实例。 例如, 教师黎明和学生李明都是一个对象。 对象名: 黎明 对象名: 李明 对象的属性: 对象的属性: 年龄: 30 年龄: 20 学历: 博士 学历: 本科 职
11、称: 教授 性别: 男 专业: 计算机软件 专业: 计算机科学与技术 对象的操作: 对象的操作: 说自己的年龄 打篮球 吃饭 睡觉 授课 听课 一个个的像黎明这样的教师就构成教师类。一个个的像李明这样的学生就构成学生类。 我们前面说过, 面向对象程序设计更接近人们的思维。面向对象程序设计中的对象和就来源于现实世界。以面向对象程序设计的观点看, 一个对象是由描述其属性的数据和定义在其上面的一组操作组成的实体, 是数据单元和过程单元的组合体。类是对一组对象的抽象, 这组对象具有相同的属性结构和操作行为, 在对象所属的类中要说明这些结构和行为。一个对象是类的一个实例。有了类, 才能够创立对象。 现在
12、我们给出类的更精确的定义: 类是创立对象的样板, 它包含对创立对象的状态描述和对操作行为的说明。如果用面向对象的观点来分析银行账户问题, 着重点在银行账户上, 而不是在存款和取款的行为上, 那么, 银行账户就是一个类。 class BankAccount public: void MakeDeposit(float amount); float WithDraw(float amount); bool Transfer(BankAccount&to, float amount); private: float balance; float interestYTD; char * owner;
13、int account_number; ); 在BankAccount类中, 说明的行为是MakeDeposit、 WithDraw和Transfer。这些行为对于任何客户来说是很重要的, 她们在银行开户的目的就是要进行存款、 取款以及转账。正因为每一个顾客(作为类的实例)都可能做这些操作, 因此对行为的说明是public, 即公有的。同时, 在BankAecount类中, 还说明了数据balance、 interestYTD、 owner和accountnumber, 这些数据是私有的, 只能在被定义的类中进行操作。 作为银行账户类的实例, 银行的每一个银行账户都是一个对象。每个银行账户对象
14、有相同的结构和行为。因此, 任何一个银行账户对象都能够使用在类中说明的MakeDeposit等操作, 而且每个银行账户对象有相同类型的数据结构balance等。在面向对象程序设计中, 一个类只在源程序的代码中出现, 而并不会在一个正在内存运行的程序中出现, 即类只是在编译时存在; 对象作为类的实例在运行的程序中出现, 并占有内存空间, 它是在运行时存在的实体。因此一个类实际上是一种新的数据类型, 当我们要用一个新的数据类型时, 首先要在源程序中说明, 而说明部分的代码是不在内存中运行的。在程序中运行的是该类的对象, 对象在内存完成。注意, 我们在此必须严格区分说明和定义。前面我们用C+写的程序
15、段是对类BankAccount的说明。在c+的类中, 我们把那些行为称为成员函数, 而把数据称为数据成员。 122对象的状态 在面向对象程序设计中, 对象是类的实例。对象给类以生命, 类想要做的事必须经过建立对象和在对象上进行操作而实现。创立类的对象的过程也叫实例化对象。对象知道什么能做和什么不能做, 而且有能力修改和维护定义在对象上的数据。 能够将对象看成是一个带有状态和行为的活的实体。属于同一个类中的对象具有相同的行为, 可是有各自独立的状态。什么是对象的状态?在现实世界中一个对象能独立存在的原因是它们有各自的特征, 这些特征就是对象的状态。对于一个人来说, 姓名、 性别、 身高都是其状态
16、。在前面的讨论中, 对象的属性与对象的状态是相同的概念。这里, 给对象的状态下一个定义: 对象的状态是所有静态属性和这些属性的动态值的总和。 , 以银行账户为例, BankAccount类对象有一项数据成员balance(余额)。假设银行不允许透支, 那么每个账户的余额(balance)不应小于零。这是所有银行账户类对象的公共属性, 也能够说是任何一个银行账户类对象的静态属性。这类属性是不需要检测的。然而, 在BankAccount类对象的生存期的任何时刻, 账户中的余额是包含在balance这个数据成员中的数值。当在对象上发生存款、 转账、 取款等行为时, 会引起该数值的变化。因此, 账户余
17、额是一个动态变化的值, 换句话说, 数据成员balance的值是动态的。 对象的状态一般不但仅是初等的数据类型(整型、 实型、 字符型等), 而且许多对象将另一个对象作为它们状态的一部分。例如, 一辆车有发动机、 车轮、 座位, 发动机是另外一个对象, 它能够作为车的状态的一部分, 。又例如, 一个银行对象能够将银行账户对象和顾客对象作为它的状态的一部分。 一辆车 一个学校 发动机座位为位座位座位车轮车轮车轮学生学生教师教师123对象的交互 现实世界中的对象不是孤立存在的实体, 她们之间存在着各种各样的联系, 正是它们之间的相互作用、 联系和连接, 才构成了世间各种不同的系统。同样, 在面向对
18、象程序设计中, 对象之间也需要联系, 我们称为对象的交互。面向对象程序设计技术必须提供一种机制, 允许一个对象与另一个对象的交互。这种机制叫消息传递。 在面向对象程序设计中的消息传递, 实际是对现实世界中的信息传递的直接模拟。一个对象向另一个对象发出的请求被称为”消息”。消息是一个对象要求另一个对象执行某个功能操作的规格的说明, 经过消息传递才能完成对象之间的相互请求或相互协作。例如, 我们有一个银行账户对象和一个顾客对象, 顾客对象能够请求银行账户对象的服务, 如”存入300元”、 ”取出200元”等, 当银行账户对象接到请求后, 确定应执行的相应的操作并执行。 在此, 我们有必要介绍一下方
19、法的概念。方法是面向对象程序设计中的一个术语。我们知道, 属于一个类的对象具有相同的行为, 当某个行为作用在对象时, 我们就称对象执行了一个方法。方法定义了一系列的计算步骤。因此, 我们能够说一个对象请求另一个对象执行一个特定的方法, 或者说一个对象发送一个消息给另一个对象, 引起那个对象方法的执行。从这个意义上看, 对象的行为是负责响应消息并进行操作。 一般情况下, 我们称发送消息的对象为发送者或请求者, 接收消息的对象为接收者或目标对象。对象中的联系只能经过消息传递来进行。接收者只有在接收到消息时, 才能被激活, 被激活的对象会根据消息的要求完成相应的功能。 消息具有三个性质: (1)同一
20、个对象能够接收不同形式的多个消息, 做出不同的响应; (2)相同形式的消息能够传递给不同的对象, 所做出的响应能够是不同的; (3)消息的发送能够不考虑具体的接受者, 对象能够响应消息, 也能够不响应。 实际上, 对象之间的消息传递机制对应于面向过程程序设计的过程调用。消息传递并非真的传递信息, 它的实质就是方法的调用。只不过方法的调用受到消息的控制, 而过程调用是直接的。 消息的内容一般应包括接收者的名字、 请求的方法、 一个或多个参数。 由发送者向接受者发送一条消息, 就是要求调用特定的方法。所调用的方法可能引起对象状态的改变, 还可能会生成更多的消息, 而导致调用其它对象中的方法。 在面
21、向对象程序设计中, 消息分为两类: 公有消息和私有消息。假设有一批消息同属于一个对象, 其中一部分消息是由其它对象直接向它发送的, 称为公有消息; 另一部分消息是它向自己发送的, 称为私有消息。 公有消息与私有消息的确定, 与消息要求调用的方法有关。如果被调用的方法在对象所属的类中是在public下说明的, 则为公有; 是在private下说明的, 即为私有。当然, 私有消息只能发送调用属于它自己的方法。例如, 一个银行账户对象的类定义是BankAccount, 一个顾客对象能够发送一个公有消息WithDraw给银行账户对象, 要求取款。这个消息是公有消息。124类的确定与划分 我们知道, 面
22、向对象程序设计技术是将系统分解成若干对象, 对象之间的相互作用构成了整个系统。而类是创立对象的样板, 在整体上代表一组对象, 设计类而不是设计对象能够避免重复的编码工作, 类只需编码一次, 就能够创立所有的对象。因此, 当我们辑决实际问题时, 需要正确地进行分”类”。我们必须理解一个类究竟表示的是哪一组对象, 如何把实际问题中的事物汇聚成一个个的”类”, 而不是一组数据。这是面向对象程序设计中的一个难点。 例如, 考虑银行系统, 我们至少应该有两类对象: 顾客类和银行账户类。学校系统应至少包含两类对象: 学生和教师。 如何确定和划分类?类的确定和划分并没有统一的标准和固定的方法, 基本上依赖设
23、计人员的经验、 技巧以及对实际问题的把握。一个基本的原则是: 寻求一个大系统中事物的共性, 将具有共性的系统成分确定为一个类。 这里以模拟一个学校系统为例。系统想要达到的目标不同, 确定和划分的类就不相同。若模拟的目的是为了管理教学, 设置的类可能是学生、 教师、 教材、 课程、 教室、 图书等。若模拟的目的是管理后勤工作, 设置的类可能是宿舍、 食堂、 后勤工作人员、 教室、 图书馆等。 确定一个事物是一个类的第一步, 是要判断它是否有一个以上的实例, 如果有, 则它可能是一个类; 第二步, 我们还要判断类的实例中有否绝正确不同点, 如果没有, 则它是一个类。因为类的每一个实例是相似的, 具
24、有相同的行为和属性结构。 例如, 颜色(Color)与鲜花联系在一起只是鲜花的一种状态, 因为表示颜色的是一些值: 红、 黄、 紫、 白等, 因此, 在此Color不是一个类。然而, 如果我们把颜色同包括复杂颜色计算的图形处理系统联系在一起, 则Color是一个类, 因为这时的颜色是基于颜色三元素(红、 绿、 蓝)的成分比例和色度的变量, 它不但仅是一个数值, 还能够附带很多的行为。 不能把一组函数组合在一起构成类。也就是说, 不能把一个面向过程的模块直接变成类。如果简单地将模块中的函数变成成员函数而使其成为类是错误的。类不是函数的集合。 例如, 考虑一个包含一组数学函数的模块, 现在我们定义
25、一个类Mathhelper: class Mathelper public: double sqrt(double aNumber), double Power(double aNumber, int raiseto) double Inverse(double aNumber); private: /任何数据项, 也可能没有 ; 确定Mathelper为一个类是错误的。问题在于该类中没有需要管理的私有数据。用户只需要提供参数对成员函数进行调用。这与面向过程的程序设计的函数调用没有根本的区别。 设计类要有一个明确的目标。一个好的类应该是容易理解和使用的。我们不能设计一个Color类来表示鲜花的
26、颜色, 可是能够在图形处理系统中将颜色Color设计为类。因为在两个系统中对颜色的要求不同。13数据的抽象与封装 面向对象系统中最突出的特性是封装性、 继承性和多态性。我们首先来讨论封装性, 封装与数据抽象的概念密切相关。131现实世界中的抽象与封装 抽象和封装的概念在现实世界中广泛存在, 特别在科学技术日益发展的今天, 大量的电器被人们使用, 对电器的使用体现了抽象与封装的概念。 以录音机为例, 录音机上有若干按键, 当人们使用录音机时, 只要根据自己的需要, 如放音、 录音、 停止、 倒带等, 按下与之对应的键, 录音机就会完成相应的工作。这些按键安装在录音机的表面, 人们经过它们与录音机
27、交互。我们无法(当然也没必要)操作录音机的内部电路, 因为它们被装在机壳里, 录音机的内部情况对于用户来说是隐蔽的, 不可见的。这就是所谓封装的原理。 那么, 我们是如何知道放音按哪个键, 停止又按哪个键的呢?是录音机的操作说明书告诉我们的, 但操作说明书并不告诉我们录音机的内部将如何去做这些事。操作说明书在录音机做什么(what td do)与怎样做(how to do)之间做出了明确的区分。这就是所谓抽象的原理。抽象出来的是做什么, 而不关心如何实现这些操作。 以一般观点而言, 抽象是经过特定的实例或例子抽取共同性质以后形成概念的过程。抽象是对系统的简化描述或规范说明, 它强调了系统中的一
28、部分细节和特性, 例如做什么, 而忽略了其它部分, 例如如何做。抽象的描述被称为它的规范说明, 例如录音机的操作说明书, 而对抽象的解释称为它的实现。132数据的抽象与封装的基本概念 将上述观点用在数据结构上, 就不难理解数据的抽象与封装。将数据结构和作用于数据结构上的操作组成一个实体, 数据的表示方式和对数据的操作细节被隐藏起来, 用户经过操作接口对数据进行操作。对于用户来说, 只知道如何经过操作接口对该数据进行操作, 而并不知道是如何做的也不知道数据是如何表示的。这就是数据的封装。 数据的抽象则是经过对数据实例的分析, 抽取其共同性质的结果。数据的抽象和我们前面讨论的类的概念之间显然存在着
29、很强的相似性。 在面向对象程序设计中, 数据的抽象是在确定类时强调对象的共同点而忽略它们的不同点的结果。也能够说, 在一个类的说明中我们只表示那些主要特性, 而忽略次要的、 引不起我们兴趣的东西。 数据的封装则是隐藏了抽象的内部实现细节的结果。封装是将数据抽象的外部接口与内部的实现细节清楚地分离开。抽象和封装是互补的。好的抽象有利于封装, 封装的实体则帮助维护抽象的完整性。重要的是抽象先于封装。以银行账户类为例, balance、 interestYTD、 owner、 accountnumber等私有数据是被封装的数据, MakeDeposit、 WithDraw、 Transfer等成员函
30、数的细节也同时被封装, 用户看到的是MakeDeposit、 WithDraw、 Transfer操作提供的接口。 接口 实现细节( 不可见) MakeDepositWithDrawTransferBalanceInterestYTDOwneraccountnumberMakeDepositWithDrawTransfer用户不能存取的数据 在上面的讨论中, 我们并没有严格区分银行账户是类还是对象, 因为封装性不但涉及到类的描述, 也涉及到组成软件系统的对象。 从类的实例对象的角度来讨论封装似乎更合理, 因为类并不真正占有存储空间。封装的单位实际是对象, 可是对象的结构和行为是用它自己的类说明
31、来描述的。对象的封装比类的封装更具体化。 能够从下面几点来理解对象的封装: (1)对象具有一个清楚的边界, 对象的私有数据、 成员函数的细节被封装在该边界内; (2)具有一个描述对象与其它对象如何相互作用的接口, 该接口必须说明消息传递的使用方法; , (3)对象内部的代码和数据应受到保护, 其它对象不能直接修改。 从用户(或应用程序员)的观点看, 对象提供了一组服务, 并提供了请求服务的接口。从系统设计员的角度看, 封装能清楚地标明对象提供的服务界面, 而水乡的行为和数据是隐蔽的, 不可见。对象的这一封装机制, 能够将对象的使用者和设计者分开。133 对象的特性(1) 封装性。(2) 模块的
32、独立性。(3) 动态连接性。(4) 易维护性14 继承性141继承的概念孩子的父母孩 子哺乳动物热血、 有毛发、 用奶哺育幼子狗有犬牙、 食肉、 特定的骨骼结构、 群居柯利狗尖鼻子、 身体颜色红白相间、 适合放牧上图说明了哺乳动物、 狗、 柯利狗之间的继承关系。图中箭头方向志向基对象。哺乳动物是一种热血、 有毛发、 用奶哺育幼仔的动物; 狗是有犬牙、 食肉、 特定的骨骼结构、 群居的哺乳动物; 柯利狗是尖鼻子、 身体颜色红白相闻、 适合放牧的狗。在继承链中, 每个类继承了它前一个类的所有特性。例如, 狗具有哺乳动物的所有特性, 同时还具有区别于其它哺乳动物(如猫、 大象等)的特征。图中从下到上
33、的继承关系是: 柯利狗是狗, 狗是哺乳动物。”柯利狗”类继承了”狗”类的特性, ”狗”类继承了”哺乳动物”类的特性。 以面向对象程序设计的观点来看, 继承所表示的是对象类之间相关的关系。这种关系使得某类对象能够继承另外一类对象的特征和能力。 若类之间具有继承关系则它们之间具有下列几个特性: (1)类间具有共享特征(包括数据和程序代码的共享); (2)类间具有差别或新增部分(包括非共享的数据和程序代码); (3)类间具有层次结构。 假设有两个类A和B, 若类B继承类A, 则属于类B中的对象具有类A的一切特征(包括数据属性和操作), 这时, 我们称被继承类A为基类或父类或超类; 而称继承类B为类A
34、的派生类或子类。同时, 我们还能够说, 类B是从类A中派生出来的。 如果类B从类A派生出来, 而类C又是从类B派生出来的, 就构成了类的层次。这样, 我们又有了直接基类和间接基类的概念。类A是类B的直接基类, 是类c的间接基类。类c不但继承它的直接基类的所有特性, 还继承它的所有间接基类的特征。 对于动物继承链, 用面向对象程序设计的术语, 我们称”哺乳动物”是”狗”的基类, ”狗”是”哺乳动物”的派生类。”哺乳动物”、 ”狗”、 ”柯利狗”构成类的层次。”哺乳动物”是”狗”的直接基类, 是”柯利狗”的间接基类。 如果类B是类A的派生类, 那么, 在构造类B的时候, 我们不必重新描述A的所有特
35、征, 我们只需让它继承类A的特征, 然后描述与基类A不同的那些特性。也就是说, 类B的特征由继承来的和新添加的两部分特征构成。具体地说, 继承机制允许派生类继承基类的数据和操作(即数据成员和成员函数), 也就是说, 允许派生类使用基类的数据和操作。同时, 派生类还能够增加新的操作和数据。例如, 子女类能够从父母类继承房子和汽车, 当然能够使用房子和汽车, 还能够对房子进行再装修。继承的作用有两个: 其一, 避免公用代码的重复开发, 减少代码和数据冗余; 其二, 经过增强一致性来减少模块间的接口和界面。 继承使程序不再是毫无关系的类的堆砌, 而具有良好的结构。继承机制为程序员们提供了一种组织、
36、构造和重用类的手段。继承使一个类(基类)的数据结构和操作被另一个类(派生类)重用, 在派生类中只需描述其基类中没有的数据和操作。这样, 就避免了公用代码的重复开发, 增加了程序的可重用性, 减少了代码和数据冗余。同时, 在描述派生类时, 程序员还能够覆盖基类的一些操作, 或修改和重定义基类中的操作, 例如子女对所继承的房子进行装修。 继承机制以相关的关系来组织事物, 能够减少我们对相似事物进行说明和记忆的规模, 为我们提供了一种简化的手段。程序员能够将相关的类收集在一起, 生成高一级的、 概括了这些类的共性的类。具有适应关系的类处于一个继承层次结构中, 高层的类作为低层的类的抽象, 使程序员能
37、够忽略那些低层类的不同实现细节, 而按照高层类编写通用程序, 而且在掌握了高层类的特征以后, 能够很快地掌握低层类的特征, 给编程工作带来方便。142继承的分类 继承有两种分类方法, 一种是从继承源上分, 另一种是从继承内容上分。 从继承源上分, 继承分为单继承和多继承。 单继承是指每个派生类只直接继承了一个基类的特征。前面介绍的动物链, 就是一个单继承的实例。图15也表示了一种单继承关系, 即Windows操作系统的窗口之间的继承关系。 单继承并不能解决继承中的所有问题, 例如, 小孩喜欢的玩具车既继承了车的一些特性, 又继承了玩具的一些特征, 如图16所示。此时”玩具车”类不是继承了一个基
38、类的特性, 而是继承了”玩具”和”车”两个基类的特性, 这是一种多继承的关系。 多继承是指多个基类派生出一个派生类的继承关系, 多继承的派生类直接继承了不止一个基类的特征。 从继承内容上划分, 继承可分为取代继承、 包含继承、 受限继承、 特化继承。 取代继承: 例如徒弟从师傅那里学到的所有技术, 在任何需要师傅的地方都能够由徒弟来替代, 这就属于取代继承。 包含继承: 例如”柯利狗”继承了”狗”的所有特征, 任何一条”柯利狗”都是一条”狗”, 这就属于包含继承。 受限继承: 例如”鸵鸟”尽管继承了”鸟”的一些特征, 但不能继承鸟会飞的特征, 这就属于受限继承。 特化继承: 例如”运动员”是一
39、类特殊的人, 比一般人具有更多体育特长, 这就属于特化继承。143继承与封装的关系 在面向对象程序设计中, 对象具有封装性, 对象之间的联系只能经过消息传递来完成, 对象的私有数据和行为是被隐藏起来的。那么, 继承机制的引入是否削弱了封装性?继承与封装是否产生矛盾?回答是否定的。继承与封装不但投有实质性的冲突, 而且还有一定的相似性。 在面向对象系统中, 封装的单位是对象, 也就是说, 把一个属于某一类的对象封装起来, 使其数据和操作成为一个整体。如果该对象所属的类是一个派生类, 那么, 它只要把从基类那里继承来的操作和数据与自己的操作和数据一并封装起来, 就能够了。对象依然是封装好的整体,
40、依然只经过消息传递与其它的对象交互, 而不是直接调用。因此, 一个对象, 无论它是基类的实例, 还是派生类的实例, 都是一个被封装的实体。因此, 我们得出结论: 继承机制的引入并不影响对象的封装性。 从另一角度看, 继承与封装还有相似性, 那就是它们都提供了共享代码的手段, 因而增加了代码的重用性。 继承提供的代码共享是静态的, 派生类对象在成为活动的实体以后, 自动地共享其基类中定义的代码段, 从而使基类对象与其派生类对象共享一段代码。 封装提供的代码共享是动态的, 例如我们在一个类中说明了一段代码, 那么属于该类的多个实例在程序运行时共享在类中说明的那段代码。15多态性151什么是多态性
41、多态性也是面向对象系统的重要特性。在讨论面向对象程序设计的多态性之前, 我们还是来看看现实世界的多态性。现实世界的多态性在自然语言中经常出现。假设一辆汽车停在了属于别人的车位, 司机可能会听到这样的要求: ”请把你的车挪开”, 司机在听到请求后, 所做的工作应该是把车开走。在家里, 一把凳子挡住了孩子的去路, 她可能会请求妈妈: ”请把凳子挪开”, 妈妈过去搬起凳子, 放在一边。在这两件事情中, 司机和妈妈的工作都是”挪开”一样东西, 可是她们在听到请求以后的行为是截然不同的, 这就是多态性。对于”挪开”这个请求, 还能够有更多的行为与之对应。”挪开”从字面上看是相同的, 但由于用的对象不同,
42、 操作的方法就不同。 面向对象程序设计借鉴了现实世界的多态性。面向对象系统的多态性是指不同的对象收到相同的的消息时产生多种不同的行为方式。例如, 我们有一个窗口(Window)类对象, 还一个棋子(ChessPiece)类对象, 现在我们来考虑对它们都发出”移动”的消息, ”移动”操作在Window类对象和ChessPiece类对象上能够有不同的行为。 c+语言支持两种多态性, 即编译时的多态性和运行时的多态性。编译时的多态性是经过重载来实现的, 我们将在152介绍其概念。运行时的多态性是经过虚函数来实现的, 程序运行的到底是哪个函数版本, 需要在运行时经过对象发送的消息来确定。由于虚函数的概
43、念略为复杂, 而且涉及到c+的语法细节, 在此不做进一步的讨论。152重载的概念 重载一般包括函数重载和运算符重载。函数重载是指一个标识符可同时用于为多个函数命名, 而运算符重载是指一个运算符可同时用于多种运算。也就是说, 相同名字的函数或运算符在不同的场合能够表现出不同的行为。 下面我们给出一个函数重载的例子。 class A public: void Print(int i) 语句段1; void Print(float f) 语句段2; void Print(const char*c) (语句段3; 其它语句 ; 在上面的类定义中我们重载了三个函数, 名字都是Print。它们有各自不同的功能, 分别用语旬段1、 语句段2、 语句段3中的语句实现, 在此略去语句的细节。函数名相同, 而函数实现的功能不同。那么, 当有要求使用Print函数的消息发送时, 到底应该执行函数的哪一个呢?这就要看消息传递的函数参数是什么, 根据参数来调用不同的同名函数。例如, 发送的消息是Print(20), 则执行的是语句段1, 而发送的消息是Print(”welcome”), 则执行的是语句段3。 为什么要使用重载?使用重载的目的是为了更好地表示行为共享, 这种行为共享就像将相似的操作划分在一起。使用重载能够使程序员在只知道操作的