首页 > 代码库 > 第十章 类、对象与实现
第十章 类、对象与实现
一、变量与表
1、变量
变量:用来标识(identify)一块内存区域,这块区域的内容是可变的。
变量名:是一个标识符(identifier),用来指代一块内存区域,即变量。
CPU是用内存地址来标识一块内存区域的,机器码中,是不会出现变量名的;出现的是逻辑相对地址。在汇编层次上,操作的都是地址,不存在任何名称;变量名是给我们程序员操作内存来使用的。简单说,变量就是地址;在对程序编译连接时由编译系统给每一个变量名分配相应的内存逻辑地址,形成符号表;该地址分配后不可改变。当你调用此变量时,编译器就会根据此符号表找到对应的逻辑地址,然后翻译成汇编指令。例如从R2寄存器为基址的表中读一个变量所在行的内容到行寄存器H5,翻译成汇编:MOV
2、内存区域
APO在内存分配时会涉及到以下区域:
◆寄存器:用于自动变量,如方法里的局部变量。
◆栈:存放临时的基本类型数据、方法的局部变量。
对象的引用(包括动态对象)、动态分配内存的变量引用,局部静态变量等都应该保存在对象的属性表中;但动态对象、动态变量的身体是存放在堆中。栈是会自动清除或覆盖数据的,适合自动变量。APO对堆内存的操作和对栈内存的操作速度是一样的,比寄存器操作稍微慢点;即使是动态内存分配和释放(有专用的硬件指令)不过是几个ns。
当在一段代码块定义一些变量时,编译器就在栈中为这些变量分配内存空间,当这些变量退出该作用域后,栈会自动释放掉(重新设置栈SP寄存器值)为这些变量所分配的内存空间,该内存空间可以立即被另作他用。栈中的数据大小和生命周期是可以确定的。
在APO中,一个进程内只有一个用户栈,所有的对象(包括线程对象)都是共用一个栈。栈的大小,是全局变量;默认是2个页,256行;如果对象数目多或很多线程,就可设大些;最大是32KH。为何JAVA等是每个线程需要一个2MB栈,大量占用内存?我认为这是设计问题。在APO中,对象消息处理代码块内只是一些少量的自动变量;普通对象完成消息处理后就会自动清除,相对来说不占空间;而线程对象可能会因等待消息,从而暂时让出CPU,但并不让出占据栈内的那部分空间;就可能会同时有多个线程占据栈空间。解决办法就是增大栈空间吧;APO中一个进程就算有6万个对象在活动,半个数据块的栈空间就足够了。编译器将自动变量翻译成对栈指针(0地址)的相对偏移地址。
◆堆:存放用动态产生的数据
堆内存用来存放由动态创建的对象和数组等动态变量。在堆中分配的内存,由APO的内存管理员来管理或由用户代码释放。动态声明(用DYV标识符)一个变量,DYVBU64KH
在堆中产生了一个动态数组或动态对象后,还可以在相应对象属性表中定义一个特殊的变量,让对象属性表中这个变量的取值最终找到数组或对象在堆内存中的首地址,对象属性表中的这个变量就成了数组或对象的引用变量。
◆常量池:存放常量(一般放在方法表区)
3、表
1)、类表
金字塔形的树状类表,有三个层次:
1))、纯抽象类:只有抽象属性和抽象方法;接口是只有抽象方法的纯抽象类。只有抽象属性的类为概念类。
2))、普通抽象类:可有抽象属性和普通属性,可有抽象方法和普通方法。
3))、具体类:只有普通属性和普通方法。
接口调用是:对象名.接口方法名字();
但编译器如发现该对象所属的类有相同的方法名,就直接编译成直接调用对象所属类的方法了。所以,接口实例化的好处只是知道接口之间,类与接口之间的继承关系吧。
类表:以类号顺序排列的类项构成的表;每类项占4行存储空间;最多8KH。
类项:前2行为类继承关系字数组,可标识32个父类以上的类号。第3行是子类变量,可标识16个下级子类号。对于类继承关系变量或子类变量;如果第一个字符是0xA5A5,表示多于32个继承关系或16个子类,需要动态变量来扩充;随后的3个字符是动态变量指针(所占行数、块号、行开始地址)。对于抽象类,第4行也属子类变量。对于具体类,第四行:16位一个参数,方法表指针(方法表所在的块号、方法表的行开始地址),方法数,方法表所占的行数;本类的对象数,属性表的属性项数,属性表所占的行数。
16位的类号:高3位为类的类型,从111到000依次是:根类、纯抽象类、概念类、接口、普通抽象类、包含类(也是小根类)、具体类、类库(只有方法的具体类)。最低2位为00;指向该类项的第0行地址。中间十一位是类的序号,最多标识到2K个类。
对象头表:以16位对象号顺序排列的1W对象属性表入口指针(块号,行开始地址)构成的表;最多64K个对象占用8KH。
在自然界中,存在就是合理;我们不应改变自然规则,使自己处于狭窄的时空。
2)、编译器符号表简单介绍
表变量BBL对应的地址值:
BUxHBBL{};//高16位行数x,低16位行开始相对地址;块号在对象头表。
元素或属性或说是变量的声明:BUxW
BUxW声明了变量的数据内容包含x个字,或说是2x个字符串。BUx 声明了包含x个位,BUxK 声明了包含x K个位,BUxM 声明了包含x M(百万)个位。在APO中,位容器的声明通常以行(256位)为单位的;因为APO包含有256位为单位的位处理器。
对象属性表的存储布局:
BU19H表1{ // 19行的表变量。(高16位为行数x,低16位为行开始地址)
DYV
// 行开始地址是第0行;行的字地址0;引用变量最小占一行,一行8个引用。
// 行开始地址是第1行,行字地址为0,长度99,占行数13行,最后行空5W。
// D变量行开始地址是第14行;行的字地址为0,长度32W,占行数4行。
引用变量内容定义:高16位是动态变量所占行数,低16位为对象号。动态对象序号在对象号区,0xC000- 0xFFFF。8K
表内的变量可看作全是1W的数组,对于静态表来说;要实现不同宽度数组就要定义表内表了。而动态数组就靠程序员了;当然,对于动态数组;编译器还是会多编译一条传送数组的16位宽度的成员项数和成员位宽度到CPU的寄存器B的指令的。
编译器翻译原则:
1)、先编排4W的变量在一起,其次是2W的变量;因它们可能是数值,也易于对齐。动态变量的所有引用变量(1W一个)放在一起成字数组,放于表头,最少占1行(8个)。
2)、多于4W的变量地址都是以行地址开始。
3)、最后是在有空洞的行插入1W/2W/3W的变量地址。
表内变量对应的地址值定义:
BU32 BL;
// T标志为1是变量的内容长度小于1行; Bit29–24的6位为长度及行的字开始地址。Bit30为1为引用变量,Bit29–27:111数组、110向量;100为对象引用等。Bit26–24的3位为行的字开始地址。变量类型属性字节Bit23–16用于特殊对象。
编译器翻译后的变量地址:不到一行的(8位,8位,16位)一组,保留字节为0,超过一行的(16位,16位)一组。
E = (10 100000,0,18);F= (10 010 100,0,18);
G = (11XXX 000,0,0);B = (99,1);D = (32,14);
A = (10 001100,0,13);C = (10 001110,0,13);
一个表必须完全被包在一个数据块里,一个数据块是64KH。我们可以在表内或代码块内声明一个动态变量为一个数据块,甚至连续数据块变量;而在表内引用该变量。但最长的字符串只有1M个字符,占一个数据块。一个数据块可以装多个表;或许有空洞。我认为内存合理分配,也应该需要程序员参与。字符串,数组,代码段在表内只是一个内容不超过32KW的变量。变量、对象的名字等可以用汉语双拼简写或英文简写或直接中文也行,APO编译器支持U编码的。
方法(method)表声明:
BUNH 方法表名字{//变量地址(方法表的16位行数,方法表的16位起始行地址)
BUxW 方法1; // 方法1变量地址(0,15位方法的指令长度x,方法的起始行地址)
….
}
方法表中的方法是一个接着一个的以行为单位紧密存放的。调用方式是:
类名(或对象名字).方法名字;
二、面向对象编程
我们可以这样说:万物皆对象,在程序里一切皆变量(对象、方法、属性、表)。
面向过程的思想:由过程、步骤、函数组成,以过程为核心;面向过程是先有算法,后有数据结构。
面向对象的思想:以对象为中心,先开发类,得到对象,通过对象之间相互通信实现功能。面向对象是先有数据结构,然后再有算法。
我们知道同一个类的对象可以有很多个,对应的对象属性表也有很多;但只有一个方法表。具有相同或相似性质的对象的抽象就是类。因此,对象的抽象是类,类的具体化就是对象,也可以说类的实例是对象。一个程序是由很多不同或相同种类的对象组成,以进程的形式在内存中运行时还可能动态增减一些新的对象。进程的执行体是线程对象,也就是说对象或方法的运行必须是通过线程对象来进行。在APO中可以有最少一个线程对象(主线程),最大32K个线程对象。其它活动对象最多也只是32K个;所以在APO中,一个进程最多有64K个打开的对象。但这并不包含打开的文件对象,一个进程打开的文件对象数最大可达64K个;是另外的文件对象管理员管理的。其实,在一个进程里;通常有4个管理员:文件对象管理员、线程对象管理员、普通对象管理员、内存管理员。管理员是通用的,系统内含的,每一个程序都要继承它们。我会以编写新型的APO操作系统为例,来说明面向对象编程的方式、过程。
1、面向对象的特征:
(1)
(2)分类性
任何类的划分都是主观的,但必须与具体的应用有关。
(3)继承性
继承性是子类自动共享父类数据结构和方法的机制,这是类之间的一种关系。在定义和实现一个类的时候,可以在一个已经存在的类的基础之上来进行,把这个已经存在的类所定义的内容作为自己的内容,并加入若干新的内容。
在类层次中,子类只继承一个父类的数据结构和方法,则称为单重继承。
在类层次中,子类继承了多个父类的数据结构和方法,则称为多重继承。
1)、继承的特点:
第一、子类拥有父类的属性和方法;
第二、子类可以有自己新的属性和方法;
第三、子类可以重写父类的方法;
第四、可以声明自己为父类,创建子类。
2)、多重继承:
在APO中,可以说表、类继承关系数组变量就很自然地实现了多重继承。多重继承是普遍的、自然的。但多重继承需解决二义性问题或说是菱形继承问题。
何时出现二义性?APO如何解决?
1))、当一个派生类从多个基类派生,而这些基类又有同名成员,在对该同名成员进行访问时,可能会出现二义性。
例子:类C3继承了2个类C1,C2中的同名方法f(),一个是c1的,另一个是c2的,如果不显式声明调用哪一个,编译器就不知道你要调用哪一个。这时就必须使用作用域分辨符:写为C3.C1.f();或C3.C2.f();就可以了。但C++非得通过C3的对象才可以,因为f()本质上是C1或者是C2的成员函数,而类成员函数都是要通过对象C3来调用的,如果你想通过C1.f()来直接调用,那么f()必须是static。为何这样?如果C1是C3的派生类,那么在C3对象中调用C1.f()就变成父类可以调用子类的方法了。所以,C3.C1.f();或this.C1.f();编译器就会查找C3对象是否有对C1的引用,有则调用,否则报错。APO的调用都是:类名.方法名;而不管类名是父类的、基类的、本类的、库类的、系统类的、动态类的等等;本类的派生类则报错。所以,APO调用是可以直接写为:C1.f()的,编译器把C1对象号和类号编译传过来后,汇编代码会在C3对象所属的类表项中的类继承关系变量中查找是否有C1的类号,如有则从类表项中找到对应的方法表基址;再加上方法表的方法相对地址最终指向方法f()。静态类由编译器直接给出方法的相对入口地址;如果C1、C2是动态对象,那只能在C3的对象所属的类表项中的类继承关系变量中分别查到C1、C2的类号,没有则报错;从而在类表项中找到对应的方法表基址;再加上方法表的方法相对地址最终指向方法f()。所以,APO在这点上是没有二意的。
2))、当一个派生类从多个基类派生,而这些基类又有一个公共的基类,在对该基类中说明的成员进行访问时,可能会出现二义性。这就是著名的菱形继承问题。
当在多条继承路径上有一个公共的基类时,在这些路径中的某几条汇合处,这个公共的基类就会产生多个副本;从而出现二义性。比如一个类继承了100个父类,而父类又继承100个祖父类。。。形成许许多多的路径,最终都到达10个根类。就算只是2级,都有10000条路径通向10个根类,也会多产生了9990个对象副本;并造成二意。
在C++中,为解决菱形继承还引入了一个更扭曲的虚继承。Python支持多种继承, 并且因为所有的对象都继承于共同的基类Object,导致任何多重继承其实都是菱形继承,当你真的开始自己用多重继承时,其实继承结构已经是一张网了,对此, Guido VanRossum的答案是用深度优先搜索,靠基类排列的顺序来决定, 显的更加神奇。
C++的多重继承很麻烦,而且又容易出错。而无数的JAVA用户者则是把C++的多重继承当作笑话和C++混乱的根源,自豪的宣布JAVA没有这个问题。J那就是没有没有C++自由的多重继承, 只支持对实现的单继承, 并引入了接口, 只允许接口的多重继承。并且, 在继承接口和继承基类形式上特别加了一些区别,所以实际中, 很多人并不把这叫做多重继承,并宣称JAVA没有多重继承。“组合优于继承”这一设计原则,体会了JAVA社区提倡的面向接口编程。“继承本身就是一种强耦合”,就是一种子类对父类的依赖耦合,在一些书籍中提到, 甚至继承本身都是不提倡的, 也就是说, 提倡的是基于对象的设计(OB), 而不是面向对象的设计(OO)。JAVA最多算半个多重继承,只是行为多继承。大家都知道,加一个继承只要一个单词,而加一个对象的组合调用,往往需要增加N个函数接口,以及N个调用(虽然有种东西叫做委托)。
Mix-in类是具有以下特征的抽象类:
不能单独生成实例
不能继承普通类
通过这种在接口和普通多重继承之间的折衷,Mix-in提供了对实现的多重继承,同时对其进行了限制, 使得继承结构不会成菱形, 而是和单一继承一样的树型. 在Ruby中, Mix-in是通过模块(module)的概念来实现的。有趣的事情是,在编程这件事情上, 很多时候, 折衷的方法却往往是优秀的方法。但不符合自然的东东,终是遗憾。
APO是很自然的支持多重继承。在对象所属的类表项中的类继承关系变量中有类关系数组,包括父类的,所有的关系类都是唯一的;不会出现多副本的情形。对象初始化;是先调用父类对象初始化方法,再对关系数组中的具体类的对象作初始化;其实,对象乱序初始化也没问题的。
扭曲式多重继承、循环式多重继承会产生父子不清的现象。比如,一只黑狗有一个儿子白狗,而黑狗的母亲狗又跟儿子白狗生了一只公黄狗。公黄狗是孙子狗?还是儿子狗?还是算父亲狗?我都转晕了。尽管自然界,这种现象极少;但还是有的。这种扭曲多重继承的特点就是:相互引用就可相互调用;也就是基类可调用派生类的方法。APO的多重继承也同样是自然的支持这种扭曲式多重继承。
多态性是指相同的操作或方法、过程可作用于多种类型的对象上并获得不同的结果。不同的对象,收到同一消息可以产生不同的结果,这种现象称为多态性。多态性允许每个对象以适合自身的方式去响应共同的消息。多态性增强了软件的灵活性和重用性。
边学、边抄、边写下这段章节后的感想:何为抽象?那是亿众瞩目的地方,那是灵魂深处的悠游,那是联想的发源地,那是万川归一的大海,那是世界的最顶峰,也是宇宙的边缘。从那看进去:一眼星河涌现、神光飞舞、浮想缠绵。
抽象,顾名思义,就是抽掉了具体形象的东西。把具体概念的诸多个性排出,集中描述其共性,就会产生一个抽象性的概念。抽象概念的外延大,内涵小,具体概念的外延小,内涵大。要抽象,就必须进行比较,没有比较就无法找到在本质上共同的部分;所以抽象的过程也是一个裁剪的、分类的过程。在抽象时,同与不同,决定于从什么角度上来抽象;抽象的角度取决于分析问题的目的。抽象通过分析与综合的途径,运用概念在人脑中再现对象的质和本质的方法,分为质的抽象和本质的抽象。分析形成质的抽象,综合形成本质的抽象(也叫具体的抽象)。万物都是容器,类名就是从无数的容器中分离出具有一些相同属性、相同方法的容器集的名称;所以分类的过程就是抽象过程。类名就是一种抽象名,我们把类的个体容器称为对象;所以,也说万物都是对象。但从另一角度看,“万物”、“对象”就是一个更为抽象的概念;它们泛指一切。我们根据一些特性,也即是属性;抽象出“苹果”类,“苹果”也有大小、青红之分等等;所以“苹果”类下还可再分成具体类,之后才到具体的某个苹果。类似的还有“香蕉”、“梨子”等等,我们又从这些“苹果”、“香蕉”、“梨子”等抽象概念中,抽象出它们的共性,得到“水果”类。而“水果”类可以说它们的成员也是类,所以说“水果”是类中类,是抽象之后的再抽象;是属于抽象类。这还没完,“水果”源自果树,是果树类中的一个属性。而果树又是植物类中的一个种类;植物和动物构成生物类;生物类和非生物类才到达最顶端的根——物类。你可以说“茶杯”是物,是非生物;但不能说是“水果”。从这“金字塔”看,最顶端只有一个“物”类,往下只是2个,再往下呢?植物类、动物类就多了,看你的分类角度吧。对于动物来说,简单的就是死、活动物类;复杂的你知道的。抽象类的尽头才是具体类,之后是它们的个体;一个个对象。所以,金字塔有4层,第四层:各种对象;第三层:具体类;第二层:抽象类(有多个层次);第一层:最顶层,只有一个抽象的根类。从金字塔层次看来,似乎只有单继承,其实不然的;如“水果”就不一定就是源自果树,它也可能来自植物藤类。就拿衣服类来说,衣服,春、夏、秋、冬衣服,长衫,短袖,棉衣等等都是抽象类;自然界太多的抽象类了。可以说,抽象类、具体类、个体对象充满了我们的世界。我们的语言就是对它们及相互关系的描述;计算机语言也应该如此!
在面向对象领域,抽象类主要用来进行类型隐藏。我们也可以构造出一个固定的一组行为的抽象描述,但是这组行为却能够有任意个可能的具体实现方式。这个抽象描述就是抽象类,而这一组任意个可能的具体实现则表现为所有可能的派生类。
抽象类往往用来表征我们在对问题领域进行分析、设计中得出的抽象概念,是对一系列看上去不同,但是本质上相同的具体概念的抽象。比如:如果我们进行一个图形编辑软件的开发,就会发现问题领域存在着圆、三角形这样一些具体概念,它们是不同的,但是它们又都属于形状这样一个概念,形状这个概念在问题领域是不存在的,它就是一个抽象概念。正是因为抽象的概念在问题领域没有对应的具体概念,所以用以表征抽象概念的抽象类是不能够实例化对象的。
1))、语法:
抽象类也有方法的抽象。例如动物是一个抽象类,他的移动方法还没有确定,因为有的动物是四条腿移动,有两条腿移动,有的飞,有的爬,但是他们都能移动,这样你可以把动物定义为抽象类,将动物的移动方法定义为抽象方法,强制继承它的子类去实现。而且你在动物这个类中移动虽然没有实现,但是可以调用他。
2))、接口
接口(interface)其实就是一个特殊的抽象类;是抽象类的变体。差别在于接口中的方法必须都是抽象方法,不可以有普通方法而已。既然是一种抽象类,接口就只能继承其它接口,抽象类和普通类也可以继承接口和抽象类。在APO中,抽象类可以包含接口!接口可以只是抽象类的行为部分。
接口(Interface)是用来定义行为的!
APO中的抽象类(Abstract
具体类()是用来执行行为的!
Interface接口表述“has
3))、实现简介
(4)消息和方法
对象之间进行通信的结构叫做消息。在对象的操作中,当一个消息发送给某个对象时,消息包含接收对象去执行某种操作的信息。发送一条消息至少要包括说明接受消息的进程号、对象名、发送给该对象的消息名(即对象名、方法名)。一般还要对参数加以说明,参数可以是认识该消息的对象所知道的变量名,或者是所有对象都知道的全局变量名。
2、方法
类中操作的实现过程叫做方法,一个方法有方法名、参数、方法体。方法只有一份,供所有的同类的对象使用!而属性是每个对象一份,因为每个对象的都不一样。
方法重载:就是在同一个类中,方法的名字相同,但参数个数、参数的类型或返回值类型不同!实际上,还是调用不同的方法。
方法重写:它是指子类和父类的关系,子类重写了父类的方法,但方法名、参数类型、参数个数必须相同!覆盖==重写叫法不同罢了。实际上,父类的方法还在,只是本类的方法与父类的同名吧。
至于加载的意思,其实就是让编译器执行某段程序,可以是类可以是包可以是任何编译器能够编译的代码。
3、公有、私有
面向对象程序设计的重点是类的设计,而不是对象的设计。类可以将数据和函数封装在一起,其中函数表示了类的行为(或称服务)。类提供关键字public、protected和private 用于声明哪些数据和函数是公有的、受保护的或者是私有的。这些类型声明的具体的实现是由编译器来判断与进行的。
1)、公共的(public):公有是所有包括本类及外部类都可以调用。
2)、私有的(private):私有是只有本类可以调用。这是为了安全和健壮性;保证了不会被别人在外部修改。
3)、受保护的(protected):本类、派生类可以访问,外部类不行。
4)、默认控制访问符(friendly):
为了实现数据的封装,提高数据的安全性,我们一般会把类的属性声明为私有的,而把类的方法声明为公共的。这样,对象能够直接调用类中定义的所有方法,当对象想要修改或得到自己的属性的时候就必须要调用以定义好的专用的方法才能够实现。提倡的是:“对象调方法,方法改属性”。
第十章 类、对象与实现