第04章 面向对象程序设计语言 4.1 Smalltalk语言 第04章 面向对象程序设计语言 4.1 Smalltalk语言 对象的思想最早源于人工智能研究,60年代末描述智能对象的框架(frame)即封装了许多槽(slot),槽既可以是属性(数据)也可以是行为(操作)和(约束)。但最早见诸文献是sketchpad提到的OO图形学(1963)。 60年代挪威的Dahl和Nyard为模拟系统研制了SIMULA-67语言,首先提出封装的类和动态生成实例对象的概念。 60年代末,美国犹他大学Alan Kay到Xerox公司PaloAlto研究中心参加了Dynabook项目。该项目的硬件是Star(个人机的前驱)软件是Smalltalk。 1972年Dan Ingalls完成Smalltalk-72第一个实用版,以后又经过-76-80两次改进,Smalltalk-80成为向外发行的正式版本。
4.1 Smalltalk语言 对象的思想最早源于人工智能研究,60年代末描述智能对象的框架(frame)即封装了许多槽(slot),槽既可以是属性(数据)也可以是行为(操作)和(约束)。但最早见诸文献是sketchpad提到的OO图形学(1963)。 60年代挪威的Dahl和Nyard为模拟系统研制了SIMULA-67语言,首先提出封装的类和动态生成实例对象的概念。 60年代末,美国犹他大学Alan Kay到Xerox公司PaloAlto研究中心参加了Dynabook项目。该项目的硬件是Star(个人机的前驱)软件是Smalltalk。 1972年Dan Ingalls完成Smalltalk-72第一个实用版,以后又经过-76-80两次改进,Smalltalk-80成为向外发行的正式版本。
4.1.1 Smalltalk系统 ·语言核心(Kernel) ·程序设计系统 ·程序设计范型(Paradigm) ·用户界面模型(User Interface Model)
4.1.2 用户界面模型 系统工作空间(System WorkSpace) 工作空间(WorkSpace) 系统副本(System Transcript) 项目(Project) 两种图形编辑窗(Form和Bit)
系统浏览器(System Browser)窗 用户就是按浏览窗中显示的模板填写程序。
4.1.3 语言核心 (1) 保留字 只有五个nil,true,false,self,super (2) 字面量 字符字面量 /数字面量 / 符号字面量 / 数组字面量 (3) 限定符和特殊符号 " ' $ # #( ) , ; : | :=或← ↑ [ ] ( ) {} (4) 变量 实例变量 / 类变量 / 临时变量 / 全局变量 / 汇聚变量 / 参数
tree class 消息class 发向tree,得到tree的类。 0.3 sin 消息sin 发向0.3,得sin(0.3) (5) 消息表达式与语句 消息表达式的一般格式是: 对象 选择子 参数 Smalltalk的消息表达式有三种: ·单目的 不带参数 tree class 消息class 发向tree,得到tree的类。 0.3 sin 消息sin 发向0.3,得sin(0.3) Array new 消息new 发向Array,创建-Array 的实例 对象 选择子-参数
· 双目的 3+4 消息‘+’带参数4发向对象3,得对象7。 100@ 50 消息‘@’带参数50发向对象100,得(100,50) (sum/count) * reserve amount 双目,括号优先 单目优先 双目
·关键字消息表达式 用关键字(带有‘:’的选择子)描述的双目表达式,也是自左至右释义。 anArray at:3 put:100 finances totalSpentOn:‘food’ ·赋值 变量在不同时间可赋以不同对象,任何表达式加上赋值前缀‘←’ quantity←19. name←‘chapter 1’。 foo ← array at:4。 数组第4元素与‘foo’同名
·块表达式 [:x:y|BicPen goto:x@y] [:x:y|BicPen goto:x@y] value:100 value:250 BicPen goto 100@ 250 |aBlock| aBlock←['This is a String' displayAt:500@ 500]. Display white. aBlock value
(6) 控制结构 条件选择一般形式是: 布尔子表达式 ifTrue:[‘真’块执行] ifFalse:[‘假’块执行] “可以不出现” 如:number<0 ifTrue:[absValue←number negated] ifFalse:[absValue←number] 条件重复一般形式是: [布尔块表达式] whileTrue:|wlieFalse:[重复块] 如:[index>listSize] whileFalse:[list at:index put:0。 index←index+1 ]
(7) 消息/方法 消息模式 | 临时变量 | 语句组 nawAt:initialLocation|newBox| 消息模式 | 临时变量 | 语句组 nawAt:initialLocation|newBox| newBox←self new. newBox setLoc:initiaPLocation tilt:0size:100 scribe:pen new. newBox show. setLoc:newLoc tilt:newTilt size:newSize seribe:newScribe| | Loc←newLoc. titl←newTilt. size←newSize. scribe← new Scribe
4.1.4 Smalltalk文件系统与虚机 Smalltalk是编译—解释执行的,Smalltalk源程序经编译器得到虚映象(Virtual image),虚映象由字节代码中间语言编写,由Smalltalk虚机解释执行。相应的文件系统管理三种文件:源文件、变更文件、映象文件。 由于Smalltalk是交互式的,被编译的方法在执行期间出了问题要反应到源程序,则要对映象文件施行反编译(decompliation) Smalltalk的虚机是一个新软件,它有三个功能部分: ·存储管理器 ·虚映象解释器 ·基本例程 用汇编码写出的底层方法实现
4.1.5 Smalltalk程序设计范型 程序设计在类的层次上进行,由类静态(于工作空间指明向类发出消息)或动态(方法运行时)生成实例对象。每个对象当接受某消息并执行其方法的消息表达式时都是在自向其它对象发消息。
4.1.5.1 一个简单的Smalltalk程序 统计字母出现频率 |s f| “定义了两个临时变量” s←Prompter prompt:‘enter line’ default:‘’. “s是Prompter的实例,将关键字表达式的结果束定于s” “意即输入一行字符串,若不输入,S为空串” f←Bag new. “f是Bag的实例” s do:[:c|c isLetter ifTure:[f add:c asLowerCase]] “s在Prompter中找方法do:的模式,若找不到,找prompter的” “父类直到Object. C是块变量,意从S中拿出某字符,isLetter” “是消息模式,判C是否字符,若为真执行内块”。 “内块中f找add:消息模式,从Bag直至上层父类,找到先执” “行右边子表达式”。 c asLowerCase是单目表达式,同样要在Prompter中找asLowerCase匹配,也是不成向上找。它返回是“第k个”小写字母,add:把它发送到对象f的第k个位置上并与原数相加。 ↑f “返回f中的值”. 这个程序一共四句。如果掀鼠标使菜单项‘doit’工作并输入: “Smalltalk is a programming Language for developing soluions to both simple and complex problem.” 则输出的f值是: 7 1 1 2 4 1 5 1 5 1 7 4 4 7 3 3 6 3 2 1 a b c d e f g h i k l m n o p r s t u v
例 字频统计对比程序 Pascal Smalltalk PROGRAM Frequency “无消息模式方法,宜写算法” CONST Size=80; VAR s:string[size]; | s c f k | k,i:Integer; “定义了四个临时变量” c:Char; f:ARRAY[1..26] OF Integer; f←Array new:26. BEGIN “f是Arrey实例长度26” Writeln(‘enter line’); s←Prompter ReadIn(s); prompt:‘enterline’ FOR i:=1TO 26 DO default:‘’. f[i]:=0; “S是Prompter的实例,装输入字串” FOR i:=1 To size DO 1 to:26 do:[:I|f at:I put:0]. BEGIN 1 to:size do:[:I| c:=aslowerCase(s[i]); c←(s at:i) asLowerCase. if isLetter (c) THEN c isLetter ifTrue:[ BEGIN k←c asciiValue k:=ord(c)-ord(‘a’)+1; -$a asciiValue + 1. f[k]:=f[k]+1 f at:k put:(f at:k) + 1 END ] END; ]. FOR i:=1 To 26 DO ↑f Write(f[i],‘’) END.
4.1.5.2 类协议
4.1.5.3 一个完整的Smalltalk程序 家庭财务帐目 建立全部流水帐类,直接挂在Object上 class name FinancialHistory superclass Object instance variable names 'caseOnHand incomes expenditures' category 'Financial Tools' class method initialBalance:amount| | “建立流水帐本初始为amount(元)” ↑super new setinitialBalance:amounT new | | “建立流水帐本初始为0(元)” ↑super new setinitialBalance:0 instance method receive:amount from:source | | incomes at:source put:lself total ReceivedFrom:source)+amount. “从来源source接收到的钱数,因而手头现金增加”. cashOnHand←cashOnHand + amount. incomes changed spend:amount for:reason | | “为事由reason支付的钱数,因而手头现金减少。” expenditures at:reason put:(self totalSpentFor:reason) + amount.
totalReceiveFrom:source | | “回答自source收钱总数” cashOnHand← cashOnHand - amount. expenditures changed CashOnHand| | “回答当前手头现金” ↑ cashOnHand expenditures | | “回答支出细目” ↑ expenditures incomes | | “回答收入细目” ↑ incomes totalReceiveFrom:source | | “回答自source收钱总数” (incomes includesKey:source) ifTrue:[↑incomes at:source] ifFalse:[↑0] totalSpentFor:reason | | “回答在reason项上总支出” (expenditures includesKey:reason) ifTrue:[↑expenditures at:reason] private SetlnitialBalance:amount| | “实例变量初始化” cashOnHand←amount. incomes←Dictionary new. expenditures←Dictionary new
Smalltalk at:# HouseholdFinances put:nil. HouseholdFinances←FinancealHistory initialBalance:1560 HouseholdFinances spend:700 for:'rent'. NouseholdFinances spend:78.53 for :'food'. HouseholdFinances receive:820 from:'pay'. HouseholdFinances receive:22.15 from:'interest'. HouseholdFinances spend:135.65 for:'utilities'. HouseholdFinances spend:146.14 for:'food'.
4.1.6 Smalltalk程序设计系统 在Smalltalk中,系统支持程序也是作为类挂在Object之下,包括算术运算、数据和控制结构的实现、输入/出、随机数生成器等。 有一些类是辅助程序设计过程的,语法分析器、编译器、解释器、反编译器这些对象的方法都有源代码,目标码两种形式。 还有一些对象表示类和方法的结构,以便程序员追踪系统。 还有将方法和向其发消息的对象联结起来的对象.这些对象统称环境(contexts)类似其他语言实现中的堆栈帧和活动记录。
4.2 Smalltalk的对象、类、方法的实现 类的存储
实例对象的存储 实例对象只存放数据,其存储格式如下图: 实例对象的存储 实例对象只存放数据,其存储格式如下图:
活动记录 ·环境部分 ·指令部分 ·发送者部分
4.3 面向对象 为什么需要面向对象? OO 语言的发展 面向对象的基本概念 封装和继承 初始化和终结处理 动态方法约束
重用的问题 实践中人们认识到重用已有开发结果的重要性,提出了软件重用的概念 · 最早的重用单元是子程序,如 Fortran 的子程序库 · 子程序是纯粹的过程抽象,基于子程序的重用有很大局限性 · 模块是更合适的重用单元,因为模块可以包装任何功能,更灵活 重用中有一种常见情况:软件开发中遇到的新问题常与解决过的问题(可以重用的库提供的功能)类似,但又不完全相同 · 已有模块的功能与需要有差异,无法以其“现有”形式直接使用 · 如果模块功能的改变只能通过修改源代码的方式进行,程序员就只能拷贝这个模块的源代码,深入研究后再设法修改,以满足新需求 但问题是有没有可以使用的源代码?常常没有: · 模块可能是购入的,提供商不提供源代码 · 模块可能是过去的遗产,源代码已经丢失或部分缺失
重用和软件开发 即使有源代码,基于修改代码的方式重用,也有很多问题: 总之,基于修改代码的重用,重用的价值大大降低了 · 修改代码的代价可能很大(需要理解开发者的想法、设计和大量细节) · 修改代码很容易引进错误 · 经过修改的代码,其可读性、易理解性和易维护性都会恶化,多次修改导致这些性质不断恶化,可能给整个系统的质量带来严重危害 · 修改后的模块,继续重用的可能性更小 总之,基于修改代码的重用,重用的价值大大降低了 在软件开发过程,重用也是非常有价值的 · 在同一软件中重复使用某些部分,可使重要设计决策得到集中处理 · 提高重用比率可能减少重复开发工作量 · 对重要基础功能的深度优化可能非常耗工耗时,重用已有的经过精心调整的代码,可能大大提高系统的性能
模块和程序组织 常规的程序单元缺乏弹性,定义好的子程序/模块都是固定功能的实体,难以提供“定制”的方式部分地改变功能以满足实际需要的变化 通过模块定义的抽象数据类型是相互独立的,不同模块之间无任何关系 而实际情况中,常常需要定义和使用一些相互有关的类型,可能需要把它们送给同一个函数/过程去处理,以同样方式存储 变体和联合机制就是为了迎合这方面的需要,但它们没有类型安全性,且未能提供解决类似问题的统一框架,难用于应付更复杂的情况 支持相关类型,可能给程序的结构组织带来新的可能性 如何在抽象数据类型的框架中提供这一类功能,也是需要解决的问题 面向对象的概念在这些方面都能发挥很大的作用 面向对象(Object-Oriented)的方法和程序技术,为基于模块(一个类也可以看作一个模块)的重用问题提供了一条解决途径。
面向对象和重用 面向对象技术的最重要能力,在于使程序员比较容易以一种外部附加的方式,在已有数据抽象的基础上定义新的数据抽象 OO 也支持定义有弹性的操作框架,使新的数据抽象可以使用这些框架,并把针对该类抽象的实例的具体操作插入框架中(重用和调整) 新定义的抽象可以继承原有抽象的行为,也可以根据需要调整改变已有功能的行为,或者添加新抽象所需要的新行为 这样大大提高了代码重用的可能性(目标是实现真正不加修改的重用。当然,实际的重用可能性还与具体数据抽象的设计有关) 面向对象还有另外的许多重要价值(有些可能同样重要或更重要),并由此发展出“面向对象的系统分析”,“面向对象的设计”等 面向对象思想对于软件领域的影响是全面的,是结构化思想(结构化程序设计,结构化分析,结构化设计等)之后软件开发领域中的又一次革命 注意:面向对象并没有取代结构化,应该看作是在另一层次上的抽象
OO 发展史 OO 技术和思想中的一个基本方面是数据和操作的封装 · 这方面的基本想法:一组数据与关联之上相关的操作形成一个对象。其内部数据构成对象的状态,操作确定对象与外界交互的方式 · OO 并不是从模块化程序设计发展出来的,它有自己的发展历程 · OO 的思想与模块化的思想是并行发展,一直相互影响、相互借鉴 Simula 67 是 OO 概念的鼻祖,其设计目标是扩充 Algol 60,以更好地支持计算机在模拟方面的应用。1960 年代在挪威计算中心设计和实现,主持其工作的 Ole-Johan Dahl 和 Kristen Nygaard 获得 2001 年图灵奖 · OO 的三个基本要素:封装、继承和动态方法约束都源于 Simula · 类的概念源自 Simula,其设计中提出用类定义把一组操作与一组数据包装起来。Simula 的这些重要想法是模块概念和 OO 的起源 · Simula 只提供了基本封装,并没有对封装的保护,也没有信息隐藏
OO 发展史 软件实践也需要 OO 的思想,并逐渐开发了 相关的支撑技术,包括: 封装的思想在面向模块的语言里发展,提出了许多重要概念和想法,如 · 作用域规则,开的或者闭的作用域 · 界面与实现 · 透明类型与隐晦类型,访问控制,等等 数据驱动的程序设计技术: · 将计算功能(子程序)约束于程序里处理的数据(结构),使我们在程序里可以从数据对象出发去启动相应的计算过程 · 在一些非常规的语言(如函数式语言)里,可以通过引用的概念提供函数/过程与数据之间的约束 · 常规语言(如 C)引进了指向函数的指针,在实现数据驱动程序设计的过程中起到了重要作用,也成为面向对象语言实现的技术基础
OO 发展史 继承和动态约束等被 Smalltalk 发展,形成目前 OO 的基本概念框架 程序里以类的方式定义各种数据抽象 程序里以类的方式定义各种数据抽象 类可以通过继承的方式扩充新功能,这样定义的新类(子类,派生类)自动继承已有类(基类,超类,父类)的功能 对象是类的实例,是程序运行时的基本数据单元 派生类的对象也看作是原有基类的对象,可以当作基类的对象使用(子类就是子类型,Liskov 代换原理,2008 年图灵奖) 类定义了对象的状态成分(数据成员)和一组相关操作(称为方法) 方法调用总是针对某个对象进行的,将方法调用看作是给相应对象送一个消息,对象通过执行相应操作的方式对消息做出响应 对一个消息执行什么方法,由接收消息的对象的类型确定(根据该对象所属的类确定,这就是动态约束) 计算,就是一组对象相互通讯的整体效果(对计算的另一种看法)
OO 发展史 Smalltalk 还有一些独特的东西: 变量采用引用模型,变量无类型,可以引用任何对象 语言里的一切都是对象: 变量采用引用模型,变量无类型,可以引用任何对象 语言里的一切都是对象: · 类也是对象,通过给类送 new 消息的方式要求创建类的实例 · 各种控制结构也是通过消息概念建立的 条件和逻辑循环是逻辑对象对特定消息的响应 枚举循环是整数对象对特定消息的响应 采用单根的类层次结构,以类 Object 作为所有类的超类 提供了块(block)的概念,作为控制结构的抽象机制 提出了容器的概念,开发了一个功能丰富的类库 与程序开发环境的紧密结合,并开发了 GUI 的基本概念和相关技术 Smalltalk 经过 72、76 发展到 Smalltalk 80,其概念和结构已臻成熟
OO 发展史 随着 Smalltalk 的成功,人们看到了 OO 的潜在威力 许多人开始研究如何把 OO 概念有效集成到常规语言里,提出了一批已有语言的 OO 扩充和许多新 OO 语言,如 Object-Pascal、Object-C 等 其中前期最成功并得到广泛应用的是 C++。C++ 在 OO 概念的广泛接受和应用方面功不可没(具体理由见后面讨论)。原因: · 在面向对象和高效程序之间取得较好的平衡 · OO 概念与常规语言的合理集成(在当时),支持数据抽象和面向对象的系统设计和程序设计,支持多泛型程序设计的结合,使与数据抽象和 OO 有关的许多新概念和新技术逐渐被实际软件工作者接受 随后是 OO 分析、OO 设计和基于 OO 的软件开发等等 后来的其他成功语言包括 Java,微软提出 C#,等等 出现了一些基于对象的脚本语言,如 Python,Ruby 等 现在,面向对象的开发已经成为一种主流的软件开发技术
面向对象的基本概念 面向对象的基本概念: 在面向对象语言里定义数据抽象的基本定义机制是类,在一个类里可以定义数据成员和子程序成员(称为方法) 封装是数据抽象和模块化的概念,与面向对象的概念并没有必然关系,但封装有助于更好发挥面向对象机制的作用 (实在的)类被看作类型,可以用于生成(定义)实例,称为对象 已有的类可以作为定义新类的基础(基类、超类) · 可通过继承方式定义新类(子类,派生类),子类继承基类的行为 · 子类可以修改基类已经定义的行为,或者增加所需的新行为 把子类看作是子类型(通常),如果 D 是 B 的子类,那么: · 若 o 是 D 类型的对象,那么 o 也看作是 B 类型的对象 · 若变量 x 可以引用 B 类的对象,那么它也可以引用 D 类的对象
面向对象的基本概念 继承有两方面作用 1. 建立类型之间的层次关系 2. 重用基类的行为(代码和数据描述) 继承有两方面作用 1. 建立类型之间的层次关系 2. 重用基类的行为(代码和数据描述) 对于面向对象的行为而言,前一方面的功能更为重要 类中的子程序成员称为方法,方法需要通过具体的对象调用 在运行中调用方法时,实际调用的方法由作为调用出发点的那个对象的类型确定的(动态约束) · 动态约束是实现面向对象行为的关键 · 它为面向对象的机制提供了模块机制所不具有的弹性,使新的功能扩充可以比较自然地结合到已有的操作过程里 · 理解动态约束是理解面向对象的关键,动态约束的高效实现也是面向对象语言的实现的关键
面向对象的语言 虽然基本框架类似,不同面向对象语言之间也存在很大差异: 基本问题:采用什么样的对象模型 采用单根的类层次结构,还是任意的类层次结构? 提供那些继承方式? 例如 C++ 里提供了三种继承方式 允许多重继承?还是只允许单继承? 是否提供丰富完善的访问控制机制? 采用基于继承的模型,还是基于指派的模型 基于类的模型,还是基于对象或原型的模型(如 JavaScript) 对象本身的独立性(是否允许不属于任何一个类的对象) 类本身是不是对象?
面向对象的语言 其他情况: 是不是追求“纯粹”的面向对象语言? · Smalltalk 尽可能追求“面向对象”理想,完全是重新设计的新语言 · Java 是接近理想的语言,但希望在形式上尽可能靠近常规语言 · C++ 设法在支持系统程序设计的过程性语言 C 上“扩充”支持面向对象的机制,是一种多范型语言,支持多种程序设计方式 · 另外的一些语言(如Ada)采用可能很不同的方式支持面向对象的程序设计,这里不准备详细介绍 采用值模型还是引用模型。从本质上说,只有采用引用模型才能支持方法的动态约束,因此大多数面向对象语言采用引用模型 · C++ 采用值模型,可以创建静态对象或栈对象,但只有通过对象引用或指向对象的指针才能实现面向对象的动态约束行为 · Java 只能把 OO 功能应用于用户定义类型,基本类型采用值模型
面向对象的语言 是否允许静态对象或者堆栈对象(自动对象)?多数面向对象语言只支持堆对象(通过动态存储分配创建的对象) · C++ 支持静态对象和自动对象,这种设计是希望尽可能借助于作用域规则来管理对象,避免依赖自动存储管理系统(GC) · 为在这种环境下编程,人们开发了许多利用自动对象的对象管理技术,如句柄对象,对象的“创建即初始化”技术等 是否依赖自动废料收集(GC)。由于 OO 程序常(显式或隐式地)创建和丢弃对象,对象之间常存在复杂的相互引用关系,由人来完成对象的管理和回收很困难。大多数 OO 语言都依赖于自动存储回收系统 · GC 的引入将带来显著的性能损失,还会造成程序行为更多的不可预见性(GC 发生的时刻无法预见,其持续时间长短也无法预计) · Java 等许多语言都需要内置的自动废料收集系统 · C++ 是例外,其设计目标之一是尽可能避免对自动存储回收的依赖,以支持系统程序设计,提高效率,减少运行时间上的不确定性
面向对象的语言 是否所有方法都采用动态约束? 一些脚本语言也支持面向对象的概念。例如, · 动态约束很重要,但调用时会带来一些额外的开销,如果需要调用的方法能够静态确定,采用静态约束有速度优势 · 大部分语言里的所有方法都采用动态约束 · C++ 和 Ada 提供静态约束(默认)和动态约束两种方式 一些脚本语言也支持面向对象的概念。例如, · Ruby 是一个纯面向对象的脚本语言,其中的一切都是对象,全局环境看作一个匿名的大对象,全局环境里的函数看作这个对象的成员函数。它还有另外一些独特性质 · JavaScript 支持一种基于对象和原型的面向对象模型。其中没有类的概念,只有对象。对象的行为继承通过原型获得
面向对象的语言 人们还提出了许多与面向对象机制有关的新想法和模型 许多新近的脚本语言提供了独特的面向对象机制:例如 · 基于对象原型(而不是类)的 OO 模型 · 在基于类的模型中允许基于对象的行为覆盖(可修改个别对象的行为) · 等等 总而言之,虽然今天面向对象的模型和语言已成为主流程序设计方法和主流程序语言,但是这类语言还远未成熟,还正在发展和研究中 · 许多语言的 OO 机制非常复杂,实际还不断提出一些新要求,使一些OO 语言在发展中变得越来越复杂 · 如何提供一集足够强大,而且又简洁清晰的机制支持 OO 的概念和程序设计,还是这个领域中需要继续研究的问题 · OO 语言有关的理论研究还处在起步阶段,也是本领域不成熟的标志
OO 语言需要提供的新机制 定义类的语言机制(语言提供特殊的描述结构) 描述或定义对象的机制 继承机制,描述类之间的继承关系。可能定义继承关系的性质(如 C++ 里的类继承有 public、protected 和 private 三种方式) 与对象交互的机制(方法调用,消息传递) 初始化新对象的机制(最好能自动进行,避免未初始化就使用的错误) 类类型对象的动态转换机制(转换对一个具体对象的观点) 控制类成员的访问权限的机制 对象销毁前的临终处理机制(最好能自动进行) 对象的存储管理机制 可能还有其他机制: 运行中判断对象的类属关系的机制、自反等等
4.3 面向对象的基本特征 P.Wegner总结了OO语言的发展, 给出以下图示澄清了概念: 封装→ 对象(数据和操作) 局部性、可维护性 封装→ 对象(数据和操作) 局部性、可维护性 抽象→ + 类 概括描述、简单性 继承→ + 类体系 可重用性 多态→ 重载、类属 可扩充性 动态束定→ 面向对象 可交互性 基于对象的语言 基于类的语言 面向对象语言 Ada 83, Actor CLU Smalltalk、 Eiffel simula 67 C++, Ada 95, Java
OO 程序 先看一点 OO 程序,复习一下基本 OO 程序的特征 这里看一段定义了几个类的 C++ 代码 每个结点里有一个域指向表头结点 定义 list_node 类,用于实现带头结点的双向循环链接表 每个结点里有一个域指向表头结点
OO 程序 定义 list_node 类,用于实现带头结点的双向循环链接表 每个结点里有一个域指向表头结点
OO 程序 定义一个list类 注意:header 是个 list_node 定义的是有头结点的循环链表
OO 程序 通过继承定义 queue 类。(只是作为示例)
OO 程序 还可以定义通用的容器类: · 基本容器类没有具体数据域,不保存具体类型的元素,只实现容器操作,如:一些基本判断谓词,插入删除等等 · 基本容器类没有具体数据域,不保存具体类型的元素,只实现容器操作,如:一些基本判断谓词,插入删除等等 · 通过继承实现存储具体类型的元素的具体容器 每个结点里有一个域指向表头结点
OO 程序 通用的表结点类 派生的 int 表结点类 使用这种 int 表的问题: 如果需要访问结点的数据内容,必须对取出的结点做强制
面向对象概念的实现 实现面向对象的语言,需要考虑它的几个标志性特征的实现 封装是一种静态机制,如 C++/Java 一类语言的各种访问控制机制也是静态的,都可以通过在符号表里记录信息,在编译中检查和处理 方法的实现与以模块为类型时局部子程序的实现一样。由于每个方法调用有一个调用对象,因此方法需要一个隐含指针,被调用时指向调用对象,所有对该对象的数据成员的访问都通过这个指针和静态确定的偏移量进行 许多语言以这一指针作为一个伪变量,称为 this 或者 self,通过这种指针 访问调用对象,方式上与通过指针访问普通结构一样 实现面向对象语言的关键是两个问题: · 继承的实现,使派生类型的对象能当作基类的对象使用 · 动态约束的实现,能够从(作为变量的值或者被变量引用的)对象出发,找到这个对象所属的类里定义的方法 下面讨论实现的一些具体问题
封装 封装是一种静态机制,仅仅在程序加工阶段起作用,有关情况与模块机制类似,在加工后的程序里(可执行程序里)完全没有关于封装的信息 不同语言里对类的访问控制可能不同: · 作为 “开模块”(允许以特定方式任意访问类成员) · 作为“闭模块”(凡是没有明确声明可访问的都不可访问) 对基本封装机制的扩充是引进进一步的控制 C++ 引进成员的 public、protected 和 private 属性,提供细致的访问控制 C++ 还允许定义派生类的不同继承方式,控制对基类成员的访问: · public 继承 · protected 继承,使基类的 public 成员变成派生类的 protected 成员 · private继承,使基类的所有成员变成派生类的 private 成员 一些新语言借鉴了 C++ 的这方面思想,可能结合另外一些想法
静态域和静态方法 许多面向对象语言的类里可以定义静态域和静态方法 静态方法和静态域的一些情况: · C++/Java 允许类里定义静态数据域 · Smalltalk 把普通的对象域称为实例变量,表示在这个类的每个实例里都有这些成分的一份拷贝;把静态数据域称为类变量 · 类的静态数据域并不出现在实例对象里,它们是类封装的静态数据成分,提出具有静态生存期,在类的作用域里可直接访问。类外能否访问由语言确定(提出有与其他成员一样的访问控制) 静态方法和静态域的一些情况: · 类的静态数据成员可以在静态区实现,在程序运行之前静态分配,在程序的整个执行期间保持其存储 · 类的静态方法可访问静态数据成员,其他方法也可以访问静态数据成员 · 可以把静态数据成员看作本类的所有对象共享的信息 · 类对象可以通过静态数据成员交换或者共享信息
静态域和静态方法 静态方法相当于普通子程序,只是具有类封装(类作用域)。特点: 仅有静态数据成员和静态方法的类,相当于一个简单模块 静态成员是静态创建的,其初始化在程序开始执行前完成(或者在语言定义的适当时刻完成),只做一次 静态成员的初始化中不能调用类的普通方法(因为没有对象) 静态方法相当于普通子程序,只是具有类封装(类作用域)。特点: · 没有调用对象 · 不能引用 this/self,不能引用类定义的普通数据成员(如 Smalltalk 里不能引用实例变量),只能引用本类的静态数据成员 · 通常采用某种基于定义类的语法形式调用 仅有静态数据成员和静态方法的类,相当于一个简单模块 · 提供模块的内部状态,可以通过所提供的方法修改状态 · 不能生成有用的实例(生成的是空实例,没有局部的实例状态) · 静态数据成员的静态方法的封装,可能定义内部数据和操作
对象和继承:数据布局 B类的对象 B类的 数据成员 D类的对象 B类的 数据成员 D类新增的 数据成员 继承关系的数据部分通过对象的适当存储布局实现 对象的实际表现就是数据成员的存储 假定 B 是一个类,有自己的数据成员 D是B的派生类,增加了数据成员。D类对象的前部仍 是B类的所有成员,扩充的成员排在后面 在D类对象里,所有B类成员相对于对象开始位置的偏 移量与它们在一个B类对象里的偏移量相同 这样,D 类对象就可以作为 B 类对象使用,B 类里的 方法能正确操作,它们只看属于 B 对象的那部分 D 类里的方法既可以使用对象中的 B 类数据成员,也 可以使用对象里的 D 类数据成员 用D类对象给B类对象“赋值”(值 copy,或者值语义时) 会产生“切割”现象,D 类数据成员不能拷贝 B类的 数据成员 D类的对象 B类的 数据成员 D类新增的 数据成员
初始化和终结处理 对象可能具有任意复杂的内部结构 要求创建对象的程序段做对象初始化,需反复描述,很麻烦,易弄错 对象可能要求特殊的初始化方式和顺序,对象的使用者难以贯彻始终 继承使对象的初始化更复杂化,因为需要正确初始化继承来的数据成员 为更容易处理对象初始化的问题,OO 语言通常都提供了专门的机制,在对象创建时自动调用 初始化操作保证新创建对象具有合法的状态。自动调用非常有意义,可以避免未正确初始化造成的程序错误 现在常把对象初始化看作调用一个称为构造函数(constructor)的初始化子程序,它(们)在对象的存储块里构造出所需要的对象 语言通常支持参数化的初始化操作,以满足不同对象的需要。对象创建可能有多种需要,为此 C++/Java 等都支持一个类有多个不同的构造函数
初始化和终结处理 如果变量采用引用语义,所有(值)对象都需要显式创建,有明确的创建动作。这样很容易保证在存储分配之后调用构造函数 如果变量是值,为保证初始化,语言需要对变量创建提供特殊语义,要求变量创建包含隐式的构造函数调用 对象初始化必须按一定的顺序进行 · 对象内部的基类部分必须在派生类部分之前完成初始化,因为派生类新增的数据成员完全可能依赖于基类成员的值 · 数据成员本身也可能是某个类的对象,在执行整体对象的构造函数的过程中,就需要执行这些对象成员的构造函数 · 这种构造规则是递归的,语言必须严格定义对象的构造顺序 如果变量采用值语义(例如 C++),在进入一个作用域的过程中,就可能出现许多构造函数调用 · 进入作用域可能是代价很大的动作
初始化和终结处理 在对象销毁之前,可能需要做一些最后动作(终结处理),例如释放对象所占用的各种资源,维护有关状态等 忘记终结处理,就可能导致资源流失,或者状态破坏 有些 OO 语言提供终结动作定义机制,销毁对象前自动执行所定义动作 C++ 采用值语义,终结动作以类的析构函数的形式定义: · 类变量是堆栈上的对象,在其作用域退出时,自动调用它们的终结动作 · 堆对象需要显式释放,释放之前恰好应该执行终结动作,易于处理 采用引用语义的语言(如 Java),通常并不提供销毁对象的显式操作(以防悬空引用),对象销毁由 GC 自动进行 · 有了 GC,对终结动作的需求大大减少,终结动作由 GC 自动进行 · 执行终结动作的时间不可预计,出现了(时间和顺序的)不确定性 · 对象关联和 GC 顺序的不确定性使终结动作很难描述
静态和动态约束的方法 OO 语言里的方法调用通常采用 x.m(...) 的形式,其中 · m 是 x 的定义类型(类,假定为 B)的一个方法 问题:x.m(...) 所调用的方法何时/根据什么确定?两种可能性: 根据变量 x 的类型(在程序里静态定义)确定(静态约束) 根据方法调用时(被 x 引用/指向)的当前对象的类型确定(动态约束) · 由于 x 可能引用(指向)B 类或其任何子类的对象,因此同为这个方法调用,不同的执行中实际调用的完全可能是不同的方法 所有 OO 语言都支持动态方法约束(否则就不是 OO 语言),多数以它作为默认方式。少数语言同时也支持静态约束的方法,如 C++、Ada 等 C++ 把动态约束的方法称为虚方法(virtual 方法),而且以静态约束作为默认方式。这种设计与它的 C 基础有关
静态约束的实现 调用静态约束的方法,实现方式就像是调用普通子程序(过程/函数),唯一不同之处就是需要传入一个指向调用对象的指针 在符号表里,每个类的记录项都包含了一个基类索引,依靠这个索引形成的基类链就可以静态(编译时)完成静态约束的方法的查找: 1. 首先在变量所属的类(静态已知)里查找(查找符号表)。如果在这里找到了所需要的方法,就生成对它的调用;如果不存在就反复做下一步 2. 转到当前类的基类里去查找相应方法,如果找到就生成对它的调用;如果找不到就继续沿着基类链上溯查找 3. 如果已无上层基类,查找失败。报告“调用了无定义的方法”错误 所有对静态约束的方法的调用都可以静态(编译时,一次)处理 · 运行时的动作与一般子程序调用完全一样,没有任何额外运行开销 · 如果语言允许静态约束的方法,采用静态约束可以提高效率。静态约束的方法还可以做 inline 处理
方法的动态约束 B 类里定义了一个一般性操作 tem,对所有 B类对象都有价值 tem 中调用了一个特殊操作 sp,该操作可能因子类不同而不同 子类 D 覆盖操作 sp后,仍能正常地调用操作tem,而且其中对 sp 的调用能调用到 D 类里新的操作定义 这是 OO 程序设计里最重要的东西: 这一特征使新类给出的行为扩充(或修改)可以自然地融合到已有功能里,包括放入已有的操作 框架里(这个例子就是)
动态约束的实现:一般模型 对最一般的对象模型,运行中调用动态约束的方法时要做一次与编译时处理静态约束方法一样的查找,这种查找可能非常耗时 为完成这种方法查找: · 每个类需要有一个运行时表示(把类也作为程序对象),类表示中需要有一个成分是基类索引,还有一个成分是方法表 · 每个对象里必须保存所属类的信息(一个类指针,指向其类) · 每个动态方法调用都启动一次方法查找。如果找到就调用,找不到就发出一个 “message is not understood”(Smalltalk)动态错误 这种方式普遍有效,可以处理具有任何动态性质的对象模型,如动态类层次结构构造和动态方法更新(修改、添加和删除)、动态类属关系等 · 查找的时间开销依赖于继承链的长度和继承链上各个类中方法的个数 · 这种方法的缺点是效率太低。如果所采用的对象模型在动态特性方面有所限制,就可能开发出效率更高的方法
动态约束的实现:受限模型 早期 OO 语言(包括 Smalltalk)都采用功能强大灵活的对象模型 · 在提供了极大灵活性的同时,也带来效率上的巨大开销 · 这也是早期 OO 语言及其概念难被实际软件工作者接受的最关键原因 提高算法效率的最基本途径是限制要解决的问题(对 更 特 殊 一 些 的 问 题 , 可能找到效率更高的算法),并设计优化的实现模型 对于 OO 语言,就是要找到一个受限的对象模型,它能比较高效地实现,同时又能满足绝大部分实际 OO 程序开发的需要 常规 OO 语言中的对象模型有如下特性(足以支持常见程序设计工作) : · 类层次结构是静态确定的 · 每个类里的动态约束方法的个数和顺序都静态确定 在这种模型里就可以避免动态方法查找,使方法调用的执行效率接近普通的子程序调用的执行效率(C++ 和 Stroustrup 的贡献)
动态约束的实现 优化实现模型,其中绝大部分工作都能静态完成: 每个类的运行时体现是一个动态约束方法的表(称为虚表,vtable),这是一个指针表,指针指向本类的对象需要调用的方法的代码体 虚表的指针按方法在类里的顺序排列,每个方法对应于一个顺序下标
动态约束的实现 在每个对象开头(数据域之前) 增加一个指针 vt 创建对象时,设置其 vt 指向 其所属的类的虚表(运行中始终不变) f 是指向 F 的指针(或引用) 调用 f->m(...)的实现 比调用静态约束的方法多了中间的两条指令,它们都需要访问内存
动态约束的实现 虚表的创建: 如果类 B 没有基类,就将其定义里的动态约束方法的代码体指针依次填入它的虚表(下标从 0 或者 1 开始算) 若类 D 的基类是 B,建立 D 的虚表时先复制 B 的虚表。如 D 覆盖了 B 的某个(某些)动态约束方法,就用新方法的指针覆盖虚表里对应的已有指针。若 D 定义了新的动态约束方法,就将它们顺次加入虚表,放在后面 如果 f 指向的对象是 B,那么 f->m(...) 也会调用正确的方法
动态约束的实现 重温受限的对象模型(对一般程序设计已经足够强大) : 优化实现的效果: 重温受限的对象模型(对一般程序设计已经足够强大) : · 类层次结构是静态确定的 · 每个类里的动态约束方法的个数和顺序都静态确定 优化实现的效果: · 构造方法表的工作在编译时完成 · 每个对象里需要增加一个指向其类的方法表的指针 · 每次方法调用需要多执行两条指令(典型情况),多访问内存两次 对这种受限对象模型,动态约束方法调用的额外开销不大,一般软件系统(包括系统软件的绝大部分情况)都可以接受 Stroustrup 在设计和实现 C++ 语言时特别希望能够得到高效的实现,最后选择了这种对象模型,并设计了这种高效的实现方法
动态约束的实现 对数据抽象和面向对象技术的支持,以及高效的实现,使实际软件工作者看到了 C++(和 OO)的潜力,最终导致了面向对象的革命 以后的主流面向对象语言也都采用了这种技术。当然,采用这种选择,也就对它们可能采用的对象模型提出了严格的限制 Pragmatics 里本章最后的练习里还讨论了其他高效实现方法,《C++ 语言的设计与演化》里也有讨论(通过几条指令构成的一段“蹦床代码”,将控制转到实际应该调用的方法,主要是要解决多重继承问题) 虚方法(动态约束方法)的一个重要缺点是不能做 inline 处理(在线展开要求静态确定被调用的方法),使编译器难以进行跨过程的代码优化 C++ 希望支持高效的系统程序设计,认为虚方法带来的效率损失有时也是不能容忍的,因此它同时支持静态方法约束 注意:如果一个类里只有静态约束的方法,该类编译之后就不会生成方法表,该类的对象也没有一个指针的额外存储开销
类层次结构和强制转换 向上强制总是安全的,不会出问题,总可以自动进行。因为派生类包含基类所有数据成分,因此可以支持基类所有操作 后一个赋值是编译时错误,派生类指针不能引用基类对象
类层次结构和强制转换 如果用 foo 类的指针 q 传递一个对象 可保证该对象一定是 foo 的或它的某个派生类的对象 如果由 foo 类指针 q 传递的实际上是一个 bar 对象,我们有时需要把它作为bar 对象使用,例如想对它调用 foo 里没有的方法 · q->s(..) 是静态类型错误(q 的指向类型是 foo,foo 无方法 s) · s = q 也是静态类型错(不能保证 q 指向的是 bar,赋值不安全) 能不能用 s = (bar*)q ? 如果 q 指向的确实是一个 bar 对象,当前情况下恰好可以,因为 · (bar*) 对指针是“非变换转换”,导致把 foo 指针当做 bar 指针 · 恰好 bar 对象的起始位置和各成分的偏移量与 foo 一样 这些条件有时不成立(下面会看到,在存在多重继承时) 这种转换不安全,它要求 q 指向的确实是 bar。动态怎么检查类型?
bar *x = dynamic_cast<bar*>(q); 类层次结构和强制转换 C++ 为安全的向下强制转换提供了专门运算符 dynamic_cast。上述转换的正确写法: bar *x = dynamic_cast<bar*>(q); 如果 q 指向的确实是 bar 类的对象,转换将成功,x 指向该 bar 类对象 如果 q 指向的不是 bar 类的对象,转换失败,x 被赋空指针值 0 通过检查 x 的值,可以判断转换是否成功 实现 dynamic_cast,就要求在运行中能判断对象的类型和类型间关系 这就是运行时类型识辨(Run Time Type Identification,RTTI) 要像支持安全的向下转换,C++ 的实现需要在 虚表里增加一个类描述符 常放在虚表最前。一些 C++ 编译器要求用户指明需要用 RTTI,在这种情况下才按这种方式创建虚表(虚表的形式与没有类描述符时不同) dynamic_cast 检查类型关系,确定能否转换,在能转换就完成转换
类层次结构和强制转换 多数 OO 语言(如 Java 等)默认支持 RTTI,虚表里总保存类描述符 如何描述类型是编译器的具体实现问题,不必关心 RTTI 机制可保证类型安全的转换 虽然 Java 的类型转换采用 C 语言类型转换的描述形式,但功能不同 在牵涉到基本类型时,可能需要做值的转换 在牵涉到类类型时,需要做动态的类型转换合法性检查 如果发现错误,就抛出异常 ClassCastException 否则做“非变换类型转换”,把相应引用直接当作所需的类型的引用 从基本类型值到类类型的合法转换,还需要自动构造对象(boxing);从类对象到基本类型值的转换需要提取对象内的值(unboxing) 运行时类型描述机制还被用于支持“自反”(reflection)功能
4.4 Ada 的面向对象机制 定义类和实例对象 package Object is Ada95以抽象数据类型实现类。类的封装性由包实现,类的继承性则扩充了标签(tag)类型和抽象类型,标签类型只限记录类型。类的继承性利用了Ada8的类型派生机制实现子类。 package Object is type Object is tagged --此类型的数据即对象的属性 record --无tagged即一般的ADT,有它是为了类继承 X_Coord:Float:=0; Y_Coord:Float:=0 --初值为缺省时用 end record; function Distance (O:Object)return Float;--Object对象的行为 function Area(O:Object) return Float; end Object
with Object, use Object; package Shapes is --这个包封装了三个子类(型) type Point is new Object with null record; --只继承不扩充的子类 type Circle is new Object with --继承并扩充此属性 record Radius:Float; end record; function Area(C:Circle) return Float; --覆盖Object中的Area type Triangle is new Object with -–继承并 A, B, C:Float; --扩充三个属性 function Area (T:Triangle) return Float; --覆盖 end Shape;
这些类(型)包外可见(可输出),在主子程序中声明实例,如同类型声明变量,以初值表达式作值构造子: 子类的实例 也是父类的实例 O:Object: P:Point; --声明实例对象 P:(O with null record); C:Cirele:=(0.0, 0.0, 34.7); C:=(O with 34.7); T;Triangle:=(3.0, 4.0, 5.0); T:=(O with 3.0, 4.0, 5.0); 如果动态生成实例,可将此声明放在类的方法(过程/函数)中,调用时生成。
以类宽类型实现多态 Ada95的每个标签类型都有一个与之对应的类类型属性T’ Class,并把它叫做类宽类型(Class Wide Type) 设已声明T类型,及T’Class的变量V,则 Y:T; --一般声明,正确 Y:T’Class; --不可以 Y:T’Class:=V; --可以,T’Class束定为V的类型 类宽类型的范围示意如下:
procedure Process_Reservation (Rc:in out Reservation’Class) is --形参可以是类宽类型,不必最初束定某特定类型 begin … Make(Rc); --它按相结合的Rc的具体类型出票 end Process_Reservation; 这个程序在编译时Make是没有也无法束定到某 make函数体上,只有在 运行中(动态)束定,束定时按Rc对应的实参的标签tag值派送,编译时只要 做出派送表
扩充程序包机制实现继承的类体系 Ada 95增设了子辈单元(child unit)和私有子辈单元。 子辈单元 package Reservation_System.Supersonic is --‘·‘后是子辈单元名 type Supersonic_Reservation is new Reservation with private; private type Supersonic_Reservation is new Reservation with record Champagne:Vintage; …… end record; procedure Make(Sr:in out Supersonic_Reservation); procedure Select_Seat(Sr:in out Supersonic_Reservation); end Reservation_System.Supersonic;
私有子辈单元 package OS is --父包OS --OS 的可见成份 type File_Descriptor is private; … private type File_Descriptor is new Integer; end OS; package OS.Exceptions is -–OS的子辈程序包 File_Descriptor_Error, File_Name_Error, Permission_Error:exception; --所定义异常OS各子辈包均可用 end OS.Exceptions; --公有,但不涉及泄露 with OS.Exceptions;
with OS.Exceptions; package OS.File_Manager is -–OS的子辈程序包 type File_Mode is (Read_Only, Write_Only, Read_Write); function Open(File_Name:String; Mode; File_Mode) return File_Descriptor; procedure Close (File:in File_Descriptor); … end OS.File_Manager; --公有,只用私有类型。也无泄露 procedure OS.Interpret(Command:String); --命令解释过程,等同子包 private package OS.Internals is -–私有子辈程序包,不用with end OS.Internals; private package OS.Internals_Debug is -—OS的私有子辈程序包 end OS.Internals_Debug;
Ada的多继承 with Abstract_Sets; package Linked_Sets is type Linked_Set is new Abstruct_Sets with private; --再定义Linked_Set的各种操作 private type Cell; type Cell_Ptr is access Cell; type Cell is record E:Element; next:Cell_Ptr; end record function Copy(P:Cell_Ptr)return Cell_Ptr; type lnner is new Controlled with The_Set:Cell_Ptr: end record: procedure Adjuse (Obj :in out lnner); type Linked_Set is new Abstract_Sets with -—继承Abstraet_sets Component:lnner: --其扩展成分又继承了Controlled end record; end Linked_Sets;