一个面向对象程序可以组织一个团体,这个团体由一组互相作用的叫做“对象”的代理组成。
每一个对象都扮演一个角色。
并且为团体中的其他成员提供特定的服务或者执行特定的行为。
行为的启动是通过将“消息”传递给对此行为负责的代理(对象)来完成的。
消息对行为对要求进行编码,并且伴随着执行要求所需的附加信息(参数)来一起传递。
接收器:消息发送的对象。如果接收器接受了信息,那么同时它也接受了消息所包含的行为责任。然后接收器响应消息,执行相应的“方法”以实现要求。
提出要求的客户不需要了解要求实现的具体方式。
1. 消息传递有指定的接收器。过程调用没有指定的接收器。
2. 消息的解释(即用于响应消息的方法)由接收器来决定,并且随着接收器的不同而不同。
3. 消息(函数或过程名称)和响应消息的代码段(方法)之间是**后期绑定关系**。传统过程调用中名称与代码段是之间是**早期(编译时或链接时)绑定关系**。
面形对象的一个基本概念就是用责任来描述行为:提高了抽象水平,对象更加独立。
协议:描述与一个对象相关的整个责任的集合。
传统程序的执行通常是对数据结构进行操作(例如改变数组或记录中的域),与之相反,面向对象程序却要求数据结构(即对象)提供服务。
对象是独立存在的客观事物,它由一组属性和一组操作构成。
属性和操作是对象的两大要素。属性是对象静态特征的描述,操作是对象动态特征的描述。
属性一般只能通过执行对象的操作来改变。
操作又称为方法或服务,它描述了对象执行的功能。通过消息传递,还可以为其它对象使用。
允许对象以任何它认为合适的,不干预其他对象的方式来完成任务,而不要干预它。
部分/整体关系中有两种方式:组合和聚合。
组合关系中部分和整体的关系很紧密。
聚合关系中则比较松散,一个部分对象可以属于几个整体对象。
根据抽象的原则对客观事物进行归纳和划分,只关注与当前目标相关的特征,把具有相同特征的事物归为一个类。
它是一个抽象的概念。
类是具有相同属性和相同操作(服务)的对象的集合。它包括属性和操作。
类是对象相关行为的储存库(repository)。即同一个类的所有对象都能执行同样的动作。
所有对象都是类的实例。
在响应消息时调用何种方法由类的接收器决定。
一个特定类的所有对象使用相同的方法来响应相似的消息。
类被组织成有单个根节点的树状结构,称为继承层次结构。
与类实例相关的内存和行为都会被树结构中的后代自动继承。
继承表达了对象的一般与特殊的关系。
特殊类的对象具有一般类的全部属性和服务。
对象之间存在着一般和特殊的结构关系,也就是说它们存在继承关系。很多时候也称作泛化和特化关系。
接收器搜索并执行相应的方法以响应给定的消息。
如果没有找到匹配的方法,搜索就会传导到此类的父类。搜索会在父类链上一直进行下去,直到找到匹配的方法,或者父类链结束。
如果能在更高类层次找到相同名称的方法,所执行的方法就称为改写了继承的行为。
多态性是指一般类中定义的属性和服务,在特殊类中不改变其名字,但通过各自不同的实现后,可以具有不同的数据类型或具有不同的行为。
抽象是指对于一个过程或者一件制品的某些细节有目的的隐藏,以便把其他方面、细节或者结构表达得更加清楚。
抽象,是控制复杂性时最重要的工具。
在抽象表现开发过程中有目的性地忽略细节。
在最高级别上,程序被视为一个对象的“团体”,这些对象间相互作用,以完成共同的目标。
在面向对象程序开发过程中,关于“团体”有两个层次的含义:
许多语言允许协同工作的对象组合到一个“单元”(unit)中。
例如,Java的“包” (packages),C++的“名字空间”(name spaces),Delphi中的“单元”(units)。
这些单元允许某些特定的名称暴露在单元以外,而其他特征则隐藏在单元中。
处理两个独立对象之间的交互。
两个对象间交互时涉及两层抽象:一个对象(服务者, server)向另一个对象(客户, client)提供服务,二者之间以通信来交互。
对象间消息传递。
该级别抽象通常用接口来表示。定义行为,但不描述如何来实现。
考虑抽象行为的具体实现方式。例:多种实现堆栈方式。
关注执行一个方法的具体操作实现。
抽象的思想可以进一步划分为不同的形式。
特化:汽车是由发动机、传动装置、车体、车轮组成;发动机是由 ……(有什么(has a)思想)
分而治之
优点:在某一个层次只关心该层次实现需要的细节即可(前提:设计好了各个配件之间的交互关系)(是什么(is a)思想)
服务:商品制造者关心实现,高层设计者和使用者不关心实现,所以对接口和实现的划分,不仅从高层的角度对设计易于理解,而且使软件组件的替换成为可能。
组合/复合法:由少量简单的形式,根据组合规则构建出新的形式。关键在于既可对初始形式进行组合,也可以对新形式进行组合。
分类:当系统中组件数量变大时,常用分类(Catalogs)来进行组织。
多视角:对一个制品进行不同视角的观察,每一种视角强调特定的细节,而忽略其他部分
汇编语言:最早的抽象机制
过程/函数
模块:解决全局名称空间拥挤问题。
用来改善建立和管理名称集合及其相关数值的一种技术。
模块提供了将名称空间划分成两个部分的能力。
公有部分可以在模块外存取,私有部分只能从模块内存取。
模块不允许实现实例化。实例化是一种能够建立数据区域多份拷贝的能力。
抽象数据类型。
目标:1).定义抽象,创建多个实例;
2).使用实例,知其所提供操作,不必知道如何实现。
抽象数据类型思想的重要进展是最终把接口的概念和实现的概念分离开来。
以服务为中心。
汇编语言和过程:功能为中心;
模块和ADT:数据为中心;(结构、表示、操纵)
面向对象:服务为中心。
消息、继承和多态。
ADT基础上增加的新思想。
实例:表示类的一个具体代表或者范例。
实例变量/数据字段/数据成员(三者同一含义):实例所维护的内部变量。
Java,C#:方法主体直接放在类定义中。
C++:分离
java内部类与cpp嵌套类区别:
java的非静态内部类有个外部类的引用outer,使用这个变量可以引用外部的所有变量,包括private
静态的java内部类也叫做嵌套类,静态的内部类就没有外部的引用了,但是也只能调用外部的静态方法和变量
c++的内部类几乎等同于语法上的嵌套,而C++的内部类不能访问外部类的private变量,想访问的话必须在内部类中声明外部类为friend class (或者在需要访问外部变量的方法的参数中传递外部类引用,或者每个嵌套类对象都包含一个外部类的引用,不过比较浪费资源)
更多知识见
http://blog.csdn.net/a450828540/article/details/8993160
http://blog.csdn.net/a450828540/article/details/9045067
被一个类的所有实例共享的公共数据字段。
Java和C++使用修饰符static创建共享数据字段。
如何对该字段初始化?
对象本身不对共享字段初始化。内存管理器自动将共享数据初始化为某特定值,每个实例去测试该特定值。第一个进行测试的做初始化。
Java:静态数据字段的初始化是在加载类时,执行静态块来完成。
c++:两种方式
详细见“反射”部分
**消息:**要求某个对象执行在它所在的那个类中定义的某个操作的规格说明。是对象间相互请求或相互协作的途径。
**消息传递(messgae passing,有时称为method lookup,方法查询):**请求对象执行一项特定行为的动态过程。
A.b(c);//A是接收器,b是消息选择器,c是参数
//C++, C#, Java,Python, Ruby
aCard.flip ();
aCard.setFaceUp(true);
aGame.displayCard(aCard, 45, 56);
//Pascal, Delphi,Eiffel, Oberon
aCard.flip;
aCard.setFaceUp(true);
aGame.displayCard(aCard, 45, 56);
#Smalltalk
aCard flip.
aCard setFaceUp: true.
aGame display: aCard atLocation: 45 and: 56.
对象之间的相互作用是通过消息产生。消息由某个对象发出,请求其他某个对象执行某一处理或回答某些信息。
静态类型语言:类型和变量联系在一起。(高效性)
动态类型语言:变量看作名称标识,类型和数值联系在一起。(灵活性)
差异:变量或数值是否具备类型这种特性。
在大多数面向对象语言中,接收器并不出现在方法列表中,而是隐藏于方法的定义中。˙只有当必须从方法体内部去存取接收器的数值时,才会使用伪变量(pseudo-variable)。
不需要声明,不能被更改
在使用时就好像作为类的一个实例
默认隐藏:很多编程语言中,对接收器伪变量的使用都可以被忽略。如果在没有引用接收器的条件下,访问一个数据字段或者调用一个方法,那么意味着接收器伪变量将作为消息的主体。
//两者等价
public void flip () { setFaceUp( ! faceUp ); }
public void flip(){this.setFaceUp(!this.faceUp); }
对象自身引用:this隐含指向调用成员函数的对象
参数传递:当某一方法想要把自身作为一个参数传递给另一个参数时,就必须使用伪变量来解决问题。
addActionListener(this);
Java中,对于构造函数,使用this和构造函数的参数进行初始化数据成员。通过显示地使用this,可以区分两个分别用作函数参数和数据成员的同名变量。
在Python,CLOS,Oberon语言中,接收器必须在方法体中显示声明。(对于这些语言,尽管原则上第一个参数可以以任何名称来命名,但是一般都命名为self或者this,以此来表示此方法与接收器伪变量之间的关系)例如在Python中:
aCard.moveTo(27,3)
而相应的方法声明需要三个参数
class PlayingCard:
def moveTo(self,x,y):
...
创建:为一个新对象分配存储空间并且将这段空间与对象名称进行绑定。
初始化:不但包含为对象的数据区域设置初始值,还包括建立操作对象所需的初始条件这个更一般的过程。
一些编程语言允许用户把变量声明和初始化结合起来(Java)
大多数面向对象语言都把变量的命名过程和对象的创建过程分离开来。
//C++
PlayingCard * aCard = new PlayingCard(Diamond, 3);
//Java, C#
PlayingCard aCard = new PlayingCard(Diamond, 3);
#Smalltalk
aCard <- PlayingCard new.
#Python(没有显式使用new操作符)
aCard = PlayingCard(2,3)
两个层次:
两个层次结合在一起。数组由对象组成,而每个对象使用缺省(即无参数)构造函数来初始化。
PlayingCard cardArray[52];
new仅创建数组。数组包含的对象必须独立创建。(典型通过循环来实现)
PlayingCard cardArray[ ] = new PlayingCard[13];
for (int i = 0; i < 13; i++)
cardArray[i] = new PlayingCard(Spade,i+1);
所有面向对象语言在它们的底层表示中都使用指针,但不是所有语言都把这种指针暴露给程序员。
对象引用实际是存在于内部表示中的指针。
按照编译原理的观点,程序运行时的内存分配有三种策略,分别是静态的,栈式(动态)的,和堆式的。
静态存储分配是指在编译时就能确定每个数据目标在运行时刻的存储空间需求,因而在编译时就可以给他们分配固定的内存空间。
这种分配策略要求程序代码中不允许有可变数据结构(比如可变数组)的存在,也不允许有嵌套或者递归的结构出现,因为它们都会导致编译程序无法计算准确的存储空间需求。
栈式存储分配也可称为动态存储分配,是由一个类似于堆栈的运行栈来实现的。
和静态存储分配相反,在栈式存储方案中,程序对数据区的需求在编译时是完全未知的,只有到运行的时候才能够知道,但是规定在运行中进入一个程序模块时,必须知道该程序模块所需的数据区大小才能够为其分配内存。
栈式存储分配按照先进后出的原则进行分配。
静态存储分配要求在编译时能知道所有变量的存储要求
栈式存储分配要求在过程的入口处必须知道所有的存储要求
而堆式存储分配则专门负责在编译时或运行时模块入口处都无法确定存储要求的数据结构的内存分配。(比如可变长度串和对象实例。)
堆由大片的可利用块或空闲块组成,堆中的内存可以按照任意顺序分配和释放。
**构造函数:**用来初始化一个新创建对象的方法。
**优点:**它能确保对象在正确地初始化之前不会被使用。防止多次调用初始化。
Java和C++中,可以通过检查与类显示的名称是否相同来识别构造函数和普通方法。并且构造函数不声明返回值的数据类型。
Java和C#中,数据字段可以初始化为特定的数值,这种赋值独立于构造函数中的参数赋值,数据字段可以在初始化时进行赋值,也可以在后来的构造函数中再次赋值。在C++中,也使用相似的语法来声明静态数据变量或常量。
class Compex{
public double realPart = 0.0;//initialize data areas
public double imagPart = 0.0;
public Complex (double rv){realPart = rv;}
}
构造函数的重载:在C++,C#,Java中,只要每个函数参数的数目、类型或次序不同,就允许多个函数使用相同的名称定义。构造函数经常使用这种定义方式。例如一个构造函数为无参构造,另一个为有参构造。
C++中调用缺省构造函数必须去掉括号,在这里使用括号虽符合语法,但是语义截然不同。
PlayingCard cardFive; // creates a new card
PlayingCard cardSix(); // forward definition for function named cardSix that returns a PlayingCard
PlayingCard cardSeven = new PlayingCard(); // Java
PlayingCard *cardEight = new PlayingCard; // C++
初始化器(C++中):用于对象成员初始化和派生类对基类初始化。
Class PlayingCard {
public:
PlayingCard (Suits is, int ir)
: suit(is), rank(ir), faceUp(true) { }
...
};
C++中几乎所有的类都应该定义4个重要的函数。这被称为正统规范的类(orthodox canonical class)
对于这四个函数,如果用户没有提供相应函数的实现,系统就会自动创建相应函数的缺省版本。
就类对象而言,相同类型的类对象是通过拷贝构造函数来完成整个复制过程的。
#include
using namespace std;
class Test {
public:
Test(int temp){
p1=temp;
}
protected:
int p1;
};
void main() {
Test a(99);
Test b=a;
}
在上面的代码中,我们并没有看到拷贝构造函数,同样完成了复制工作,这又是为什么呢?因为当一个类没有自定义的拷贝构造函数的时候系统会自动提供一个默认的拷贝构造函数,来完成复制工作。
Java中使用final。
C++中使用。
在构造函数中使用一个初始化子句来赋值。
final aBox = new Box(); // can be assigned only once
aBox.setValue(8); // but can change
aBox.setValue(12);
C++中。内存空间开始释放对象,就会自动调用析构函数。
析构函数粉名称为波浪字符“~”加上类的名称。它不需要任何参数,也不会被用户直接调用。
自动变量:当声明变量的函数返回时,变量的空间就会被释放。
动态分配的变量:空间的释放通过操作符delete进行。
Java中,在垃圾回收系统即将回收变量的内存前,才调用finalize方法,由于这项操作可能发生于任何时刻,也可能从不发生,因此,在Java语言中使用finalize方法的情况远不如在C++语言中使用析构函数的情况多。
Java,Smalltalk中类本身是对象。那么什么类代表了对象所属的类别,即这个类是什么类。有一个特殊的类,一般称为Class,这就是类的类。
类是对象。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ppu4QQ6N-1600225751543)(/Users/yuxiangning/Library/Application Support/typora-user-images/image-20200425132345427.png)]
反射(reflection)和内省(introspection)是指程序在运行过程中“了解”自身的能力。
用于反射和内省的技术分为两大类
Java获取类对象:
Class aClass = aVariable.getClass();
C++获取类对象:
typeinfo aClass = typeid(AVariable);
获取父类
Class parentClass = aClass.getSuperclass(); // Java
字符串类名称
char * name = typeinfo(aVariable).name(); // C++
String internalName = aClass.getName();//Java
String descriptiveName = aClass.toString();
检测对象类
Child *c = dynamic_cast<Child *>(aParentPtr);
if (c!=0){ … } //C++
if (aVariable instanceof Child) …
if (aCalss.isInstance(aVariable)) … //Java
通过类建立实例
Object newValue = aClass.newInstance(); // Java
Class forName(string)
Class getSuperClass()
Constructor[] getConstructors()
Field getField(string)
Field[] getFields()
Method[] getDeclaredMethods()
boolean isArray()
boolean isAssignableFrom(Class cls)
boolean isInstance(Object obj)
boolean isInterface()
Object newInstance()
Java和Smalltalk中,将方法看作是可以存取和操纵的对象。
Java中,一个方法是Method类的一个实例。定义了如下操作:
String getName()
Class getDeclaringClass()
Int getModifiers()
Class getReturntype()
Class[] getParameterTypes()
Object invoke(Object receiver,Object[]args)
举例:
Method [ ] methods = aClass.getDeclaredMethods();
System.out.println(methods[0].getName());
Class c = methods[0].getReturnType();
Class sc = String.class;
Class [ ] paramTypes = new Class[1];
paramTypes[0] = sc;
try {
Method mt = sc.getMethod( "concat“ , paramTypes);
Object mtArgs [ ] = { "xyz" };
Object result = mt.invoke("abc", mtArgs);
System.out.println("result is " + result);
} catch (Exception e) {
System.out.println("Exception " + e);
}
Java语言的标准类库定义了一个名称为ClassLoader的类,这个类可以根据存储于文件中的信息来加载一个类。
面向对象语言一般支持以下原则
那么就可以回答类对象属于什么类?
在Java语言中,回答相对比较简单,一个类为Class的实例,也就是自身的一个实例。
//得到某个对象的属性
public Object getProperty(Object owner, String fieldName) throws Exception {
Class ownerClass = owner.getClass();
Field field = ownerClass.getField(fieldName);
Object property = field.get(owner);
return property;
}
//得到某个类的静态属性
public Object getStaticProperty(String className, String fieldName) throws Exception {
Class ownerClass = Class.forName(className);
Field field = ownerClass.getField(fieldName);
Object property = field.get(ownerClass);
return property;
}
//执行某对象的方法
public Object invokeMethod(Object owner, String methodName, Object[] args) throws Exception {
Class ownerClass = owner.getClass();
Class[] argsClass = new Class[args.length];
for (int i = 0, j = args.length; i < j; i++) {
argsClass[i] = args[i].getClass();
}
Method method = ownerClass.getMethod(methodName, argsClass);
return method.invoke(owner, args);
}
//执行某个类的静态方法
public Object invokeStaticMethod(String className, String methodName,Object[] args) throws Exception {
Class ownerClass = Class.forName(className);
Class[] argsClass = new Class[args.length];
for (int i = 0, j = args.length; i < j; i++) {
argsClass[i] = args[i].getClass();
}
Method method = ownerClass.getMethod(methodName, argsClass);
return method.invoke(null, args);
}
//新建实例
public Object newInstance(String className, Object[] args) throws Exception {
Class newoneClass = Class.forName(className);
Class[] argsClass = new Class[args.length];
for (int i = 0, j = args.length; i < j; i++) {
argsClass[i] = args[i].getClass();
}
Constructor cons = newoneClass.getConstructor(argsClass);
return cons.newInstance(args);
}
//判断是否为某个类的实例
public boolean isInstance(Object obj, Class cls) {
return cls.isInstance(obj);
}
//得到数组中的某个元素
public Object getByArray(Object array, int index) {
return Array.get(array,index);
}
创建一个新的隐藏类:元类(metaclass)。
元类是描述类的类。
类也是对象,每个类一定是某个元类的实例。
元类为语言提供了一个方法:
类的继承关系与相应的元类的继承关系是平行的。
如果B是A的子类,则B的元类也是A的元类的子类。
object是根类(无超类),而其相应的元类objectclass还有一个抽象超类class。
类层次结构图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wmEz6vOb-1600225751545)(/Users/yuxiangning/Library/Application Support/typora-user-images/image-20200425141700493.png)]
对应的元类层次结构图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8DMTdLVM-1600225751546)(/Users/yuxiangning/Library/Application Support/typora-user-images/image-20200425141826271.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QxPE6HtD-1600225751548)(/Users/yuxiangning/Library/Application Support/typora-user-images/image-20200425132202953.png)]
类MetaPlayingCard中的行为,只能被对象PlayingCard所理解,而不能被其他对象所理解。对象PlayingCard是类MetaPlayingCard的唯一实例。
类中描述对象的个体性质
元类中描述对象的公共性质
对象的产生有两种基本方式。
一种是以原型(prototype)对象为基础产生新的对象。
一种是以**类(class)**为基础产生新对象。
原型的概念已经在认知心理学中被用来解释概念学习的递增特性,原型模型本身就是企图通过提供一个有代表性的对象为基础来产生各种新的对象,并由此继续产生更符合实际应用的对象。而原型-委托也是OOP中的对象抽象,代码共享机制中的一种。
一个类提供了一个或者多个对象的通用性描叙。从形式化的观点看,类与类型有关,因此一个类相当于是从该类中产生的实例的集合。而这样的观点也会带来一些矛盾,比较典型的就是在继承体系下,子集(子类)对象和父集(父类)对象之间的行为相融性可能很难达到,这也就是OOP中常被引用的——子类型(subtype)不等于子类(subclass)[Budd 2002]。
而在一种所有皆对象的世界观背景下,在类模型基础上还诞生出了一种拥有元类(metaclass)的新对象模型。即类本身也是一种其他类的对象。
以上三种根本不同的观点各自定义了三种基于类(class-based),基于原型(prototype-based)和基于元类 (metaclass-based)的对象模型。
而这三种对象模型也就导致了许多不同的程序设计语言(如果我们暂时把静态与动态的差别放在一边)。是的, 我们经常接触的C++ ,Java都是使用基于类的对象模型,但除此之外还有很多我们所没有接触的OOPL采用了完全不一样的对象模型,他们是在用另外一种观点诠释OOP的内涵。
继承作为一种扩展同时也作为一种收缩的思想,正是面向对象技术强大的原因,同时也会在正常的部署中引起混淆。
继承总是向下传递的,因此一个类可以从它上面的多个超类中继承各种属性 。派生类可以覆盖从基类继承来的行为。
is-a检验(“是一个”检验):检验两个概念是否为继承关系。
继承的作用:
private:只能用于父类内部。
public:可用于类定义外部。
protected(注意区别):
Java中:默认对本包和所有子类可见。
C++中:只能被本类或者子类访问。
可见/访问性 | 在同一类中 | 同一包中 | 不同包中 | 同一包子类中 | 不同包子类中 |
---|---|---|---|---|---|
public | yes | yes | yes | yes | yes |
protected | yes | yes | no | yes | yes |
package | yes | yes | no | yes | no |
private | yes | no | no | no | no |
面向对象编程语言可以分为两类:
静态类型语言中,父类的数据类型和子类(或派生类)的数据类型之间的关系:
替换原则:指如果类B是类A的子类,那么在任何情况下都可以用类B来替换类A,而外界毫无察觉。
子类:一个类是通过继承创建的。
子类型:符合替换原则的子类关系。区别于一般的可能不符合替换原则的子类关系。
所有面向对象编程语言都支持替换原则,尽管有些语言在改写方法时需要附加的语法。(C++语言例外,对于C++语言,只有指针和引用真正地支持替换原则,声明为值(不是指针)的变量不支持替换原则)
静态类型语言比动态类型语言更加强调替换原则。
可替换性是面向对象编程中一种强大的软件开发技术。
可替换性:变量声明时指定的类型不必与它所容纳的值类型相一致。
这在传统的编程语言中是不允许的,但在面向对象的编程语言中却常常出现。
如果说新类是已存在类的子类型,那么这个新类不仅要提供已存在类的所有操作,而且还要满足于这个已存在类相关的所有属性。
因此,即使符合堆栈的接口定义,但是不满足堆栈的属性特征,也不是子类型。
子类型关系是通过行为这个术语描述的,与新类的定义或构造无关。(可以不是继承)
例如:Dictionary类支持与Array类相同的接口,因此即使Dictionary类与Array类之间并不存在继承关系,但是也可以说Dictionary是Array的子类型。
子类有时为了避免继承父类的行为,需要对其进行改写。
语法上:子类定义一个与父类有着相同名称且类型签名相同的方法。
运行时:变量声明为一个类,它所包含的值来自于子类,与给定消息相对应的方法同时出现于父类和子类。
改写与替换结合时,想要执行的一般都是子类的方法。
Java,C#:abstract
C++:virtual(纯虚方法,并赋值为0)
abstract class 在 Java 语言中表示的是一种继承关系,一个类只能使用一次继承关系。但是,一个类却可以实现多个interface.
在abstract class 中可以有自己的数据成员,也可以有非abstarct的成员方法,而在interface中,只能够有静态的不能被修改的数据成员(也就是必须是 static final的,不过在 interface中一般不定义数据成员),所有的成员方法都是abstract的。
abstract class和interface所反映出的设计理念不同。其实abstract class表示的是"is-a"关系,interface表示的是"like-a"关系。
实现抽象类和接口的类必须实现其中的所有方法。抽象类中可以有非抽象方法。接口中则不能有实现方法。
接口中定义的变量默认是public static final 型,且必须给其初值,所以实现类中不能重新定义,也不能改变其值。
抽象类中的变量默认是 friendly 型,其值可以在子类中重新定义,也可以重新赋值。
接口中的方法默认都是 public,abstract 类型的。
很多情况下,都是为了特殊化才使用继承。
在这种情况下,基类有时也被称为抽象规范类。
规范化继承可以通过以下方式辨认:基类中只是提供了方法界面,并没有实现具体的行为,具体的行为必须在派生类中实现。
树-独木舟
堆栈-队列
写二进制文件-写学生信息文件
构造子类化经常违反替换原则(形成的子类并不是子类型)
泛化子类化通常用于基于数据值的整体设计,其次才是基于行为的设计。
双向队列-〉堆栈
控制机械鼠标=控制轨迹球
创建匿名类(也称为类定义表达式)的条件:
p.add(new ButtonAdapter(“Quit”){
public void pressed(){System.exit(0);}
}
);
实际上匿名类是一个新类的定义。如上,这个匿名类是基于ButtonAdapter的子类,重写了pressed方法,并且创建了一个关于这个子类的实例。这个新类所需的所有方法都由匿名函数给出。
继承使得构造函数这个过程变得复杂
由于父类和子类都有待执行的初始化代码,在创建新对象时都要执行
Java等语言
C++
是通过在初始化时使用父类的名称来实现这一功能的。
Python
不会自动调用父类的初始化方法。
C++中,如果不将虚构函数声明为virtual,将无法正确调用子类的析构函数。
如果指向父类的指针变量指向子类的一个实例并对此变量进行释放(通过delete语句),那么将只调用父类的析构函数。如果将父类的析构函数声明为virtual,那么父类的析构函数和子类的析构函数都将执行。
class Parent{
public:
~Parent(){ cout << "in parent\n"; }
};
class Child : public Parent{
public:
~Child(){ cout << "in child\n"; }
}
Parent *p = new Child();
delete p;
//输出
in parent
C++中将析构函数声明为虚拟是一个良好的习惯。
**静态:**用来表示在编译时绑定于对象并且不允许以后对其进行修改的属性或特征。
**动态:**用来表示直到运行时绑定于对象的属性或特征。
Object obj = new Dog();
在上述代码中,Object
是静态类,Dog
则是动态类
对于静态类型面向对象编程语言,在编译时消息传递表达式的合法性不是基于接收器的当前动态数值,而是基于接收器的静态类来决定的。
如果有
class Animal{
public void speak(){ System.out.print("Animal speak");}
}
class Dog extends Animal{
public void speak(){ bark();}
public void bark(){ System.out.print("Woof!");}
}
则
Dog fido;
fido = new Dog();
fido.speak();//输出 Woof!
fido.bark();//输出 Woof!
//but
Animal pet;
pet = fido;
pet.speak();//输出 Woof!
pet.bark();//编译时错误
Java中,如果父类是被子类实例化的(向上转型),且子类重写了父类中的某个方法,此时父类调用这个方法,是被子类重写之后的方法。要想调用父类中被重写的方法,则必须使用关键字super。
向上造型语法:
//java
Animal aPet = ...;
if (aPet instanceof Dog){
...
}
//cpp
Animal * aPet = ...;
Dog * d = dynamic_cast<Dog *>(aPet);
if (d != 0){
...
}
做出数值(变量)是否属于指定类(使用instanceof等)的决定之后,通常下一步就是将这一数值的类型由父类转换为子类。这一过程称为向下造型,或者反多态,因为这一操作所产生的效果恰好与多态赋值的效果相反。
几种语言的函数在造型转换成功时返回有效结果,在造型转换非法时返回空值。
//java
Animal aPet;
Dog d;
d = (Dog)aPet;
//cpp
Animal * aPet = ...;
Dog * d = dynamic_cast<Dog *>(aPet);
if (d != 0){
...
}
响应消息时对哪个方法进行绑定是由接收器当前所包含的动态数值来决定的。
在程序执行前方法已经被绑定(也就是说在编译过程中就已经知道这个方法到底是哪个类中的方法),此时由编译器或其它连接程序实现。例如:C。
针对Java简单的可以理解为程序编译期的绑定;这里特别说明一点,java当中的方法只有final,static,private和构造方法是前期绑定。
在运行时根据具体对象的类型进行绑定。
若一种语言实现了后期绑定,同时必须提供一些机制,可在运行期间判断对象的类型,并分别调用适当的方法。也就是说,编译器此时依然不知道对象的类型,但方法调用机制能自己去调查,找到正确的方法主体。
不同的语言对后期绑定的实现方法是有所区别的。但我们至少可以这样认为:它们都要在对象中安插某些特殊类型的信息。
动态绑定的过程:
更详细的关于静态绑定和动态绑定的介绍,可以看
如果方法所执行的消息绑定是由最近赋值给变量的数值的类型来决定的,那么就称这个变量是多态的。
Java,Smalltalk等变量都是多态的。
C++声明为简单类型的变量,非多态。
C++中同时满足
1.使用指针或引用;
2.相关方法声明为virtual;
才可以实现多态消息传递。
Java中
class Animal{
public void speak(){ System.out.print("Animal speak");}
}
class Dog extends Animal{
public void speak(){ bark();}
public void bark(){ System.out.print("Woof!");}
}
class Bird extends Animal{
public void speak(){ System.out.print("Tweet!");}
}
Animal pet;
pet = new Dog();
pet.speak();//输出 Woof!
pet = new bird();
pet.speak();//输出 Tweet!
C++中
class Animal{
public:
virtual void speak(){
cout << "Animal Speak !\n";
}
void reply(){
cout << "Animal Reply !\n";
}
}
class Dog: public Animal{
public:
virtual void speak(){
cout << "Woof! \n";
}
void reply(){
cout << "Woof again!\n";
}
}
class Bird : public Animal{
public:
virtual void speak(){
cout << "Tweet !\n";
}
}
简单类型不多态:
Animal a;
Dog b;
b.speak();//输出 Woof!
a = b;
a.speak();//输出 Animal speak!
Bird c;
c.speak();//输出 Tweet!
a = c;
a.speak();//输出 Animal speak!
指针和引用的对象是多态的:
Animal * d;
Dog b;
d = &b;
(*d).speak();//输出 Woof!
Bird c;
d = & c;
d->speak();//输出 Tweet!
Animal & e;
Dog b;
&e = b;
e.speak();//输出 Woof!
Bird c;
&e = c;
d->speak();//输出 Tweet!
如果省去virtual,指针所指向的对象引用就将不再是多态的了。例如reply方法:
Animal * g;
Dog b;
g = &b;
(*g).reply();//输出 Animal reply!
Bird c;
g = & c;
g->reply();//输出 Animal reply!
//如果接收器的静态类是子类,而不是父类,那么在子类中修改的非虚拟方法也将得以执行。
b.reply();//输出 Woof again!
为了防止采用这种策略时因为多态而引发的程序错误(具体参照P179),C++改变了虚拟方法的调用规则:
区分:“分配策略”见第四章中“指针与内存分配”的“内存分配”
**浅复制(shallow copy):**共享实例变量。
深复制(deep copy):
建立实例变量的新的副本。
一个对象A,在某一时刻A中已经包含了一些有效值,此时可能会需要一个和A完全相同新对象B,并且此后对B任何改动都不会影响到A中的值,也就是说,A与B是两个独立的对象,但B的初始值是由A对象确定的。这种过程便是克隆。
在Java语言中,用简单的赋值语句是不能满足这种需求的。要满足这种需求虽然有很多途径,但实现clone()方法是其中最简单,也是最高效的手段。
下面的例子包含三个类UnCloneA,CloneB,CloneMain。CloneB类包含了一个UnCloneA的实例和一个int类型变量,并且重载clone()方法。CloneMain类初始化CloneB类的一个实例b1,然后调用clone()方法生成了一个b1的拷贝b2。最后考察一下b1和b2的输出:
package clone;
class UnCloneA {
private int i;
public UnCloneA(int ii) { i = ii; }
public void doubleValue() { i *= 2; }
public String toString() {
return Integer.toString(i);
}
}
class CloneB implements Cloneable{
public int aInt;
public UnCloneA unCA = new UnCloneA(111);
public Object clone(){
CloneB o = null;
try{
o = (CloneB)super.clone();
}catch(CloneNotSupportedException e){
e.printStackTrace();
}
return o;
}
}
public class CloneMain {
public static void main(String[] a){
CloneB b1 = new CloneB();
b1.aInt = 11;
System.out.println("before clone,b1.aInt = "+ b1.aInt);
System.out.println("before clone,b1.unCA = "+ b1.unCA);
CloneB b2 = (CloneB)b1.clone();
b2.aInt = 22;
b2.unCA.doubleValue();
System.out.println("=================================");
System.out.println("after clone,b1.aInt = "+ b1.aInt);
System.out.println("after clone,b1.unCA = "+ b1.unCA);
System.out.println("=================================");
System.out.println("after clone,b2.aInt = "+ b2.aInt);
System.out.println("after clone,b2.unCA = "+ b2.unCA);
}
}
/** RUN RESULT:
before clone,b1.aInt = 11
before clone,b1.unCA = 111
=================================
after clone,b1.aInt = 11
after clone,b1.unCA = 222
=================================
after clone,b2.aInt = 22
after clone,b2.unCA = 222
*/
输出的结果说明int类型的变量aInt和UnCloneA的实例对象unCA的clone结果不一致,int类型是真正的被clone了,因为改变了b2中的aInt变量,对b1的aInt没有产生影响,也就是说,b2.aInt与b1.aInt已经占据了不同的内存空间,b2.aInt是b1.aInt的一个真正拷贝。相反,对b2.unCA的改变同时改变了b1.unCA,很明显,b2.unCA和b1.unCA是仅仅指向同一个对象的不同引用!从中可以看出,调用Object类中clone()方法产生的效果是:先在内存中开辟一块和原始对象一样的空间,然后原样拷贝原始对象中的内容。对基本数据类型,这样的操作是没有问题的,但对非基本类型变量,我们知道它们保存的仅仅是对象的引用,这也导致clone后的非基本类型变量和原始对象中相应的变量指向的是同一个对象。
大多时候,这种clone的结果往往不是我们所希望的结果,这种clone也被称为“影子clone”。要想让b2.unCA指向与b2.unCA不同的对象,而且b2.unCA中还要包含b1.unCA中的信息作为初始信息,就要实现深度clone。
默认的克隆方法为浅克隆,只克隆对象的非引用类型成员。
怎么进行深度clone?
把上面的例子改成深度clone很简单,需要两个改变:
一是让UnCloneA类也实现和CloneB类一样的clone功能(实现Cloneable接口,重载clone()方法)。
二是在CloneB的clone()方法中加入一句o.unCA = (UnCloneA)unCA.clone();
Java中常用的拷贝操作有三个:
int x=1;
int y=x;
y=2;
x is 1
y is 2
Integer a=1;
Integer b=a;
b=2;
a is 1
b is 2
String m="ok";
String n=m;
n="no";
m is "ok";
n is "no";
Integer a=1;
Integer b=new Integer(a);
b=2;
a is 1
b is 2
String m="ok";
String n=new String(m);
n="no";
m is "ok“;
n is "no";
public class Person implements Cloneable {
private int age;
private String name;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
void setName(String name) {
this.name = name;
}
public static void main(String[] args) {
Person p = new Person();
p.setAge(32);
p.setName("陈抒");
Person p2 = p;
p2.setAge(33);
p2.setName("老陈");
System.out.println(p.getAge());
System.out.println(p.getName());
System.out.println(p2.getAge());
System.out.println(p2.getName());
}
}
//输出:
33
老陈
33
老陈
可查找关于“Java中的深拷贝和浅拷贝”了解更多
指一个对象可以有两个或更多不同的父类,并可以继承每个父类的数据和行为。
解决方法一:
使用全限定名
GraphicalCardDeck gcd;
Card *aCard = gcd->CardDeck::draw();
Card *aCard = gcd->GraphicalObject::draw();
不够理想:
解决方案二:
使用重定义和重命名的结合
class GraphicalCardDeck : public CardDeck, public GraphicalObject {
public:
virtual Card*draw () { return CardDeck::draw(); }
virtual void draw(Graphics *g) { GraphicalObject::draw(g); }
}
GraphicalCardDeck gcd;
Graphis g;
gcd->draw(); // selects CardDeck draw
gcd->draw(g); // selects GraphicalObject draw
class GraphicalCardDeck : public CardDeck, public GraphicalObject {
public:
virtual void draw () { return CardDeck::draw(); }
virtual void paint () { GraphicalObject::draw(); }
}
GraphicalCardDeck gcd;
gcd->draw(); // selects CardDeck draw
gcd->paint(); // selects GraphicalObject draw
名称重定义仅解决了单独使用GraphicalCardDeck类时的部分问题。
考虑使用替换原则带来的问题?
GraphicalObject处于图形对象组成的列表中
GraphicalObject * g = new GraphicalCardDeck();
g->draw(); // opps, doing wrong method!
希望执行显示图像,结果执行了CardDeck中对应的draw方法,而不是图形操作。
在C++语言中,解决这一问题的典型方法就是引入两个新的辅助类。并且使用不同的方法名称来重定义draw操作。
Class CardDeckParent : public CardDeck {
Public :
virtual void draw () { cardDeckDraw ();}
virtual void cardDeckDraw () { CardDeck :: draw ();}
};
Class GraphicalObjectParent : public GraphicalObject {
Public :
virtual void draw () { goDraw ();}
virtual void goDraw () {GraphicalObject :: draw ();}
};
Class GraphicalCardDeck : public CardDeckParent, GraphicalObjectParent {
Public :
virtual void cardDeckDraw () { }
virtual void goDraw () { }
};
GraphicalCardDeck * gcd = new GraphicalCardDeck();
CardDeck * cd = gcd;
GraphicalObject * go = gcd;
cd->draw();//execute cardDeckDraw
go->draw();//execute goDraw
gcd->draw();//error
Java,C#语言都不支持类的多重继承,但它们都支持接口的多重继承。
对于子类来说,接口不为其提供任何代码,所以不会产生两部分继承代码之间的冲突。
interface CardDeck {
public void draw ()
}
interface GraphicalObject {
public void draw ()
}
class GraphicalCardDeck implements CardDeck, GraphicalObject {
public void draw (){ … } //Only one method
}
interface CardDeck {
public void draw ();
}
interface GraphicalObject {
public void draw (Graphics g);
}
class GraphicalCardDeck implements CardDeck, GraphicalObject {
public void draw (){ … }
public void draw (Graphics g){ … }
}
interface CardDeck {
public void draw () throws EmptyDeckException;
}
interface GraphicalObject {
public void draw ();
}
class GraphicalCardDeck implements CardDeck, GraphicalObject {
public void draw (){ … } //error
}
子类应该拥有公共基类数据成员的一份还是两份拷贝? 实际中两种现象都存在。
情况一:
class Link{
public:
Link *nextLink;
}
class CardDeck:public Link{..};
class GraphicalObject:public Link{..}
当创建同时继承自CardDeck类和GraphicalObject类的子类时,这个子类应该包含多少个nextLink字段?
假如纸牌列表和图形对象列表是不同的,那么每种类型的列表都应该有各自的链接。因此,子类用于两个独立的链接字段看起来更为恰当。
情况二:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yGo0obYb-1600225751549)(/Users/yuxiangning/Desktop/图片1.png)]
输入输出流既是输入流的派生,也是输出流的派生。但是,只存在一个对象,两个文件指针引用相同的数值。即指需要公共祖先的一份拷贝。
class Stream{
File *fid
}
class InStream : public virtual Stream{
int open(File *)
..};
class OutStream : public virtual Stream{
int open(File *)
..};
class InOutStream : public InStream, public OutStream {
int open(File *)
..};
在C++语言中,这个问题通过在父类列表中使用virtual修饰符来解决。
关键字virtual意味着在当前派生类中,超类可以出现多次,但只包含超类的一份拷贝。
它需要将处于中间状态的父类而不是最终的结合类,指定为虚拟类型。
构造函数也遵从先祖先(基类),再客人(成员对象),后自己(派生类)的原则,有多个基类之间则严格按照派生定义时从左到右的顺序来排列先后。析构函数调用正好相反。
虚基类:是用关键字virtual声明继承的父类,即便该基类在多条链路上被一个子类继承,但是该子类中只包含一个该虚基类的备份,虚基类主要用来解决继承中的二义性问题,这就是是虚基类的作用所在。
一般情况下,派生类的构造函数只需负责对其直接基类初始化,再由直接基类负责对间接基类初始化。
而对虚基类的派生类:
代码一:
#include
using namespace std;
class A
{
public:
A(){cout<< 'a';}
int a;
};
class B: public A
{
public:
B(){cout<< 'b';}
int b;
};
class C: public A
{
public:
C(){cout<< 'c';}
int c;
};
class D: public B, public C
{
public:
D(){cout<< 'd';};
int d;
};
int main()
{
D d;
return 0;
}
//abacd
程序输出abacd,调用类D的构造函数会先调用类D的直接基类的构造函数,因为按基类出现的顺序调用构成函数,所以先调用类B的构造函数,同理:调用类B的构造函数,先调用类A的构造函数,所以输出abacd。
代码二:
<pre name="code" class="cpp">#include <iostream>
using namespace std;
int t= 0;
class A
{
public:
A(){cout<< 'a'; t++;}
A(int i){cout<< 'a'+ i; t++;}
int a;
};
class B: virtual public A
{
public:
B():A(7){cout<< 'b';}
int b;
};
class C: virtual public A
{
public:
C():A(5){cout<< 'c';}
int c;
};
class D: public B, public C
{
public:
D():B(),C(){cout<< 'd';};
int d;
};
int main()
{
D d;
cout<< t;
return 0;
}
//abcd
类D的构造函数通过初始化调用了虚基类的构造函数A,然后再调用类B和类C的构造函数。
D对象中只含一个A的对象数据。
不会, 因为C++编译器只执行最后的派生类对虚基类的构造函数的调用,而忽略基类的其他派生类对虚基类的构造函数的调用。
将D类改成如下即可
class D: public B, public C
{
public:
D():A(2),B(),C(){cout<< 'd';};
int d;
};
见《面向对象编程导论》原书第3版 中文版 P204。
Java中的嵌套类可以访问外部类的方法。C++语言禁止这样使用,但可以通过构建内部类时,传递外部类的引用参数来模拟这一行为。
为了创建一个继承于两个父类的对象,可以使外部类继承自第一个父类,而内部类继承于第二个父类。外部类改写来自第一个父类的方法。内部类改写来自第二个父类的方法。
Class GraphicalCardDeck extends CardDeck {
public void draw ( ) { //外部类改写CardDeck的draw()方法
}
private drawingClass drawer= new drawingClass();
public GraphicalObject myDrawingObject () {return drawer;}
private class drawingClass extends GraphicalObject {
public void draw ( ) {//内部类改写GraphicalObject的draw()方法
}
}
}
替换问题:GraphicalCardDeck类的实例可以赋值给类型为CardDeck的变量,却不能赋值给类型为GraphicalObject的变量。
解决方案:外部类可以返回一个关于内部类的实例,并赋值给父类变量。
Class overloader{
//three overloaded meanings for the same name
public void example (int x){……}
public void example (int x,double y){……}
public void example (string x){……}
}
Class parent{
public void example(int x){……}
}
Class child extends parent{
//same name,different method body
public void example(int x){……}
}
Parent p=new child();//declared as parent,holding child value
Template <class T> T max(T left,T right)
{
//return largest argument
if (left<right)
return right;
return left;
}
两种常用的软件复用机制:
举例:书P209:List和set例子
函数类型签名是关于函数参数类型、参数顺序和返回值类型的描述。
类型签名通常不包括接收器类型。因此,父类中方法的类型签名可以与子类中方法的类型签名相同。
对于一个程序代码中的任何位置,都存在着多个活动的范畴。
类成员方法同时具有类范畴和本地范畴
通过类型签名和范畴可以对重载进行两种分类:
多个过程(或函数、方法)允许共享同一名称,且通过该过程所需的参数数目、顺序和类型来对它们进行区分。即使函数处于同一上下文,这也是合法的。
class Example{
//same name,three different methods
int sum(int a){return a;}
int sum(int a,int b){return a+b;}
int sum(int a,int b,int c){return a+b+c;}
}
关于重载的解析,是在编译时基于参数值的静态类型完成的。涉及运行时机制。【重要】
Class Parent { };
Class Child : public Parent { };
void Test(Parent *p) { }
void Test(Child *c) { }
Parent * value = new Child( );
//Test(value);会执行第一个方法!
通过重载可以将一个库函数扩展成支持用户定义的数据类型的库函数。
Ostream & operator << (ostream & destination, Fraction & source )
{
destination << source.numerator()<<“/”<<source.denominator();
return destination;
}
强制是一种隐式的类型转换,它发生在无需显式引用的程序中。
double x=2.8;
int i=3;
x=i+x;//integer i will be converted to real
转换表示程序员所进行的显式类型转换。在许多语言里这种转换操作称为“造型”。
x=((double)i)+x;
造型和转换既可以实现基本含义的改变(例如将实数变为整数);也可以实现类型的转换,而保持含义不变(子类指针转换为父类指针)。
//x是y的父类
//上溯造型
X a=new X();
Y b=new Y();
a=b; //将子类对象造型成父类对象,相当做了个隐式造型:a = (X)b;
//下溯造型
X a=new X();
Y b=new Y();
X a1=b
Y b1=(Y)a1
当一个语句涉及隐式转换、显示转换和面向对象数值的替换时,用来解决重载函数名称的算法将变得非常复杂。通常,这个算法至少包含一下几个步骤:
相同的名称可以在不引起歧义且不造成精度损失的情况下出现于多个不同的范畴。
并不一定语义要相关。
Java使用融合模型,对所有可能的方法进行检测,选择最匹配的方案。
class Parent {
public void example (int a)
{System.out.println(“in parent method”);}
}
class Child extends Parent {
public void example (int a,int b)
{System.out.println(“in child method”);}
}
//main方法中:
Child aChild = new Child();
aChild.example(3);
对于Java和C#来说会调用父类方法。
C++使用分级模型,即在名称定义所在的范畴内进行匹配。
上述逻辑的代码在C++中会编译出错,解决的方法是在Child
类中增加一个一个参数的example()
方法
class Parent {
public void example (int a)
{System.out.println(“in parent method”);}
}
class Child extends Parent {
public void example (int a)
{Parent::example(a);}
public void example (int a,int b)
{System.out.println(“in child method”);}
}
如果子类的方法具有与父类的方法相同的名称和类型签名,称子类的方法改写了父类的方法。
各种语言在如何通过代码实现标识改写这方面存在着差异。(Java不需要,C++需要virtual)
改写并不能改变方法的可存取性。如果一个方法在父类中为public,那么不允许在子类中将该方法声明为private。反之亦然。
两种不同的关于改写的解释方式:
这两种形式的改写都很有用,并且经常在一种编程语言内同时出现。如:几乎所有的语言在构造函数中都使用改进语义。
如果方法在父类中定义,但并没有对其进行实现,那么我们称这个方法为延迟方法。
延迟方法有时也称为抽象方法,并且在C++语言中通常称之为纯虚方法。
延迟方法的一个优点就是可以使程序员在比实际对象的抽象层次更高的级别上考虑与之相关的活动。
延迟方法更具实际意义的原因:在静态类型面向对象语言中,对于给定对象,只有当编译器可以确认与给定消息选择器相匹配的响应方法时,才允许程序员发送消息给这个对象。
Java,C#:abstract
C++:virtual = 0