《C++面向对象高效编程(第2版)》——1.8 深入了解对象

1.8 深入了解对象

C++面向对象高效编程(第2版)
如前所述,对象是类的实例,对象赋予类生命。换言之,可以通过实例化一个对象,并对其进行操作,体验类可以完成的功能。对象很聪明,它知道可以做什么,不可以做什么。另外,对象还知道如何修改和维护它的数据成员。

因此,区别类和对象是一个逻辑问题。简单地说,对象是带有状态和行为的活的实体。所有类对象的行为都定义在类中,而状态则由对象单独维护。状态和行为这两个词非常简单,但应用于对象上却意义深远。

1.8.1 对象的状态

为了讨论对象的状态,我们再回顾一下银行账户问题。每个BankAccount类对象中都有一个balance数据成员。假设我们不允许客户的账户透支,那么,只需声明账户中的余额不允许小于0。这是任何银行账户对象的已知性质(property)1,我们不必检查对象的状态来确认这个属性。也就是说,这是每个银行账户对象的静态性质(static property)。

然而,在BankAccout类对象生存期内的任何时候,账户中的余额都是balance数据成员中的值。该数据成员的值随着账户的存款、转账、取款不断变化。因此,账户余额是一个动态变化的值。换言之,balance数据成员是一个动态值。对象的状态是所有静态性质以及这些静态性质的动态值的集合。性质是一个对象独有的特征(feature)或质量(quility)。例如,注册号可以作为汽车的一个性质,每辆汽车都有一个注册号(其值是不同的);与此类似,名字可以作为人的一个特征,每个人都有一个名字(尽管不是唯一的)。

对象的状态不仅仅是(通常不是)简单的数据类型。许多对象中还包含其他对象作为自身状态的一部分。例如,Car类对象会包含Engine类对象,作为自身状态的一部分;而Bank类对象会包含BankAccount类对象和Customer类对象,作为它状态的一部分(见图1-5)2。

1.8.2 对象状态的重要性

你可能会疑惑,为何要如此关注被封装对象中的数据部分?因为对象如何响应我们的命令(操作)以及对其他对象(客户)做什么,都直接依赖于对象的状态;执行某方法所

图1-5

得的结果也直接依赖于对象的状态。例如,向BankAccount类对象发送WithDraw消息时,将依次发生以下步骤:

(1)检查核实账户是否属于调用操作的人。

(2)如果请求的总额大于当前余额,则打印错误消息,并返回调用程序。

(3)否则,从余额中减去提款数额,并返回。

以上的每一个步骤都需要知道对象状态的信息,每种方法都依赖于对象的状态。这些方法都假定对象的状态是正确的。如果对象的状态不正确(由于一些未知原因),对象的行为将无法预测。

洗衣机可以作为讲解状态的另一个例子。当我们按下“WASH”按钮时,机器利用对象中的某些数据成员检查门是否关闭(可能还包括检查是否装入了衣物)。如果未关门,设备将不会运转。大多数洗衣机利用传感器(一个简单的开关)来检查门的状态,用户无法直接操控这个开关(它是一个封装的数据成员)。如果一个冒失的用户不小心接触到这个开关,并操作此开关,那么洗衣机将被蒙蔽,(错误地)相信门已经关闭。如果现在用户按下“WASH”按钮,洗衣机肯定会运转,也许还会混合洗涤剂、衣物和水。之所以出现这样不可预测(或不希望出现)的行为,是由于对象的状态被非法改动。一个设计良好的类的实现不应该允许客户直接访问对象的状态。状态只能由成员函数修改,而客户只能通过操作对象来使用成员函数。同理,如果用户在未关门的情况下按下COOK按钮,微波炉会出现什么情况?微波炉不会启动。因为微波炉在启动磁电管(产生微波的设备)之前会检查门是否关闭,如果门未关闭,则不会启动设备。这与洗衣机中使用传感器确认门是否关闭非常类似。但是,如果客户可以操控这个开关并执行关闭(并未真正关门),然后按下COOK按钮,即使微波炉的门实际上未关闭,微波炉也会启动磁电管。因为它相信门已经关上了,这可能会对站在微波炉附近的人造成无法弥补的伤害。

1.8.3 谁控制对象的状态

如前所述,对象的状态通过成员函数修改。然而,并不是所有的方法(成员函数)都可以修改对象的状态——一些方法仅允许使用状态中的值(如BankAccount中的GetBalance)。类中的每一个方法都会对对象的状态进行一定程度的假定,这样的假定可在文档中说明,也可在代码中说明。而且,类假定无法从外部修改对象的状态只有在成员函数内才能修改对象的状态。成员函数非常了解用对象的状态值可以做什么,也非常清楚如何改变对象的状态值,正是成员函数控制了对象的状态3。注意,成员函数代表客户执行操作,这点很重要。客户调用一个操作(即向对象发送请求),操作便完成一些有意义的工作。通常,方法由客户启动4,它不会自己执行。

成员函数也了解对象状态的约束(或限制)。再次以BankAccount类为例,BankAccount类中的所有方法都清楚对账号的约束,即任何账号中的余额都不能小于0。该类中的每一个方法都强制执行这个约束。如果对象的状态不是从成员函数内部进行修改,那么对象的行为将无法预测,这就是洗衣机例子中未关上门就能启动的问题。再举一例,如果有人非法侵入他人的银行账户,将其balance设置为0,则该账户真正的持有者将无法从账户中提款,因为该账户的状态显示余额为零。语言无法阻止这种恶意地侵入,但是,它可以防止出现意外错误。这种保护通过将balance数据成员设置为私有来完成。换言之,所有不让普通客户访问的数据都应封装在类的private区域中。这也称为数据封装(data encapsulation)。

创建类时,一定要为其进行封装。为确保正确的行为,类的对象需要一些内部信息。没有任何数据成员的类(不是抽象基类)是糟糕的设计,它说明该类创建的对象没有任何状态。我们将在第2章详细讲解数据封装。

1.8.4 对象的行为

客户通过类的对象使用方法来进行有意义的操作。对象的行为在某种方式上是对客户调用消息的响应。行为是对象对消息采取的行动和做出的反应。消息会引起状态的变化,也会引起发送更多的消息至其他对象,或两者兼有之。当客户向对象发送消息时,为了完成操作,该对象可能向另一个对象发送其他消息。例如,BankAccount类对象在收到Withdraw消息时,为了记录当前的交易,可能要向TransactionLogger类的对象(假设为tl)发送消息;为了保存当前交易,对象tl可能还要向数据库(也许位于其他城市)发送消息。很明显,向对象发送一个消息,可能引起向其他对象发送别的消息,还可能会出现其他对象发送某个消息至原始对象(甚至是递归地)。行为在对象对消息作出响应时,记录外部可见的动作。这就是客户从外部所感知的情况。

有些消息可能会引起状态的变化,有些可能不会。在C++(和Eiffel)中,可以清楚地识别不会引起任何状态变化的消息5。在类的文档中,必须为每个方法都清楚地记录该消息能完成什么任务(从客户的观点来看)。作为设计者,我们这样设计的目的是,在不暴露类实现细节的前提下,为客户尽可能多地提供类的信息。

时间: 2024-11-03 05:09:52

《C++面向对象高效编程(第2版)》——1.8 深入了解对象的相关文章

《C++面向对象高效编程(第2版)》——4.2 无用单元收集问题

4.2 无用单元收集问题 C++面向对象高效编程(第2版) 在我们讨论无用单元收集1(garbage collection)之前,先了解一下何为无用单元(garbage),何为悬挂引用(dangling reference). 4.2.1 无用单元 所谓无用单元(garbage),是一块存储区(或资源),该存储区虽然是程序(或进程)的一部分,但是在程序中却不可再对其引用.按照C++的规定,我们可以说,无用单元是程序中没有指针指向的某些资源.以下是一个示例: main() { char* p =

《C++面向对象高效编程(第2版)》——4.6 对象赋值的语义

4.6 对象赋值的语义 C++面向对象高效编程(第2版) 赋值与复制的操作非常类似.在C++中,绝大多数的复制操作都由语言隐式调用(当对象按值传递或按值返回时).当通过现有对象创建新对象时,也进行了复制操作(但不是很频繁).与复制相反的是,赋值是必须由程序员显式调用的操作.然而,在Eiffel和Smalltalk中,赋值和复制操作都由程序员显式调用.这也是基于值的语言与基于引用的语言之间的区别. 在C++中,对于对象和基本类型赋值都具有相同的含义.把基本类型变量赋值给另一个(兼容的)基本类型变量

《C++面向对象高效编程(第2版)》——2.21 确保抽象的可靠性——类不变式和断言

2.21 确保抽象的可靠性--类不变式和断言 C++面向对象高效编程(第2版) 任何抽象都必须与客户履行它的契约(contract).当客户使用类时,他希望类的对象像其发布描述的那样运行正常.另一方面,类的实现者必须千方百计地确保对象运行正常.但是,类只有在客户履行自己那部分契约后,才能正确行使它的职责.例如,类的成员函数可能要求传入的参数为非零指针(non-zero pointer).只有满足此前提条件,成员函数才能保证它的行为.因此,客户必须履行一些义务.换言之,如果客户履行了她那部分契约,

《C++面向对象高效编程(第2版)》——2.30 has-a关系的重要性

2.30 has-a关系的重要性 C++面向对象高效编程(第2版) "has-a"关系(也称为关联.聚集.包含.组合)是在OOD(面向对象设计)中频繁使用的重要关系.但是,许多设计者和程序员都没有很好地理解其相关性,从而导致复杂僵化的设计和不必要的继承. 在OOD阶段,软件的复用主要通过两种方式完成:继承和包含.它们各有优缺点.优秀的设计者了解它们的局限性.优点和代价,可以灵活自如地应用它们.继承将在第5章.第6章以及第二部分的第12章中详细讨论. 包含是一项强大的设计技术,它比继承更

《C++面向对象高效编程(第2版)》——2.27 关联

2.27 关联 C++面向对象高效编程(第2版) 关联表示对象与不同类之间的结构关系(structual relationship),大多数关联都是二元关系(binary relation).类之间的多重关联(multiple association)和类本身的自关联(self association)都是合法的(见图2-19). 关联可以有一个名称,表明阅读方向的箭头为可选.注意,方向箭头为可选,但关联名必须显示.关联在不同的方向可以有不同的名称,但是,大多数情况下,没必要注明(特别是在已标出

《C++面向对象高效编程(第2版)》——3.2 类要素的细节

3.2 类要素的细节 C++面向对象高效编程(第2版) 3.2.1 访问区域 客户可以访问在类的public区域中声明的任何成员.我们可以把该区域看做是通用公共(general public)的接口,它没有任何保护,是类限制最少的区域.一个设计良好的类绝不会将数据成员包含在public区域,该区域只能包含成员函数.如果在public区域包含数据成员,那么无需类的实现者,仅通过编译器即可访问这些数据成员.这违反了数据抽象和封装原则.这也是我们为什么总将数据成员放在private或protected

《C++面向对象高效编程(第2版)》——第2章 什么是数据抽象

第2章 什么是数据抽象 C++面向对象高效编程(第2版) 面向对象编程的一项基本任务是创建带有适当功能的类,并隐藏不必要的细节(即抽象数据).下面,我们将用一个现实生活中的例子来解释数据抽象的概念. 绝大多数人都见过影碟播放机(laser disc player)(或LD播放机).现在,提出一个简单的问题:设计一个影碟播放机,要求易于使用和修改,可后续添加更多有用的功能. 注意: 如果难以理解影碟播放机,可以用CD播放机代替LD播放机,其设计原理类似.实际上,影碟播放机的功能是CD播放机功能的超

《C++面向对象高效编程(第2版)》——1.5 什么可以作为类

1.5 什么可以作为类 C++面向对象高效编程(第2版) 用简单的例子详细讨论类和对象非常容易,但是难点在于如何为给定的问题找出合适的类.我们必须理解类代表什么,何时将问题中的某些部分转化为类,而非数据,反之亦然.根据我们的定义,类拥有一组对象的共同属性(或者特性).怎样的共同才是共同?何时说这是一个类,而不是另一个类的对象?这些都是我们在学习OOP时会遇到的,和真正关心的问题. 当我们决定创建一个类时,第一个问题就是"是否确实需要这个类的多个实例?",如果答案为"是&quo

《C++面向对象高效编程(第2版)》——4.3 C++中的无用单元收集

4.3 C++中的无用单元收集 C++面向对象高效编程(第2版) C++提供类的析构函数专门处理无用单元收集,但是,这并不意味着无用单元收集只发生在析构函数中.实际上,某些其他成员函数也必须考虑无用单元收集. 类的析构函数给予对象最后一次机会释放它所获得的所有资源.在退出某作用域之前,由语言自动为在该作用域中创建的自动(基于栈)对象调用析构函数.此时,对象即将被销毁(也就是说,被对象占用的内存即将被系统回收).一旦析构函数完成,对象将彻底地消失. 删除(使用delete操作符)指向某对象的指针时

《C++面向对象高效编程(第2版)》——2.10 抽象数据类型—栈的实现

2.10 抽象数据类型-栈的实现 C++面向对象高效编程(第2版) 下面的示例用于说明,在C中一个简单栈的实现. Stack.h文件--让所有的抽象数据类型用户都可以使用Stack. typedef Stack* Stackld; typedef int bool; struct Stack { int data; / 在栈上存储元素 */ unsigned count; / 栈上元素的数量 / int top; / 栈顶部的指针 */ / 略去其他细节 .../ };``` Stack.c文件