Delphi的特性还记得么?其中一条就是,既支持面向过程的编程,又支持面向对象OOP的编程,虽然我们完全可以在不了解OOP语言特性的情况下编写Delphi程序,但是我们只有充分理解这种特性后才能更充分的了解Delphi编程环境。

我们先了解一下OOP编程语言的三大特性:

  1. 封装(表现为类的实现,private的设置)

它就是将类的数据封闭在类的内部,使得外部程序必须使用正确的方法才能对类内部的数据进行访问,它很大程度上的保护了类内部的私有数据。

  1. 继承(父类子类等)

子类继承父类,直接就获得父类的一切内容,包括父类的每一个属性和方法,然而子类可以在其上进行进一步的发展扩充。就象现实中人类,分男人和女人,那么父类就是人类,它用有人类的共同特性,而男人和女人是有区别的,这些区别便可以分别表示在子类中的扩充中。

  1. 多态(函数的重载等)

多态意味着我们同样的动作可以由多种方式来实现,这取决于执行该动作的类。例如吃事物这个事件,人们吃饭需要将食物做熟,并使用工具帮助进食,而其他动物可能就是直接的食用,因为类别的不同,即使是同样的事件,也存在着不同的处理方式,这就类似于多态。

这是很重要的,我们在程序设计时按照这种逻辑思路去设计,将会明白这三个词所代表的意义有多大。

一:类与对象。

我们在使用Delphi时,每加的一个组件其实就是一个类的对象。最初的Form1我们也可以把它看做一个类的对象,不过这些类是在系统库中就被定义好的,它的属性,函数都是在系统库中。类本身是不可直接被调用的,它仅仅在内存中为自己的对象提供一个指针或者引用,在我们使用它之前,必须创建一个新的实例,用它来分配内存,并且调用类中的一切属性和函数。

我们声明一个类,使用保留字class,如

Type
  TMyFirstClass = Class(TObject)
  ……
  end;

这是声明了一个类,它继承于TObject类。值得注意的是,类的标识符通常以“T”开头,仅仅是个命名规则,编译器是不在乎的。但为了可读性还是加上的好。另外,若是新建的类没指明其父类的话,则默认的认为该类继承TObject类。如

Type TMySecondClass = class
 ……
 end;

之后我们可以声明它的对象

var
  MyFirstClass : TMyFirstClass ;

再在后面加个点就可以调用其中的属性或者函数方法了。

如C++一样,我们在生成一个类的对象并分配内存时,需要调用它的构造方法,不过在C++中,我们New的时候,系统自动为我们做了调用构造函数的事情,而Pascal中就需要我们手工调用该函数进行内存分配和初始化了,默认的系统会给一个Create的构造函数,但是它不会帮助我们初始化数据,要想初始化数据,我们需要自己来写我们的构造函数,使用关键字Constructor.例如

Type
  TMyThirdClass = Class
  Name : String;
  Age : Integer;
  Constructor Create();
  Function IsAdlt : Boolean;
 end;

其中构造函数的实现为

Constructor Create();
Begin
Name := 'duzhi';
Age := 23;
end;

这样,我们在创建一个该类对象时就可调用其构造函数为对象分配内存并且初始化。

var
Duzhi3 : TMyThirdClass.Create() ;

或者分两步为:

var
Duzhi3 : TMyThirdClass ;
begin
Duzhi3.Create*();
……
end;

当然我们需要注意的是,我们在程序运行结束够并没有释放刚才创建的对象,要释放对象,就要使用Free函数。

但是要注意的是Free完毕后,我们的对象并非一定指向Nil(相当于C++中的NULL)。

我们可以会疑问,哪儿来的Free函数,包括在构造对象时,我们可以调用系统的Create函数,这个函数又是哪儿来的呢?还记得开始我说过的么?我们的类,默认的都是继承TObject的,在其中就有这两个函数,他们是负责对象内存的创建和释放的。我们的任何类都继承了这两个方法,所以可以直接调用。

二:类的封装

我们在设计类的时候,封装性是为了增加类的安全性而设计的,我们可以将类想象为一个“黑盒子”,我们可以完全不清楚它内部到底是什么,它仅为我们留下了一个小小的接口,我们不能直接访问类的内部成员,这种OOP设计对安全性,可修改性都很有利。对于封装性,ObjectPascal(op)提供了五个控制符:

public,private,protected,published和automated。

1)public

它是允许在外部对该类型进行访问的,和C++/JAVA等语言是完全一致的,不再细说了,使用方法如下例

2)private

它是完全定义在类内的私有成员的,不允许在外部进行访问调用,但在类内的代码允许进行调用,使用方法如下

type
  TMyForthClass = Class(Object)
  public
  Name :String;
  private
  Age : Integer;
  public
  Function GetAge :Integer;
end;

我们在外部可以调用Name属性和GetAge函数,对Age却是不可进行调用的。例如

var
Duzhi4 : TMyForthClass ;
iTemp : Integer;
begin
Duzhi4 := TMyForthClass.Create;
Duzhi4.Name := 'Duzhi';    // 正确的
// Duzhi4.Age := 23;       // 错误的
iTemp := Duzhi4.GetAge; // 正确的
end;

3)protected类型

和C++一样,允许被其子类(派生类)访问,不赘述了。

4)published类型

这是OP中特有的,它的可见性最高,甚至比public还高。public是程序在运行期间可以被访问,而该“发行成员”在设计期间和运行期间都可以被访问。实际上某志现在还不大清楚其真实的意义。呵呵,等候高手解答。

5)automated类型

和public基本一致,唯一区别是声明为该类型的属性方法会自动生成类型信息,为创建OLE(自动化服务)成为可能。

三:类的继承性

这个性能主要目的,某志觉得就是节省代码,然后就是对类的优化好处比较多。举例,我们在某个程序6.0版本中发现了一些比较优秀的方法,在7.0中我们准备继续使用,OK那么我们继承,觉得有问题的方法,我们就不再继承,在7.0版本中也可以再添加一些新的方法。其实,我们如果复制6.0的有效代码,拷贝到7.0上单独又算一个新的类到也没有什么问题,不过当某一方法出现问题了,6.0和7.0中都需要分别修改一遍罢了,其他区别还真没发现。

四,类的多态性。

这里我给出一段代码,大家从中看看能够注意到多少细节。

type
 Tshape = Class  //定义了一个TShape类,注意,该段后没有分号
 ……
 procedure Paint(x,y:Integer);Virtual;  //声明了一了虚函数,注意声明方式
end;
 TCircle = Class(TShape) //继承TShape类的TCircle类。
 ……
 procedure Paint;Override;  //重载了Paint方法,注意没带参数,注意重载方式,后面必须加Override
end;
  TRectangle = Clas(TShape)  //继承了TShape类的TRectangle类
  ……
  procedure Paint;Override; //重载了Paint方法
end;
Var
 MyShape : TShape;
 x,y : Integer;
 begin
 x:=50;
 y:=50;
 MyShape := TCircle.Create;   //创建TCircle的对象
 MyShape.Paint(x,y);  //调用TCircle对象的Paint方法
 MyShape.Free;  //别忘记释放对象
 MyShape := Treatang.Create;   //创建Treatang的对象
 MyShape.Paint(x,y);  //调用Treatang对象的Paint方法
 MyShape.Free;  //别忘记释放对象
end.     //注意,最后是句号,表示代码结束。

我们在主程序部分,可以发现,有两行MyShape.Paint(x,y); 此时我们是调用哪个Paint方法呢?是父类还是子类,是哪个子类,显然,编译器需要根据调用此方法的对象类型进行判断。

值得注意的是重载的方法必须和父类中的同名虚函数在参数个数,顺序,类型上都完全一致,若是有返回值的函数的话,返回类型也必须相同。

五:静态方法。

除非特别说明,否则声明的方法都是静态方法,他和一般的过程和函数是没有区别的,编译器能够确定静态方法的确切内存地址,对它的访问编译都极快。

但是由于静态方法即使被类继承,它的子类也将在相同的地方使用同样的内存地址进行该方法的访问,所以,静态方法是无法进行重载的。他仅会被其他函数覆盖。所以,在上面的例子中,若我们不对函数添加virtual修饰词的话,下面的方法都会互相进行覆盖,最后仅仅留有最后声明的函数。

但若是改变了函数的参数个数,顺序,类型的话,那就不会被覆盖,系统会认为他们不是一个函数。

六:虚函数和方法的重载,重写。

其实在例子中我们已经可以看出,只要在函数后加以virtual修饰,就代表它是一个虚函数,当我们子类中想重载这个函数时,可以在新的函数后增加一个Override指令。但是可能有三种情况会导致编译错误:

  1. 该方法在父类中不存在,子类也就无从继承了。
  2. 该方法在父类中是静态的,那么也无法进行继承。
  3. 该方法和父类中的声明不完全相同(参数类型,个数,顺序不同),其实也是第一种情况的变形了。

函数的重写,使用关键字overload来标志该方法,就会获得一个具有相同名字的方法,且两方法的参数是不同的。

可能有朋友弄不清重写和重载的区别,实际上可以简单理解区别为是否在一个类中,若两同名异参的函数在一个类中,就理解为重写,相反,在不同类中,两类又有父子关系,就为重载。

七:动态方法。

呃,本来某志不想提虚函数表的,因为这块很容易弄晕人。但是这个动态方法和虚方法的区别仅仅就是一个虚函数表,不得不说了。

我们需要知道,虚函数的地址,是由运行期间由对象进行寻找的,而虚函数在声明时就在对象的VMT(虚拟函数表)中创建了一个表项,在这里存储自己的地址,以方便对象的访问。我们每创建一个子类,它就会把其父类的VMT继承过来,再加以补充自己的虚函数。

这样做法就导致了动态方法和虚方法的区别。动态方法的地址没有虚方法好找,因为它没有自己的VMT方便寻找。但是动态方法又比虚方法节省内存(不用保存VMT了啊),这两一个在时间上,一个在空间上有自己的优势,根据用户需要了。 要使用动态方法,可以在其声明之后增加指令Dynamic.

八:抽象类

和JAVA中一样,加个Abstract指令。之后它的子类就必须实现这个抽象方法咯,除非其子类也是抽象类,那么不需要实现该方法,但是抽象类是不能实例化生成其对象的。

九:补充说明

1:注意,之前我们有说过,函数可以有一至多个带有默认值的参数,即是将该参数加以括号标志出的,此时我们传参时可忽略此参数。他们会获得默认值。

2:在一个方法中,可以使用Self关键字访问当前对象。当引用对象的本地数据时,对Self的引用是隐含的了。

3:默认情况下,方法是使用register调用约定:简单的参数和返回值要从调用代码传递到函数之中,并且使用CPU寄存器而非堆栈返回,这个过程可以使方法调用的更快。

4:若我们在封装时,想避免使用全局变量,可以为该变量设置一个Get和Set方法,使其他类调用该变量时,必须使用这两种方法来进行访问,此时我们可以这么写代码

property Str :String read GetText write SetText;

在我们按下Ctrl+Shift+C进行代码框架的自动编写时,系统会自动的生成两个方法,GetText和SetText,分别为对给变量Str的读取访问和写入访问。这样的话我们可以改善封装性。