第二章 什么是数据抽象

● 我们的设计必须从客户角度出发, 不应让内部的细节打扰终端用户的使用。

● 数据抽象: 在忽略类对象间存在差异的同时, 展现了对用户而言最重要的特性。的确, 抽象应该对终端用户隐藏无关紧要的细节, 避免暴露有可能分散用户注意力或与使用环境毫不相干的细节

● 设计 , 或者说抽象, 就是把各个大小不一、功能各异的零部件组合起来, 在它们之间建立适当的通信机制。 进一步而言, 设计为用户提供一个明晰的接口, 将某物品的所有部件(即实现)隐藏, 只显示用户操作该物品所需的控件。


(1) 接口的含义


● 传统的面向过程编程思想认为, 接口和实现差异不大。 但是在面向对象编程中, 整个设计过程都围绕着接口进行

● 类对象的接口支持外部视图, 接口就是用户观察或感觉到的对象视图,以及用户可以用接口做什么(也包括接口对用户做什么)

● 当我们设计接口时, 应最大程度地满足用户的要求, 这些用户也称为客户。 所谓类的客户, 就是使用类且不知道(或不关心)类内部运作的人。 客户可创建类的对象, 并通过接口对其进行操作。


(2)什么是充足的接口


● 数据抽象的目的是, 提供清晰和充足的接口, 在方便且受控的模式下允许用户访问底层实现。 接口应满足用户使用对象的基本需求。 抽象的首要目标是, 为客户简化操作。

● 在大多数情况下, 如果现有接口无法满足用户需求, 用户都倾向于绕过所有的安全和保护, 直接干扰内部运作。 设计良好的接口不应该出现这样的问题。 当且仅当接口能满足用户需求时, 该抽象才是设计良好的抽象


(3) 实现的含义


● 接口告诉用户可以做什么, 实现则负责如何做, 所有的工作都在实现中完成。 客户无需了解类如何实现接口所提供的操作。 因此,实现用于支持由对象表现的接口

● 使用接口并不需要了解实现。 实际上, 使用借口的用户不了解内部实现反而会更安全些。部分(甚至完全) 了解实现可能导致黑客代码突破接口或实现的障碍。 另外, 单一接口可由不同实现支持, 不同接口可由单一实现支持。


(4)保护实现


● 传统的面向过程编程,缺乏对实现者的保护。

● 接口都由实现支持, 而且该实现由对应的接口来保护(即接口提供一个清晰且定义明确的方法访问实现)。 换言之, 实现以特定方式工作, 并跟踪自身的状态。 另外, 实现假设它的状态仅能通过接口更改, 如果违反此前提条件(既不知何故, 实现的状态被直接从外部更改, 并未通过提供的接口更改)。 则无法保证实现进行正确地操作。 从接口的角度看, 实现应通过公共接口运行(或由公共接口访问), 以确保实现的完整性。

数据抽象引出的了相关的概念: 数据封装。 只要存在由实现支持的带接口的对象, 就一定存在实现隐藏(也称为信息隐藏), 有些信息对实现而言相当重要, 但是对使用接口的用户而言却毫无价值, 这些信息将被隐藏在实现中。 实现由接口封装, 而且接口是访问实现的唯一途径

● 被封装的数据对于对象的实现极其重要。 进一步而言, 实现必须维护被封装信息的完整(或正确的状态)。


(5) 数据封装的优点


● 数据被封装后, 客户无法直接访问, 更不能修改, 只有接口函数才可访问和修改封装的信息。 进一步而言, 使用借口的用户完全不知道描述该接口的函数如何使用封装信息, 而且对象(或类)的用户对此也毫无兴趣。

封装的信息只能通过已发布的接口访问, 才能确保对象的完整。

● 数据封装的另一个优点是 实现独立。 改动封装内的数据不会(也不该)影响用户所见的接口。

● 接口十分了解实现和被封装的数据, 接口明白如何利用新的实现工作。 因此, 改变封装信息只会影响接口对封装数据的操作(实现), 不会影响客户所见的接口。 也就是说, 实现中的改动不应该影响该对象的客户所见的接口。


(6) 接口、 实现和数据封装之间的关系


这里写图片描述
这里写图片描述


(7) 数据封装注意事项


● 尽管封装的信息对于用户而言无关紧要, 然而, 为了高效地使用对象, 用户可能也需要访问封装的信息。 实现者封装某元素后, 必须在接口提供访问或操控封装信息的工具。 尽管这些访问是受限或受控的, 但仍然要给接口提供适当的工具用于访问和修改封装的实体。 如果抽象封装了一部分对用户很重要的信息, 却未提供合适的工具来访问被封装的信息, 这样的抽象是不正确的(即接口不足)。


(8) 确定封装的内容


这里写图片描述
这里写图片描述


(9)抽象数据类型


● 抽象数据类型是由程序员定义的新类型, 附带一组操控新类型的操作。 定义新抽象类型将直接应用数据抽象的概念。 抽象数据类型也称为程序员定义类型

● 语言定义类型(内置类型)的用户不能直接访问这些类型的内部表示, 而且必须使用语言提供的操作来操控他们

● 在面向对象编程语言中,函数与数据结构形成一个完整的单元, 必须使用该类提供的函数才能访问对象中的数据。 对象拥有数据结构, 且只允许接口访问它。 由此可见, 在支持OOP 的语言中更容易实现抽象数据类型。

● 利用数据抽象,我们创建了一个新类型, 并且为这个新类型提供了一组有用的操作。 因为语言没有直接支持这个类型, 所以程序员只好利用抽象实现它, 因此它是一个抽象数据类型。
鉴于此, 数据抽象有时也被定义为: 定义抽象数据类型以及一组操作并隐藏实现的过程。

注意: 我们希望让抽象数据类型也拥有和语言定义类型(内置类型)相同的特权和责任(也就是说,不应该让新类型的客户发现语言定义类型和抽象数据类型之间的任何区别)。 操作符重载, 并不是所有的面向对象语言都允许这样做。


(10)类的访问区域


● public 区域是最重要的区域, 为类的用户指定了类的接口。 任何客户都可以访问public区域。

● private 区域是任何客户都不能直接访问的区域,只供实现使用。 换言之, 只有类的实现才能访问private 区域。

● protected区域, 用于代码的扩展和复用(继承)。

● 在一个类中, 可以声明多个访问说明符, 编译器将负责合并。

● 在C++中, 其他类型(或声明)名称都不会有~,“~”符号(逻辑非运算符), 表示它为逆构造函数, 加上类名称来定义。 当某个类对象不再处于程序段的作用域内时, 该函数负责清理工作。

● 从一个函数(或块)中退出时, 编译器将自动销毁在该函数(或块)中创建的对象。 但是, 对象可能已经聚集了资源(动态内存、磁盘块、网络连接), 这些资源存储在对象的数据成员中, 由成员函数操控。 由于对象被销毁(退出函数)后不可再用, 因此, 必须释放该对象存储的资源

但是, 编译器并不知道这些资源(它们可能由动态分配),因此, 对象必须释放它们。 为了帮助对象完成这些工作, 在退出函数(或块)时, 所有在该函数(或块)中静态创建(既不使用 new()操作符创建)的对象都将调用析构函数。

析构函数将释放对象存储的所有资源。 换言之, 析构函数提供了一种机制, 即对象在被销毁前可自行处理掉自身存储的资源。

注意: 析构函数和构造函数都是特殊的成员函数。 在声明中, 它们无任何返回值类型, 这表明它们不能返回任何值。

● 复制构造函数: 用于通过现有对象创建新对象, 因而称为 复制构造函数。

当对象按值传递给一个函数时, 该对象的副本必须像内置类型那样被复制。 然而, 对象不是简单变量, 它们是由程序员实现的复杂实体。 因此, 编译器在复制对象时需要帮助。

逻辑上, 应该由对象的实现者负责复制对象(内置类型的实现者是编译器)。 复制构造函数就提供了这样的帮助。

无论何时需要对象的副本, 编译器都会调用复制构造函数来复制对象。 特别是, 当类在它的实现中使用动态内存时, 复制构造函数必不可少。 如果类的实现者不提供复制构造函数,编译器将会自动生成一个复制构造函数。

至于这个生成的复制构造函数是否满足类的要求, 那完全是另一个问题, 注意: 复制构造函数是一个特殊语义的构造函数。 我们在创建和初始化一个新对象(从无到有地创建)时调用普通构造函数, 再通过现有对象创建一个新对象时才调用复制构造函数, 这是复制构造函数与其他构造函数的主要区别。

● 出现下列情况时,将调用复制构造函数:

对象从一个函数按值传递至另一个函数时
对象从函数按值返回时
通过现有对象初始化一个新对象时

● 赋值运算符: 赋值运算符用于将现有对象显式赋值给另一现有对象。 赋值使用户显式完成的操作。

● 注意: 操作符重载是非常强大的机制, 很容易被滥用。 类的实现者必须为实现的类谨慎选择合适的操作符。 类只能使用它所支持(即实现)的操作符(赋值操作符, 即 = 操作符除外, 如果类的实现者未提供该操作符的实现, 需要时编译器将自动生成一个)。

● 严格意义上说,四则运算这四个运算符应作为非成员函数(或友元函数)实现。

● const成员函数: const应用于函数, 而非任何参数。 只有成员函数(非普通函数) 可以声明为const。 类的数据成员、函数的参数、对象、普通变量等 也都可以声明为const。

const成员函数保证在被调期间, 不会修改调用对象的数据成员。 它只能从对象中读取数据, 不能在对象的数据成员中写入(修改) 数据。

const函数中,编译器通过禁止给对象的数据成员赋值来确保对象的这种常量性


● 在C++中, 类的接口作为函数在该类中列出, 这样的函数称为成员函数。这样的函数提供类的接口, 因此也称为接口函数。

● 注意: 设计良好的接口绝不会把任何数据成员置于public区域。


(11) 识别成员函数的目标对象


● this指针是隐含在每个类成员函数的指针, 其指向正在操作此函数的类对象。 this指针的作用域是在类的内部, 当在类的非静态成员函数中访问类的非静态成员时, 编译器会自动将对象本身的地址作为一个隐含参数传递给函数。

● 对于类的非静态数据成员,即每个对象都有自己的数据成员,不过函数却都是每个对象共享的, 那么对象调用共享的成员函数是通过this指针找到自己的数据成员, 类的静态成员函数没有this指针。

● 类的每个成员函数都有一个特殊的指针——this。 这个this指针内含调用成员函数的对象的地址(即this指针总是指向目标对象)。 this指针只在成员函数内部有效,this是c++中的关键字。

● 在成员函数内部,this指针指向调用该成员函数的类实例。

● 在每个成员函数中都隐含了一个this指针作为函数参数, 并且函数调用时将对象自身的地址隐含作为实际参数传递到该成员函数中。

● 那每个成员函数接受的第一个参数就是this指针(但存在一些限制)。尽管程序员从未显式声明this指针,但是他一定存在。 this指针通常是每个(非静态)成员函数隐含的第一个参数, 编译器在每个成员函数的声明中都会插入这个隐含的参数。

● 注意 : 一旦离开成员函数, this指针将不再有效。

● 是否一定要使用this指针来引用目标对象中的成员?

不是所有的情况都需要这样做,只有在成员函数使用该类成员(数据成员和成员函数)的非限定名时,才意味着使用this指针。 如果在成员函数内部引用类的成员, 编译器会在每条表达式中均插入this指针(如果程序员没有这样做)

● this->m_member 表达式的意识是: this指针指向该对象中的m_member 数据成员, 所以说可以像使用成员函数的其他参数那样使用this指针。

就算在成员函数中没有显式使用this指针引用成员, 编译器也会将m_member 表达式自动展开为 this->m_member 表达式。


TInt aInt;
aInt.print();

对象aInt调用print()(即向对象aInt发送print()消息),在print()函数中this指针将指向aInt。

● 由于this指针是指向对象的指针, 因此,如果要使用this指针获得整个对象, 我们必须使用 * 操作符对this指针解引用为*this, 正如其他指针那样, this内部存放的是对象的地址, *this 则是该对象的值。


这里写图片描述

这里写图片描述


(12) 对接口的再认识


● 如果一个对象支持定义在TInt接口中的所有操作, 则该对象的类型就是TInt. 一个对象可能由许多类型,换言之, 一个对象可以支持(或响应)多个接口。 如果两个对象都支持相同的接口,则它们的类型相同, 但两者的实现可能完全不同。

对象的类和类型之间的区别?

对象的类定义如何实现对象,它定义对象的内部表示以及操作的实现。然而,对象的类型与实现无关——它只涉及对象可响应的操作集合。 一个对象可以有多种类型, 不同的类可以有相同的类型。 但是另一方面, 类和类型的关系十分密切。 类清晰地定义了该类对象可执行的操作, 因此, 类也定义了对象的类型。 类的任何对象都支持该类定义的接口。

● 接口不应该暴露实现细节。 精心设计的接口不应该要求客户了解实现的任何细节。 如果接口中暴露了某些实现细节(甚至是非常微小的), 一些用户就会期望获得所有的实现细节, 从而使得软件非常脆弱,毫无稳定性可言。


(13)类的不变式和断言


● 每个类都会在对象中包含一些恒为真的条件, 无论对象调用任何成员函数, 这些条件都必须为真。 这样的条件称为不变式。

● 除这些类不变式之外, 成员函数可能会包含其他条件, 在执行代码前必须保证这些条件为真, 这些在操作开始被调用之前必须为真的条件,称之为 前置条件。

● assert宏: 该宏接受一个表达式, 而且必须判断表达式的真假。 若真, 则继续执行, 若假, 程序停止, 并显示错误消息表明断言失败。

● 一旦成员函数完成它的操作, 将会执行某些条件必须为真的断言。 换言之, 如果成员函数成功执行完毕, 它将生成一个满足某些条件的结果, 这样的条件被称为后置条件。


(14) 面向对象设计的表示法


● 我们需要一种表示法可以代表类、对象、类关系、状态图、进程图、对象关系等。
可以使用 Booch表示法和UML

Booch 表示法

● 对象用实线不定形表示, 对象场景图在描述解决方案的状态时非常有用。

Booch 中类的关系

● 发现类并建立类之间的关系, 是分析和设计过程中最重要的阶段。

● Booch 方法论在类之间使用两种主要关系—— is-a关系和has-a 关系, 关系的完整列表如下:
● 关联、继承(is-a)、 聚集(has-a)、使用、元类。

关联

● 关联用于分析的早期阶段, 最终会成为has-a 、is-a 或者“使用” 关系。 关联是一个双向关系。 关联的两端的数字代表基数,

聚集(has-a)

● “has-a” 关系可以通过指针、引用或者甚至通过包含的对象来实现,也就说“has-a” 并不意味着每个类对象 在物理上包含许多其他的类对象(尽管也可能这也实现)

● 空心圆圈0 表示“使用” 关系, “使用”关系并不意味着“has-a” 关系

“has-a” 关系是“使用” 关系的超集, 而且“has-a” 比“使用”关系的功能更强大

● 在问题的解决方案中,使用” 关系 并不常见。

● “is-a” 关系用于表示继承。

● 大多数项目都不可能在一张纸上显示出该项目涉及的所有类。 为帮助分组(和建模)类, 我们使用类范畴。 类范畴通过包含在其中的类提供服务。 类范畴有自己唯一的名称(类似于类)

● 要多整个系统进行高级描述, 只能用类范畴。 类范畴的分解图必须显示其中包含的所有类, 以及这些类之间的关系。


(14) UML中类的关系


● 统一建模语言在类之间主要使用两种关系 : 关联 和 泛化 (继承)


关系


  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值