首页 > 代码库 > 第十章 类、对象与实现

第十章 类、对象与实现

                                                                                     第十章 类、对象与实现

 

          万物都是容器,容器的符号是U;对象就是单个容器的别名。一切皆对象,具有某些相同属性特征的对象归纳成类。对象(Object)是类(Class)的一个实例(Instance);类是对象的模板。如果将对象比作房子,那么类就是房子的蓝图。我们以自然语言去描述世界,而计算机是用各种数据结构去描述世界;数据可以用x个二进制位的位容器BUx来表示。对象具有状态、方位、时间等属性,每个属性是用数据值或说位容器BUx来描述的;最后构造了描述对象属性的数据表。对象还有操作,用于改变对象的属性,操作就是对象的行为。操作也可以说是方法或函数,方法就是一段指令;一条指令就是一个数据字W;所以,方法就是一个指令字数组。由一系列方法构成的指令数据表就描述了对象的行为。所以,一个对象就是由属性数据表和方法数据表来描述的。或者换句话说对象实现了数据和操作的结合,使数据和操作封装于对象的统一体中。在APO中,数据表不外是一个行(8字)对齐的位容器,是一段32字节或8字对齐的存储空间;和C语言的结构struct类同。数据表中的元素最小是一个字(4字节、2字符),并最终是行对齐。

 

一、变量与表

 

1、变量

 

变量:用来标识(identify)一块内存区域,这块区域的内容是可变的。
变量名:是一个标识符(identifier),用来指代一块内存区域,即变量。

 

CPU是用内存地址来标识一块内存区域的,机器码中,是不会出现变量名的;出现的是逻辑相对地址。在汇编层次上,操作的都是地址,不存在任何名称;变量名是给我们程序员操作内存来使用的。简单说,变量就是地址;在对程序编译连接时由编译系统给每一个变量名分配相应的内存逻辑地址,形成符号表;该地址分配后不可改变。当你调用此变量时,编译器就会根据此符号表找到对应的逻辑地址,然后翻译成汇编指令。例如从R2寄存器为基址的表中读一个变量所在行的内容到行寄存器H5,翻译成汇编:MOV H5, R2(变量逻辑相对地址); 结果是:( R2 + 变量逻辑相对地址变量 ) -> H5。在一个数据表中的逻辑相对地址是指变量地址与表初始地址0的相对偏移,编译器把逻辑相对地址、变量名、变量类型等一起放在符号表中。所以,变量名并不占内存空间;在APO中,变量逻辑相对地址(简称变量地址)是一个字1W。


         程序是由一些类文件组成,一个具体类就产生2张表(属性表、方法表)。在编译时产生的类文件的2张表都是从地址0开始的,在连接时将各个类文件进行符号替换,这时会修改相应的表逻辑相对地址,最后产生一个2张合表都从地址0开始的程序文件。在运行时加载器会把程序的2张合表加载到某个不定的内存区域中(每次加载到的物理内存地址不一定相等)。当然,许多类属性表或方法表合作一起,程序运行时还有很多对象在参与;应该有一个方向指示总表:对象头列表。对象属性表、方法表或全局变量或静态变量(包括一些复杂类型的常量),它们所需要的空间大小可以明确计算出来,并且不会再改变,因此它们可以直接存放在程序文件的特定的节里(而且包含初始化的值),程序运行时也是直接将这个节加载到内存中,不必在程序运行期间用额外的代码来产生这些变量和初始值。其实在运行期间再看“变量”这个概念就不再具备编译期间那么多的属性了(诸如名称,公有、私有等类型,作用域,生存期等等),对应的只是一块内存(只有首址和大小),所以在运行期间动态申请的空间,是需要额外的代码维护,以确保不同变量不会混用内存。

 

        一个程序有大量的属性表、代码表、属性元素、数组、字符串等等变量;CPU不认识变量名字,只认识地址数字;所以,编译器的任务之一就是把各种变量名称翻译成相应的地址了。变量所指元素的数据类型在许多语言中都有很多说法,如整形,单精度,双精度,字符串等等。真实情形是CPU不认识它们,只是编译器、方法(代码段)认识它们。APO是汇编语言,没那么多说法。简单地,APO变量的数据类型只有位容器;但需考虑对齐。表不外是x行容器BUxH,表中的元素或许是一个字,或是32位对齐的位图,或是字(字符)数组,或是一个表,或是另一个表的引用(动态数组,动态字符串,表)。一个数据块是64KH(行)= 1MZ(字符)= 512KW(字)= 2MB(字节)。

 

2、内存区域

 

APO在内存分配时会涉及到以下区域:

◆寄存器:用于自动变量,如方法里的局部变量。

◆栈:存放临时的基本类型数据、方法的局部变量。

  对象的引用(包括动态对象)、动态分配内存的变量引用,局部静态变量等都应该保存在对象的属性表中;但动态对象、动态变量的身体是存放在堆中。栈是会自动清除或覆盖数据的,适合自动变量。APO对堆内存的操作和对栈内存的操作速度是一样的,比寄存器操作稍微慢点;即使是动态内存分配和释放(有专用的硬件指令)不过是几个ns。

 

           当在一段代码块定义一些变量时,编译器就在栈中为这些变量分配内存空间,当这些变量退出该作用域后,栈会自动释放掉(重新设置栈SP寄存器值)为这些变量所分配的内存空间,该内存空间可以立即被另作他用。栈中的数据大小和生命周期是可以确定的。

 

           在APO中,一个进程内只有一个用户栈,所有的对象(包括线程对象)都是共用一个栈。栈的大小,是全局变量;默认是2个页,256行;如果对象数目多或很多线程,就可设大些;最大是32KH。为何JAVA等是每个线程需要一个2MB栈,大量占用内存?我认为这是设计问题。在APO中,对象消息处理代码块内只是一些少量的自动变量;普通对象完成消息处理后就会自动清除,相对来说不占空间;而线程对象可能会因等待消息,从而暂时让出CPU,但并不让出占据栈内的那部分空间;就可能会同时有多个线程占据栈空间。解决办法就是增大栈空间吧;APO中一个进程就算有6万个对象在活动,半个数据块的栈空间就足够了。编译器将自动变量翻译成对栈指针(0地址)的相对偏移地址。

 

◆堆:存放用动态产生的数据

 

           堆内存用来存放由动态创建的对象和数组等动态变量。在堆中分配的内存,由APO的内存管理员来管理或由用户代码释放。动态声明(用DYV标识符)一个变量,DYVBU64KH A;那么系统就会自动为变量A分配一个数据块,64KH行的内存空间,并返回首指针,大小给代码保存好。这时,在编译器的符号表中没有变量A的相对地址;而是需要另外的代码通过保存在对象属性表中的首指针去操作变量。声明BU1W[512K] A;字数组 或者BU1Z [1M]A;百万个字符串数组;实际上也是分配一个数据块。定义向量或说动态数组BUxW[]也是可以的,可能会使用到连续的数据块或数据块链表。但内存总是有限的,还不如用数据流,只是开辟一个数据块窗吧;数据流可以无限。动态内存的生存期由程序员决定,使用非常灵活,但如果分配了空间,就有责任回收它,否则运行的程序会出现内存泄漏,频繁地分配和释放不同大小的空间将会产生内存碎块。当然,在APO中系统内存管理员也会定期清理垃圾并回收内存。对象的析构方法也会据对象引用计数为0时释放相应的对象内存。

 

   在代码块中以表声明、或在代码块和类中以向量声明、DYV声明的变量都放到堆区,都当作是动态对象;系统会返回分配的块号、行数、行开始地址。并将后3项保存到动态对象头列表相应项中。由于动态变量的地址,编译器无法预先定义;所以是不会把动态变量翻译成地址的。动态变量在符号表中的1W数据,高16位依然是动态变量的行数大小;但低16位不再是相对地址,而是唯一指示动态变量的动态对象号。这一字数据会传入CPU的寄存器A;很简单,编译器只是多汇编出一条到A寄存器的立即数传送指令吧。对于数组动态变量,还需另外传送数组的16位宽度的成员项数和成员位宽度到CPU的寄存器B。B和A(行数)都放在动态对象头,而A的动态对象号却形成间接引用指针放在对象引用变量中。接着,用户需以寄存器A的参数调用内存管理员中的方法获得内存地址等数据并保存,之后是初始化方法,否则系统默认初始化为0。我真不明白JAVA为何要在栈建动态变量符号表?不多余?

 

          在堆中产生了一个动态数组或动态对象后,还可以在相应对象属性表中定义一个特殊的变量,让对象属性表中这个变量的取值最终找到数组或对象在堆内存中的首地址,对象属性表中的这个变量就成了数组或对象的引用变量。 引用变量就相当于是为数组或对象起的一个名称,以后就可以在程序中使用对象属性中的引用变量来访问堆中的数组或对象。引用变量是普通的变量,定义时在对象属性表中分配,引用变量在程序运行到其作用域之外后被释放(还在的,但如果释放内存,会变为空指针);而动态数组和动态对象本身在堆中分配。动态数组和动态对象在没有引用变量指向它的时候变为垃圾;不能再被使用,但仍然占据内存空间不放;在随后的一个不确定的时间被垃圾回收器收走(释放掉)。所以,用户对象退出时调用析构方法;而析构方法要包含释放对象内存代码。

 

  ◆常量池:存放常量(一般放在方法表区)

 

3、表

 

        表的定义:BUxH 表名字{  n个元素… },表中的每行一个元素的声明以;结束。BUxH意思是表占有x行(16字/行)的存储空间;不写也行,由编译器据元素占用情况定。类的定义:class 类名字{ 类属性表{},类方法表{}}。程序的所有具体类,编译器都实例化一个与类同名的对象出来;并为它们分配好逻辑地址空间,分配对象号,建立对象头列表。具体类对象属性表也可以一开始就初始化(在类文件中设置),或由系统默认清0,或另外写一个初始化(构造)方法。全局表或全局变量或静态变量(包括一些复杂类型的常量)和对象头表、类表及一些公共方法,建立一个程序根类来实现。在编译器符号表中,对象名对应的地址值:高16位是所属的类号;低16位为对象号。在堆中分配的动态变量(包括对象、向量、大数组等)全部视为动态对象,编译器对于代码块中的对象、动态变量(用DYV声明)都分配动态对象号;可由对象序号构成对象头表。抽象类(包括接口),虽然没有实例化的对象,但有类关系(描述继承关系)、抽象属性,还有抽象方法。抽象类(包括接口)和具体类构成树结构;所有的类名、类关系、抽象属性名字,还有抽象方法名字等等最终组成类表。类表、对象头表、对象属性表、具体类方法表为四大要素表。

 

1)、类表

 

金字塔形的树状类表,有三个层次:

1))、纯抽象类:只有抽象属性和抽象方法;接口是只有抽象方法的纯抽象类。只有抽象属性的类为概念类。

2))、普通抽象类:可有抽象属性和普通属性,可有抽象方法和普通方法。

3))、具体类:只有普通属性和普通方法。

 

            类描述的是该类成员(成员也可能是类)的特征,与类的所属关系;还有行为或说成员所具有的能力。伟大的、光荣的、无产阶级革命家雷锋同志。电脑如何理解呢?伟大、光荣是2个概念类;无产阶级革命家、同志是有抽象属性和抽象方法的纯抽象类;有一个多继承于它们的隐藏具体类;雷锋是具体类中的一员;毛泽东、周恩来等也是其中一员。大海中的海生动物类中的龟类中的小龟类中的mm小龟正在 游玩。这是金字塔中,从上往下的类描述,我们的语言通常是隐藏了中间的抽象类层次;只是说明路径的2端。如大海中的mm小龟 正在游玩。大海是根类;小龟是具体类,mm是具体类中的一员;游玩是小龟类的能力或其具有的抽象方法,而“正在”是“游玩”方法的时间描述,也就是方法的属性或说是变量。“游玩”是一个抽象方法,也可以搞成一个接口。不止是小龟的“游玩”,狗、猪、牛也可以继承这个接口。不知道大家晕了没有?我是差不多要晕倒了。

 

         金字塔的顶端之后是什么?如“物类”,物之顶是“虚无”吗?我不知道!但一些根类的之后,我大约了解一些。比如“水果”这抽象类,并不能说它是继承于果树类;只是果树类中;花、果、等等抽象属性中的一种。果树类继承植物类,植物类继承生物类,生物类继承于物类。“水”之后是什么?是分子式类中的H2O属性吗?显然,抽象类的属性可以是类;这不是继承方式,是另外一种包含方式,称为包含类。具体类的属性可以是另一个对象的引用,但不能是类。

 

        每一个类都有与类名字对应的类名序号。一个类有派生类和包含类,它们的类名序号构成字数组。这是类的子类描述变量,子类数量可以是变动的,我们应该视电脑为另一种智能生命;而不是死物。随着电脑的不断学习、不断进步;子类数量就可能增加。


         每一个类都可能继承多个父类,我们把继承的父类序号,包括到根路径上的继承链上的所有父类序号一起构成类继承关系字数组变量;数组里的所有父类序号是唯一的、一个序号只能出现一个。一个对象的初始化方法或析构方法,必须按照对象所属类的继承关系数组中的具体类来进行。子类变量和类继承关系变量是每个类必有的属性;要记住  继承就意味着全盘接收父类的方法和属性。对于行为来说是使用抽象类还是接口,就要看实际情形了。子类变量提供了一条由上到下的路径,这会破坏对象的封装性;所以,接口和抽象类要慎重设计。类关系数据库文件是人类知识库中的一个文件,类名序号需要用32位来表达数十亿的公共类。一个程序里的类应该不多,默认最大是256个;用户最多可设置到2K个。类都可以实例化,只不过是具体类实例化成对象吧。接口也是一种抽象类,理论上,其实例化是子类变量和类继承关系变量;
接口调用是:对象名.接口方法名字();

但编译器如发现该对象所属的类有相同的方法名,就直接编译成直接调用对象所属类的方法了。所以,接口实例化的好处只是知道接口之间,类与接口之间的继承关系吧。

 

类表:以类号顺序排列的类项构成的表;每类项占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 G;     //  G是动态变量的引用变量。对象属性表可能有n个引用。

// 行开始地址是第0行;行的字地址0;引用变量最小占一行,一行8个引用。

 BU32 A;  //A变量是32位的位容器或说是2个字符或一个字,BU32 = BU2Z

 BU99W B; //B变量是99字的存储空间,或是198个字符的字符串数组。

// 行开始地址是第1行,行字地址为0,长度99,占行数13行,最后行空5W。

 BU32 C; //C是32位的位容器;A、C变量可插入第13行的空洞。

 BU1K D; //D是1K位的位图,也可能是一个字数组,取决于你的代码。

// D变量行开始地址是第14行;行的字地址为0,长度32W,占行数4行。

 BU4W E; //E变量行开始地址是第18行;行的字地址为0,长度4

 BU2W F; //F变量行开始地址是第18行;行的字地址为4,长度2

 

 

引用变量内容定义:高16位是动态变量所占行数,低16位为对象号。动态对象序号在对象号区,0xC000- 0xFFFF。8K

 

           表内的变量可看作全是1W的数组,对于静态表来说;要实现不同宽度数组就要定义表内表了。而动态数组就靠程序员了;当然,对于动态数组;编译器还是会多编译一条传送数组的16位宽度的成员项数和成员位宽度到CPU的寄存器B的指令的。

 

编译器翻译原则:

1)、先编排4W的变量在一起,其次是2W的变量;因它们可能是数值,也易于对齐。动态变量的所有引用变量(1W一个)放在一起成字数组,放于表头,最少占1行(8个)。

2)、多于4W的变量地址都是以行地址开始。

3)、最后是在有空洞的行插入1W/2W/3W的变量地址。

 

表内变量对应的地址值定义:

BU32 BL;

 Bit31 T; // 为0,则Bit30–16的15位是字长度;所以变量的内容最多32KW或4KH。如需要大内容变量,那就要定义对动态变量的引用了;在运行时再分配空间。

// T标志为1是变量的内容长度小于1行; Bit29–24的6位为长度及行的字开始地址。Bit30为1为引用变量,Bit29–27:111数组、110向量;100为对象引用等。Bit26–24的3位为行的字开始地址。变量类型属性字节Bit23–16用于特殊对象。

 BU16 HDV; //变量所在的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,方法的起始行地址)

….

}

方法表中的方法是一个接着一个的以行为单位紧密存放的。调用方式是:

           类名(或对象名字).方法名字; 编译器会由类名或对象名字、方法名字从符号表中得到类号、对象号和方法指令长度、方法的起始行地址,将这4个值和指令长度、行数一起编译成指令传到寄存器A、B。接着据A、B寄存器,设置数据块号;并将方法的起始行地址作为方法入口指针,调用相应的方法。所以,调用任何一个方法必须指明对象名或类名。当然,调用系统中的类则无须指明类名;因为系统中的所有类中的方法都在第一个数据块的本地内存中;只需要方法名的起始行地址作为方法入口指针,调用相应的方法。对于本类也可以用this来指代。

 

二、面向对象编程

 

我们可以这样说:万物皆对象,在程序里一切皆变量(对象、方法、属性、表)。

面向过程的思想:由过程、步骤、函数组成,以过程为核心;面向过程是先有算法,后有数据结构。

面向对象的思想:以对象为中心,先开发类,得到对象,通过对象之间相互通信实现功能。面向对象是先有数据结构,然后再有算法。

 

           我们知道同一个类的对象可以有很多个,对应的对象属性表也有很多;但只有一个方法表。具有相同或相似性质的对象的抽象就是类。因此,对象的抽象是类,类的具体化就是对象,也可以说类的实例是对象。一个程序是由很多不同或相同种类的对象组成,以进程的形式在内存中运行时还可能动态增减一些新的对象。进程的执行体是线程对象,也就是说对象或方法的运行必须是通过线程对象来进行。在APO中可以有最少一个线程对象(主线程),最大32K个线程对象。其它活动对象最多也只是32K个;所以在APO中,一个进程最多有64K个打开的对象。但这并不包含打开的文件对象,一个进程打开的文件对象数最大可达64K个;是另外的文件对象管理员管理的。其实,在一个进程里;通常有4个管理员:文件对象管理员、线程对象管理员、普通对象管理员、内存管理员。管理员是通用的,系统内含的,每一个程序都要继承它们。我会以编写新型的APO操作系统为例,来说明面向对象编程的方式、过程。

 

1、面向对象的特征:

 

(1)       对象唯一性


     在一个进程中,每个对象都有自身唯一的标识(16位的所属类号和16位的对象号),通过这种标识,可找到相应的对象(方法表、属性表入口,大小,引用计数)。在对象的整个生命期中,它的标识都不改变。

(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的多重继承也同样是自然的支持这种扭曲式多重继承。

 

 (4)多态性(多形性)

           多态性是指相同的操作或方法、过程可作用于多种类型的对象上并获得不同的结果。不同的对象,收到同一消息可以产生不同的结果,这种现象称为多态性。多态性允许每个对象以适合自身的方式去响应共同的消息。多态性增强了软件的灵活性和重用性。

 

 (5)、抽象类

 

           边学、边抄、边写下这段章节后的感想:何为抽象?那是亿众瞩目的地方,那是灵魂深处的悠游,那是联想的发源地,那是万川归一的大海,那是世界的最顶峰,也是宇宙的边缘。从那看进去:一眼星河涌现、神光飞舞、浮想缠绵。

 

           抽象,顾名思义,就是抽掉了具体形象的东西。把具体概念的诸多个性排出,集中描述其共性,就会产生一个抽象性的概念。抽象概念的外延大,内涵小,具体概念的外延小,内涵大。要抽象,就必须进行比较,没有比较就无法找到在本质上共同的部分;所以抽象的过程也是一个裁剪的、分类的过程。在抽象时,同与不同,决定于从什么角度上来抽象;抽象的角度取决于分析问题的目的。抽象通过分析与综合的途径,运用概念在人脑中再现对象的质和本质的方法,分为质的抽象和本质的抽象。分析形成质的抽象,综合形成本质的抽象(也叫具体的抽象)。万物都是容器,类名就是从无数的容器中分离出具有一些相同属性、相同方法的容器集的名称;所以分类的过程就是抽象过程。类名就是一种抽象名,我们把类的个体容器称为对象;所以,也说万物都是对象。但从另一角度看,“万物”、“对象”就是一个更为抽象的概念;它们泛指一切。我们根据一些特性,也即是属性;抽象出“苹果”类,“苹果”也有大小、青红之分等等;所以“苹果”类下还可再分成具体类,之后才到具体的某个苹果。类似的还有“香蕉”、“梨子”等等,我们又从这些“苹果”、“香蕉”、“梨子”等抽象概念中,抽象出它们的共性,得到“水果”类。而“水果”类可以说它们的成员也是类,所以说“水果”是类中类,是抽象之后的再抽象;是属于抽象类。这还没完,“水果”源自果树,是果树类中的一个属性。而果树又是植物类中的一个种类;植物和动物构成生物类;生物类和非生物类才到达最顶端的根——物类。你可以说“茶杯”是物,是非生物;但不能说是“水果”。从这“金字塔”看,最顶端只有一个“物”类,往下只是2个,再往下呢?植物类、动物类就多了,看你的分类角度吧。对于动物来说,简单的就是死、活动物类;复杂的你知道的。抽象类的尽头才是具体类,之后是它们的个体;一个个对象。所以,金字塔有4层,第四层:各种对象;第三层:具体类;第二层:抽象类(有多个层次);第一层:最顶层,只有一个抽象的根类。从金字塔层次看来,似乎只有单继承,其实不然的;如“水果”就不一定就是源自果树,它也可能来自植物藤类。就拿衣服类来说,衣服,春、夏、秋、冬衣服,长衫,短袖,棉衣等等都是抽象类;自然界太多的抽象类了。可以说,抽象类、具体类、个体对象充满了我们的世界。我们的语言就是对它们及相互关系的描述;计算机语言也应该如此!

 

          在面向对象领域,抽象类主要用来进行类型隐藏。我们也可以构造出一个固定的一组行为的抽象描述,但是这组行为却能够有任意个可能的具体实现方式。这个抽象描述就是抽象类,而这一组任意个可能的具体实现则表现为所有可能的派生类。

         抽象类往往用来表征我们在对问题领域进行分析、设计中得出的抽象概念,是对一系列看上去不同,但是本质上相同的具体概念的抽象。比如:如果我们进行一个图形编辑软件的开发,就会发现问题领域存在着圆、三角形这样一些具体概念,它们是不同的,但是它们又都属于形状这样一个概念,形状这个概念在问题领域是不存在的,它就是一个抽象概念。正是因为抽象的概念在问题领域没有对应的具体概念,所以用以表征抽象概念的抽象类是不能够实例化对象的。

 

1))、语法:


       抽象类中除了抽象属性,还可以有抽象方法,也可以有普通方法,抽象方法只可以被声明,不能被实例化(就是不能有方法体),必须由继承该抽象类的普通类来实例化。如棉裤,茶 杯,服 装,放 大镜等等;自然界抽象类的多继承比比皆是;APO也是很自然的支持抽象类的多继承;使用Abstract 声明抽象类。

 

       抽象类也有方法的抽象。例如动物是一个抽象类,他的移动方法还没有确定,因为有的动物是四条腿移动,有两条腿移动,有的飞,有的爬,但是他们都能移动,这样你可以把动物定义为抽象类,将动物的移动方法定义为抽象方法,强制继承它的子类去实现。而且你在动物这个类中移动虽然没有实现,但是可以调用他。

2))、接口

 

         接口(interface)其实就是一个特殊的抽象类;是抽象类的变体。差别在于接口中的方法必须都是抽象方法,不可以有普通方法而已。既然是一种抽象类,接口就只能继承其它接口,抽象类和普通类也可以继承接口和抽象类。在APO中,抽象类可以包含接口!接口可以只是抽象类的行为部分。

         接口是对具有的动作能力预定义,抽象类是对具有的动作的抽象。接口是对动作的抽象,抽象类是对根源与动作的抽象。抽象类表示的是,这个对象是什么?做什么;接口表示的是,这个对象能做什么。比如,男人,女人,这两个类(如果是类的话……),他们的抽象类是人。说明,他们都是人。人可以吃东西,狗也可以吃东西,你可以把“吃东西”定义成一个接口,然后让这些类去实现它。接口是一种规范和标准,用于约束类的行为,接口可以将方法的特征和实现分割开来。抽象类中可以加入非抽象的方法,而接口是不能的。抽象类是对象的抽象,然而接口是一种行为规范。抽象类对于提供模式、蓝图和后代类遵循的原则有用,如果遵循了蓝图的语义,后代类的行为可能按抽象类提供者和使用者所期望的那样。抽象类将事物的共性的东西提取出来,抽象成一个高层的类。子类由其继承时,也拥有了这个超类的属性和方法;---也就实现了代码的复用了。子类中也可加上自己所特有的属性和方法;----也就实现了多态。接口只能定义抽象规则;抽象类既可以定义规则,还可能提供已实现的成员。接口是一组行为规范;抽象类是一个不完全的类,着重族的概念。接口只包含方法、属性、索引器、事件的签名,但不能定义字段和包含实现的方法;抽象类可以定义字段、属性、包含有实现的方法等。

 

接口(Interface)是用来定义行为的!
APO中的抽象类(Abstract Class)是用来定义与实现行为的!
具体类()是用来执行行为的!

 

           Interface接口表述“has a”关系,用于描述功能是用来说明它的是实现类是“做什么?”的;至于“怎么作?”,interface并不进行约束。而abstract class表述“is a”关系,它在描述功能的同时也通过具体实现部分功能的方式,来约束“怎么作?”。APO中的抽象类也可以有并不进行约束的方法,不就是放在接口上吧。

 小结:APO的抽象类、接口首先是一个类;接口只是抽象类的行为部分。但接口包含的内容少,显得更抽象。

 

3))、实现简介

 

         从上面看出,编译器的符号表很重要;它是电脑与人脑沟通的桥梁;它描述了类名、变量名等与机器数值的一一对应。所以,编写智能程序应该把符号表包含进去。我们人类用字符语言去描述世界,机器却是用数值去描述。从某个角度看,可以说抽象类的个体就是一个个类。而抽象类名又指向它们的派生类集;最后,构成一棵类树结构。类表是一个多树结构,表中的每一个节点都包含下一个分叉的节点数组——子类描述变量,也包含所有的上级节点数组——类继承关系变量。对于具体类还有方法表入口、属性表描述等。

 

(4)消息和方法

           对象之间进行通信的结构叫做消息。在对象的操作中,当一个消息发送给某个对象时,消息包含接收对象去执行某种操作的信息。发送一条消息至少要包括说明接受消息的进程号、对象名、发送给该对象的消息名(即对象名、方法名)。一般还要对参数加以说明,参数可以是认识该消息的对象所知道的变量名,或者是所有对象都知道的全局变量名。

 

2、方法

类中操作的实现过程叫做方法,一个方法有方法名、参数、方法体。方法只有一份,供所有的同类的对象使用!而属性是每个对象一份,因为每个对象的都不一样。


方法重载:就是在同一个类中,方法的名字相同,但参数个数、参数的类型或返回值类型不同!实际上,还是调用不同的方法。


方法重写:它是指子类和父类的关系,子类重写了父类的方法,但方法名、参数类型、参数个数必须相同!覆盖==重写叫法不同罢了。实际上,父类的方法还在,只是本类的方法与父类的同名吧。


至于加载的意思,其实就是让编译器执行某段程序,可以是类可以是包可以是任何编译器能够编译的代码。

 

3、公有、私有

 

           面向对象程序设计的重点是类的设计,而不是对象的设计。类可以将数据和函数封装在一起,其中函数表示了类的行为(或称服务)。类提供关键字public、protected和private 用于声明哪些数据和函数是公有的、受保护的或者是私有的。这些类型声明的具体的实现是由编译器来判断与进行的。

 

类是属性与方法的集合。而这些属性与方法可以被声明为私有的(private),公共的(public)或是受保护(protected)的,他们描述了对类成员的访问控制。

1)、公共的(public):公有是所有包括本类及外部类都可以调用。
2)、私有的(private):私有是只有本类可以调用。这是为了安全和健壮性;保证了不会被别人在外部修改。
3)、受保护的(protected):本类、派生类可以访问,外部类不行。
4)、默认控制访问符(friendly):


为了实现数据的封装,提高数据的安全性,我们一般会把类的属性声明为私有的,而把类的方法声明为公共的。这样,对象能够直接调用类中定义的所有方法,当对象想要修改或得到自己的属性的时候就必须要调用以定义好的专用的方法才能够实现。提倡的是:“对象调方法,方法改属性”。


第十章 类、对象与实现