历年题目:
参考资料<https://blog.csdn.net/qq_52370024/article/details/125402799 >
抽象是指对于一个过程或者一件制品的某些细节有目的的隐藏,以便把其他方面、细节或者结构表达得更加清楚。抽象,是控制复杂性时最重要的工具。
在典型的OOP程序中,有许多级抽象,更高层次的抽象部分地体现了面向对象程序面向对象的特征:
团体:在最高级别上,程序被视为一个对象的“团体”,这些对象间相互作用,以完成共同的目标。
在面向对象程序开发过程中,关于“团体”有两个层次的含义:
- 程序员的团体,他们在现实世界中相互作用,以便开发出应用程序来。
- 这些程序员创建的对象的团体,它们在虚拟世界中相互作用,以完成它们的共同目标。
单元:许多语言允许协同工作的对象组合到一个“单元”(unit)中。
例如,Java的“包” (packages),C++的“名字空间”(name spaces),Delphi中的“单元”(units)。这些单元允许某些特定的名称暴露在单元以外,而其他特征则隐藏在单元中。
CS:处理两个对象之间的交互。
涉及两层抽象:一个对象向另一个对象提供服务,二者之间以通信来交互;消息传递。该级别抽象通常用接口来表示。定义行为,但不描述如何来实现。
服务实现方式:考虑抽象行为的具体实现方式。
具体实现:关注执行一个方法的具体操作实现。
抽象的思想可划分为不同的形式:
鸭嘴兽提醒我们,总会有例外(非标准行为) 。面向对象的语言,也需要有一种机制来覆盖从上一级继承来的信息。
抽象机制的发展过程:
封装(利用数据抽象进行编程)的作用:
使用实例来表示类的一个具体代表或范例。实例包括实例变量(数据成员/数据字段)。对象 = 状态(实例变量) + 行为(方法)。
对象外部看,客户只能看到对象的行为;对象内部看,方法通过修改对象的状态,以及和其他对象的相互作用,提供了适当的行为。
变量的静态类是用来声明变量的类;变量的动态类是与变量值相关的类。
为了增强可读性,类在声明时字段次序建议:
Java、C#中类的实现是直接放在类定义中的,C++则将定义和实现分离。对于Java来讲,可以通过接口实现分离。接口的特点:
把方法体放在类定义之外有两个原因。
类主题的变化包括:
这一节介绍消息传递的机制,然后将探讨对象的创建和初始化。
消息:对象间相互请求或相互协作的途径。
消息的特点:
语言的类型区别:
静态语言类型:类型和变量联系在一起。
编译时作出内存分配决定。不必运行时刻重新分配。控制类型错误。
动态语言类型:变量看作名称标识,类型和数值联系在一起。
在消息传递这方面,静态类型语言和动态类型语言之问存在显著的差异:
消息总是传递给接收器。然而,在大多数面向对象语言中,接收器并不出现在方法的参数列表中,而是隐藏于方法的定义之中。只有当必须从方法体内部去存取接收器的数值时,才会使用伪变量(pseudo-variable)。伪变量和通常的变量很相似,只是它不需要声明,也不能被更改(也许用伪常量这一术语更加合适,但是这一术语好像没有出现在任何语言的定义中),也就是Java中的this
。
显然,Python 语言并不是这样的,他需要显式声明
self
。
创建就是为一个新对象分配存储空间并且将这段空间与对象名称进行绑定。初始化不但包括为对象的数据区域设置初始值,这类似于对记录中数据字段进行的初始化,而且还包括建立操作对象所需的初始条件这个更一般的过程。
在大多数面向对象语言中,后者对于使用对象的容户的隐藏程度是封装的一个重要的方面,我们认为这是面向对象技术优于其他编程技术的一个主要方面。
对象数组的创建涉及两个层次的问题。一是数组自身的分配和创建,然后是数组所包含的对象的分配和创建。在C++语言中,这些特征是结合在一起的。数组由对象组成,而每个对象则使用缺省 (即无参数)构造函数来进行初始化;另一方面,在Java 中,表面上看来相似的语句却有着完全不同的效果。用来创建数组的 new操作符只能用来创建数组。数组包含的每个数值必须独立创建,典型的方法是通过循环来实现。
所有面向对象语言在它们的底层表示中都使用指针,但不是所有的语言都把这种指针暴露给程序员。Java的对象引用实际是存在于内部表示中的指针。这一点有三个原因:
内存的回收有两种机制:
内存的分配策略有三种:
静态:在编译时就能确定每个数据目标在运行时刻的存储空间需求,因而在编译时就可以给他们分配固定的内存空间。
堆式:在编译时能知道所有变量的存储要求,栈式存储分配要求在过程的入口处必须知道所有的存储要求,而堆式存储分配则专门负责在编译时或运行时模块入口处都无法确定存储要求的数据结构的内存分配,比如可变长度串和对象实例。
堆由大片的可利用块或空闲块组成,堆中的内存可以按照任意顺序分配和释放。
栈式:可称为动态存储分配,是由一个类似于堆栈的运行栈来实现的。
栈式存储分配按照先进后出的原则进行分配。
构造函数(constructor) 是用来初始化一个新创建对象的方法。把创建和初始化联系起来有很多优点。最重要的是,它确保对象在正确地初始化之前不会被使用。当创建和初始化分离时(当使用没有构造函数的编程语言时),程序员在创建新对象之后, 很容易忘记调用初始化过程,这样通常会导致不良后果。
有一个特殊的类,一般称为Class,这就是类的类。在一种所有皆对象的世界观背景下,在类模型基础上还诞生出了一种拥有元类(metaclass)的新对象模型。即类本身也是一种其他类的对象。
目前,有三种不同观点的对象模型:
替换原则:如果类B是类A的子类,那么在任何情况下都可以用类B来替换类A,而外界毫无察觉。
子类型:指符合替换原则的子类关系。区别于一般的可能不符合替换原则的子类关系。
子类有时为了避免继承父类的行为,需要对其进行改写。
改写与替换结合时,想要执行的一般都是子类的方法。
与类一样,接口可以继承于其他接口,甚至可以继承于多个父接口。虽然继承类和实现接口并不完全相同,但他们非常相似,因此使用继承这一术语来描述这两种行为。
抽象方法是介于类和接口之间的概念,定义方法但不实现;创建实例前,子类必须实现父类的抽象方法。
继承共有八种:
很多情况下,都是为了特殊化才使用继承。在这种形式下,新类是基类的一种特定类型,它能满足基类的所有规范。用这种方式创建的总是子类型,并明显符合可替换原则。
与规范化继承一起,这两种方式构成了继承最理想的方式,也是一个好的设计所应追求的目标。
规范化继承用于保证派生类和基类具有某个共同的接口,即所有的派生类实现了具有相同方法界面的方法。在这种情况下,基类有时也被称为抽象规范类。
基类中既有已实现的方法,也有只定义了方法接口、留待派生类去实现的方法。派生类只是实现了那些定义在基类却又没有实现的方法。
派生类并没有重新定义已有的类型,而是去实现一个未完成的抽象规范。 也就是说,基类定义了某些操作,但并没有去实现它。只有派生类才能实现这些操作。
在Java中,关键字abstract确保了必须要构建派生类。声明为abstract的类必须被派生类化,不可能用new运算符创建这种类的实例。除此之外,方法也能被声明为abstract,同样在创建实例之前,必须覆盖类中所有的抽象方法。
一个类可以从其基类中继承几乎所有需要的功能,只是改变一些用作类接口的方法名,或是修改方法中的参数列表。即使新类和基类之间并不存在抽象概念上的相关性,这种实现也是可行的。
当继承的目的只是用于代码复用时,新创建的子类通常都不是子类型。这称为构造子类化。一般为了继承而继承,如利用一些工具类已有的方法。构造子类化经常违反替换原则(形成的子类并不是子类型)。
派生类扩展基类的行为,形成一种更泛化的抽象。泛化子类化通常用于基于数据值的整体设计,其次才是基于行为的设计。
如果派生类只是往基类中添加新行为,并不修改从基类继承来的任何属性,即是扩展继承。(泛化子类化对基类已存在的功能进行修改或扩展,扩展子类化则是增加新功能)。由于基类的功能仍然可以使用,而且并没有被修改,因此扩展继承并不违反可替换性原则,用这种方式构建的派生类还是派生类型。
如果派生类的行为比基类的少或是更严格时,就是限制继承。常常出现于基类不应该、也不能被修改时。由于限制继承违反了可替换性原则,用它创建的派生类已不是派生类型,因此应该尽可能不用。
两个或多个类需要实现类似的功能,但他们的抽象概念之间似乎并不存在层次关系。
但是,通常使用的更好的方法是将两个类的公共代码提炼成一个抽象类,并且让这两个类都继承于这个抽象类。与泛化子类化一样,但基于已经存在的类创建新类时,就不能使用这种方法了。
可以通过合并两个或者更多的抽象特性来形成新的抽象。一个类可以继承自多个基类的能力被称为多重继承。
对于静态类型的面向对象语言来说,存在着一个关于继承和替换这一面向对象核心思想的悖论。这一悖论来自于子类(sublass)和子类型(subtype)这一对概念。
如果一个类是通过继承创建的,那么就称这个类为子类。
如果说新类是已存在类的子类型,那么这个新类不仅要提供已存在类的所有操作,而且还要满足于这个已存在类相关的所有属性;子类型关系是通过行为这个术语描述的,与新类的定义或构造无关。
当继承的目的只是用于代码复用时,新创建的子类通常都不是子类型,这称为构造子类化。
使用可复用组件的程序员只需了解组件的性质和接口,而不必了解用于实现组件的技术的详细信息。这样可以滅少软件系统的相互关联。前面我们曾提到,这种相互关联的特性是导致传统软件变得复杂的一个主要原因。
编程语言中,术语静态总是用来表示在编译时绑定于对象并且不允许以后对其进行修改的属性或特征;术语动态用来表示直到运行时绑定于对象的属性或特征。
编程语言中最显著的分歧来自于静态类型语言与动态类型语言之间的差异。动态类型语言与静态类型语言之问的差异在于变量或数值是否具备类型这种特性。
替换原则:声明为父类类型的变量可以用来保存子类类型的数值。
为了区别这两种类型,我们引入一对术语,静态类(static clase)和动态类(dynamnie class)。关于变量的静态类是指用于声明变量的类。静态类(就像名称所暗示的那样)在编译时就确定下来,并且再也不会政变。关于变量的动态类是指与变量所表示的当前数值相关的类。同样,如名称所暗示的那样,动态类在程序的执行过程中,当对变量赋新值时可以改变。例如:
var obj = GraphicalObject; (* GraphicalObject is the static class *)
begin
obj = new Ball() (* Ball is the current dynamic class *)
obj = new Wall() (* Wall is now the dynamic class *)
end
静态类型与动态类型之间的最重要区别为:对于静态类型面向对象编程语言,在编译时消息传递表达式的合法性不是基于接收器的当前动态数值,而是基于接收器的静态类来决定的。
运行时类型决定:替换原则可以通过提升数值在继承层次上的位置来体现。有时则相反,还需要判断一种变量目前所包含的数值是否为类层次中的低层次类。
向下造型(反多态):做出数值是否属于指定类的决定之后,通常下一步就是将这一数值的类型由父类转换为子类。这一过程称为向下造型,或者反多态,因为这一操作所产生的效果恰好与多态赋值的效果相反。
对于几乎所有的面向对象编程语言来说,在响应消息时对哪个方法进行绑定是由接收器当前所包含的动态数值来决定的。
Animal pet;
pet = new Dog(;
pet.speak();
Woof !
pet = new Bird();
pet.speak():
tweet !
如果方法所执行的消息绑定是由最近赋值给变量的数值的类型来决定的,那么我们就称这个变量是**多态(polymarphic)**的(对于Smalltalk、Java 和大多数其他面向对象语言来说,从这种意义上来讲,所有变量都是多态的)。而对于C++语言的声明为简单类型的变量,在这种意义上则不是多态的;而使用指针引用的对象数值是多态的。例如:
Animal a;
Dog b;
b.speak(;
woof!
a=b;
a.speak();
Animal speak!
Bird c;
c.speak();
tveet!
a=C;
a.speak();
Animal speak!
Animal * d;
d = &b; 1/ point to the dog from earlier exampie
(*d).speak();
woof !
d=c;
d->speak();
tweet !
方法绑定:分为静态方法绑定和动态方法绑定。响应消息时对哪个方法进行绑定是由接收器当前所包含的动态数值来决定的。
多态变量:如果方法所执行的消息绑定是由最近赋值给变量的数值的类型来决定的,那么就称这个变量是多态的。
内存分配方案有三种:
最小静态空间分配:只分配基类所需的存储空间。
代价是带来切割。把子类赋值给父类后,子类特有空问会被截掉(调用父类拷贝构造函数)。
最大静态空间分配:无论基类还是派生类,都分配可用于所有合法的数值的最大的存储空间
动态内存分配:堆栈中不保存对象值,只分配用于保存一个指针所需的存储空间。
复制语义 (copy semantics):赋值会将操作符右侧的变量值复制给操作符左侧的变量。此后,这两个变量值是互相独立的,其中一个变量值的改变不会影响到另外一个变量值。复制语义有时用在C++ 语言中,有时则不是。
指针语义(pointer semantics ):赋值会将操作符左侧变量的参考值改变成右侧变量的参考值(这种方法有时也称为指针赋值 (pointer assignent))。这样,两个变量不仅具有相同的数值,而且还指向存储数值的同一内存地址。一个变量值的改变会同时改变两个变量的数值,这可以通过不同的变量名称得以反映。Java、CLOS、Objeet Pascal 语言以及许多其他的面向对象语言都采用指针语义。
当对指向其他对象的变量值进行复制时,有两种可能的方案。一种是与原来变量共享实例变量的浅复制(shallow copy),即原有变量和复制产生的变量引用相同的变量值;另一种方案就是深复制(deep copy),这种方式将建立实例变量的新的副本。
clone
方法。多重继承:一个对象可以有两个或更多不同的父类,并可以继承每个父类的数据和行为。
父类有重名方法:
父类有重名值:
对于C++如果父类中值为virtual
,则子类对象维护同一个重名值,否则分别维护。
Java和C#支持接口的多重继承,接口不会提供代码,不会因为重名引起冲突。
多态有四种形式:
最常用的软件复用机制:继承和组合。
组合和继承的比较:
重载是在编译时执行的,而改写是在运行时选择的。重载是多态的一种很强大的形式。非面向对象语言也支持。
函数类型签名是关于函数参数类型、参数顺序和返回值类型的描述。类型签名通常不包括接收器类型——因此,父类中方法的类型签名可以与子类中方法的类型签名相同。
范畴定义了能够使名称有效使用的一段程序,或者能够使名称有效使用的方式。(局部变量/public成员)。通过继承创建的新类将同时创建新的名称范畴,该范畴是对父类的名称范畴的扩展。
对于一个程序代码中的任何位置,都存在着多个活动的范畴。(类成员方法同时具有类范畴和本地范畴)。
通过类型签名和范畴可以对重载进行两种分类:
强制、转换和造型:
强制是一种隐式的类型转换,它发生在无需显式引用的程序中:
double x=2.8;
int i=3;
x=i+x;//integer i will be converted to real
转换表示程序员所进行的显式类型转换。在许多语言里这种转换操作称为“造型”:
x=((double)i)+x;
造型和转换既可以实现基本含义的改变;也可以实现类型的转换,而保持含义不变(子类指针转换为父类指针) 。
造型也就是有两种:
Parent a = new Child()
Child b = (Child) new Parent()
子类定义了一个与父类具有相同名称但类型签名不同的方法
子类的方法具有与父类的方法相同的名称和类型签名,可看成重载的特殊情况。
各种语言在如何通过代码实现标识改写这方面存在着差异。
存在两种不同的关于改写的解释方式:
如果方法在父类中定义,但并没有对其进行实现,那么我们称这个方法为延迟(delerred) 方法。延迟方法有时也称为抽象(abstract)方法,并且,在C++语言中通常称之为纯虛方法。
只有当编译器可以确认与给定消息选择器相匹配的响应⽅法时,才允许程序员发送消息给这个对象。
遮蔽是在编译时基于静态类型解析的,并且不需要运行时机制。 C++需要对改写显式声明,如果不使用关键字,将产生遮蔽。
多态变量是指可以引用多种对象类型的变量,这种变量在程序执行过程可以包含不同类型的数值。
多态变量有四种形式:
简单变量
接收器变量:多态变量最常用的场合,用来表示正在执行的方法内部的接收器 (this
, self
)
反多态(向下造型):处理多态变量的过程,判断多态变量能否赋值给子类变量。取消多态赋值的过程,也称为反多态。
纯多态(多态方法):支持可变参数的函数。
通过类型的使用提供了一种将类或者函数参数化的方法。
将名称定义为类型参数,在将来的某一时刻,会通过具体的类型来匹配这一类型参数,这样就形成了类的完整声明。
主要目的之一就是用来指定容器要持有什么类型的对象,而且由编译器来保证类型的正确性。
Java泛型的核心概念:告诉编译器想使用什么类型,然后编译器帮你处理一切细节。
对于一类相似问题的骨架解决方案。通过类的集合形成,类之间紧密结合,共同实现对问题的可复用解决方案。
继承允许进行高级别算法细节的封装,基类不需要改变,由特化子类满足不同的需求。