•面向对象语言通过将数据和作用其上的操作封装成一种新的数据类型,实现了很好的数据抽象性•通过继承和组合能够方便的实现模块的可重用性和可扩充性•本章我们概述面向对象语言的重要概念和实现它们的方法序面向对象语言的概念•对象•类•继承性-对象•一个对象由它的状态和操作于该状态的过程组成,状态由一组属性的值表示,过程也叫做方法或行为。•属性和方法共同形成了对象的特征。•对象封装了数据及其上的操作,它是一个独立的有约束的实体,有自己的记忆和活动。-类•用户可以将一些属性和方法封装在自定义的类型中,这种类型就称为类。一个类规范了类中对象的属性和方法,一个对象要想属于一个类,它必须至少应该包含该类所具有的特征,当然还可以含有其它一些特征。-继承:从已有的类派生出新的类。子类继承父类的属性与操作,可对其修改;子类增加新的内容。目的:复用单继承、多继承类与类之间构成了层次关系。类类类类类类类类类多继承表示起来复杂,实现起来不容易,存在有语义模糊性问题,存在属性名和函数名冲突问题。不鼓励使用多继承,有的语言不支持。-多态•多态是指基类的方法调用可以根据实际运行的对象类型不同实现对不同函数体的调用。•考虑类间的层次关系,解决功能和行为的再抽象。单继承的编译方案•子类中属性的偏移值应该如何定?对于单继承,即每个派生类只有一个父类的情况,采用最简单的“前置”技术就能解决上面的问题。例如当B从A派生而来时,那些从A继承来的属性被置于B的属性域的最开始,并和它们在A中出现的顺序是相同的。那些从B派生的属性被放在后面,ClassA{inta;};ClassB:publicA{intb;intc;};ClassC:publicA{intd;};ClassD:publicB{inte;};AaBabcCadBabce属性的单继承单继承的编译方案•方法的编译同普通函数的编译很相似:它被编译变换成机器代码存于指令空间的一个特定地址中。在编译的语义分析阶段,每一个变量的环境域会包含一个指针指向它的类描述符;每个类描述符又包含一个指向它父类的指针和自身的方法实例列表;每个方法实例又包含一个对应的机器指令级的标号。§静态方法•静态方法是指在编译时就可以确定下来执行函数体的方法。在c++中没有特殊标明的方法都属于此类。例如下面的c++程序:classA{intx;voidf();}classB:publicA{voidg();}classC:publicB{voidg();}classD:publicC{inty;voidf();}该程序中所有的方法都是静态方法。当我们调用一个方法c.f()时,编译出的机器代码是依据变量c的类型来定的,而不是依据c所指向的对象类型来定的,这样静态方法对应的执行函数体就可以在编译时确定下来。•为了编译类似于c.f()的方法调用,编译器首先要找到c所对应的类描述符,假设为C;接着要在C中查找方法f,假设没有找到;•然后编译器再查找C的父类B,然后在B中寻找方法f,以此类推。•假设最后在某个祖先类A中找到了静态方法f,这时编译器就可以将c.f()这个调用编译为机器标号A_f。§动态方法•动态方法是指在编译时无法确定下来执行函数体,要到运行时根据变量指向的实际类型来确定的方法。在c++中用带有virtual关键字的虚函数来表示。与静态方法相反,编译出的机器代码不是依据变量的类型来定而是依据变量所指向的对象类型来定的。对于前面的例子,类A中的方法f是一个动态方法,它在类C的子类D中被覆盖。这样当我们同样调用c.f()时,我们没有办法在编译时确定变量c指向的类型是D还是C,因而也就无法确定应该编译成机器标号D_f,还是A_f。•动态方法表(也称为虚函数表)£为解决动态方法在编译时无法确定应该编译成的机器标号,引入了动态方法表,该表记录了每个动态方法对应的机器标号。£当类B从类A派生出来以后,它的动态方法表首先包含A中已有的方法,然后才是自己新增的动态方法。classA{intx;voidf();}classB:publicA{voidg();}classC:publicB{voidg();}classD:publicC{inty;voidf();}AA_fBA_fB_fCA_fC_gBD_fC_g类的动态方法表当定义一个对象时,我们会在对象的起始地址处插入一个指针指向该对象所在类的动态方法表,这个指针在c++中也被称为虚指针。有了这些准备,当编译c.f()时,就会生成以下的指令:1.在对象c的起始地址取得虚指针;2.从虚指针指向的动态方法表对应的f所在的偏移量处(这是一个常量)获得机器标号p;3.跳转到地址p处继续执行,并保存返回结果。•根据上面属性和方法的编译方案,下面程序对应的对象实例如右图所示:classA{intx;voidf();}classB:publicA{voidg();}classC:publicB{voidg();}classD:publicC{inty;voidf();}动态方法示意程序A_fB_gxA_fxABA_fC_gxCD_fC_gyxD单继承的对象实例多继承的编译方案•与多继承性有关的所有问题和可能的解决办法都可以通过双继承性来阐明。•多继承性引入了许多单继承性不存在的问题,主要包括命名冲突和重复继承。1)命名冲突:当基类B1和B2包含相同的方法名或属性名时,类C从B1和B2派生时就会引起冲突2)重复继承:例如在图10.8中,若B1和B2都直接从类A继承,那么C将从A重复继承,也就是说C中将包含两份A的实例。•对于问题(1),它是一个语言定义的问题,与编译实现关系不大。•对于问题(2),会有两种实现,一种是允许多个基类的实例,一种是只允许一个基类的实例。AB1B2C重复继承A附加(B1)A附加(B2)附加(C)重复继承的多个实例附加(B1)A附加(B2)附加(C)重复继承的单个实例私有属性和方法的编译●真正的面向对象语言能够为对象的属性提供保护,防止其它对象的方法直接操作它们。◊私有属性是指禁止从不属于该对象的函数中读取或访问的属性;◊私有方法是指不能从对象外部调用的方法。●不同的语言允许不同的私有性和保护,包括:◊属性和方法只能从声明它们的类中访问。◊属性和方法只能从声明它们的类和该类的任何子类中访问。◊属性和方法只能从与声明它们的类位于相同的模块(包,名字空间)访问。◊属性在声明它们的类外部是只读的,只能被该类的方法修改。面向对象语言的编译优化●面向对象语言的编译优化一个很重要的方面是将对动态方法的调用尽可能的转化为静态方法的调用。◊现代的计算机体系结构处理向常量地址的跳转比处理从表中获取的地址跳转更加有效。当指令流中的跳转地址为常量时,处理器能够引导指令获取机制预取位于跳转地址中的指令,并存于cache中;但当跳转地址无法预测时,预取也无法实现,因而就会将指令的获取-执行流水线滞延好几个周期。◊一个编译器如果不知道在某个位置将被调用的函数实例,也就对很难分析出这个函数调用的影响。那么编译器就不能对内联函数扩展或是过程间分析做有效的优化。因此,这就需要面向对象语言的优化编译器通过全局程序分析找到那些总是调用相同的函数实例的动态方法调用,并将对它们的调用替换为对相应静态方法的调用。