Delphi 高手突破(申 旻 著)-第 2 章 面向对象编程理论基础

面向对象是一种思维方式(理念),是一种方法论。

每个软件开发人员都会经常听到、 看到“ 面向对象” 这个词,程序员们也时常会把它挂在嘴上。那么, 什么是面向对象?什么是面向对象编程?是不是写几个类就算面向对象了?为什么要面向对象?因为别人都用,所以我也要用?显然, 并不是在程序中写了几个类就算面向对象编程了, 用面向对象编程也并不是为了赶时髦。

“结构化编程” ( SP) 是一种编程方法, 是用计算机的视角来抽象问题的方法。 而“ 面向对象编程” ( OOP)也是一种编程方法, 它从更接近真实世界的视角来分析问题,使用更接近人们理解真实世界的方法来抽象问题,这种方法称为“面向对象”( OO)。

“ 面向对象” 这个词代表的是一种认识世界、分析问题、解决问题的方法, 因此它是一种方法论。而面向对象编程( OOP) 则是将之应用于编程的方法。 当用户会使用面向对象的方法去思考,用面向对象的模式去分析和解决问题的时候,才是真正的“面向对象”了。

本章会试图使用 Object Pascal 语言来告诉用户面向对象编程的理论知识,包括面向对象编程的特性以及这些特性在语言中的实现、在语义上对程序设计的影响。

2.1 类和对象的本质

“类” 和“ 对象” 是面向对象编程中最基本的概念, 很多人都可以轻易地回答出什么是类, 什么是对象: “类” 是对一类事物的抽象( abstract) , 是创建对象的模板; “对象”是类的实例( instance)。

但这只是简单的概念解释而已,除此以外,还必须清楚类和对象的本质,即从语言和语义本身的角度来看,它们分别代表什么,它们是如何支撑起庞大的面向对象的世界的。

2.1.1 语言的“类”和“对象”

从语言的角度来说,“ 类”是用户自定义的数据类型,“ 对象” 则是“类” 类型的变量。类定义了所生成的对象的模板,于是也决定了对象所占用的内存空间。类成员分为两种: 方法(即 C++中的“成员函数” , 笔者个人比较喜欢“成员函数” 的叫法, 但“方法”已经是 Object Pascal 的一个术语。 在 Delphi 相关的书中, 称“方法” 更合理一些, 因此下文全部以“ 方法”称呼)和数据成员。数据成员表示类对象的状态, 而方法则是改变类状态的操作。

当用 class 关键字声明一种类型时,就创造了一个类:

type
  TMyClass = class
end;

虽然在 TMyClass 中,还没有为它定义任何成员,但是,它的确是一个类,完全可以
创建这个类的对象实例:

var
MyObj : TMyClass;
begin
MyObj := TMyClass.Create();
......

Create()是这个类的构造函数,它负责初始化对象数据成员。
生老病死是人之常情,对象也一样会有被销毁的时候:

......
MyObj.Free();

不过, Free()方法不是类的析构函数, Free()负责的是调用类的析构函数来销毁对象。
至于采用这种机制的原因,稍后会讲述。
现在就来探讨一下 Object Pascal 中对象生存与销毁的秘密吧!
每个应用程序可以获得的内存空间分为两种:堆( heap) 和栈( stack)。
堆又称为“ 自由存储区”,其中的内存空间的分配与释放是必须由程序员来控制的。
例如, 用 GetMem 函数获取了一定大小的内存空间, 则在使用完后, 必须调用 FreeMem 函
数将空间释放,否则就会发生所谓的“内存泄漏”。“借债还钱,天经地义”。
栈又称为“ 自动存储区”,其中的内存空间的分配与释放是由编译器和系统自动完成
的,不需要程序员过问。函数调用时按值传递的参数所占空间、函数中的局部变量等, 都
是在栈中被分配空间的。比如函数:

var
  i : Integer;
  j : Integer;
begin
  for i := 1 to 10 do
  ......
  ......
end;

其中, i 和 j 的空间是由编译器在栈中分配的。 在函数末尾, 也不需要程序员手动去释放这两个变量所占的内存空间。Objecgt Pascal 遵循所谓的“引用/值” 模型。 无论在参数传递还是变量定义中, 简单类型( 如 Integer、 Cardinal、 char 以及 record 等) 被按值传递或使用, 其内存空间从栈中分配。

而复杂类型( class) 则被按引用传递或使用, 其内存空间从堆中分配。 在 Object Pascal 中,
所有对象( 类类型的) 都被建立在内存的堆空间上,而非栈上。因此在创建对象时,其构
造函数不会被编译器自动调用,也没有 C++中所谓的“默认构造函数”。调用构造函数来
创建对象以及调用析构函数来消灭对象都是程序员的职责。
如何为自己的类编写构造函数呢?在调用了诸如
MyObj := TMyClass.Create();

MyObj.Free();
之后究竟发生了哪些事情呢?
? 构造函数与对象内存的分配
定义构造函数使用 Constructor 关键字。按惯例,构造函数名称为 Create(当然也可以
用其他名称,但那绝非优良的设计)。如:

type
TMyFamily = class // 为你的家庭定义的类
Private
FMyFatherName : String; // 你父亲的名字
FMyMotherName : String; // 你母亲的名字
…… // 你家庭中的其他成员
Public
Constructor Create(strFatherName, strMotherName : String);
…… // 其他方法
End;

创建对象时则直接调用构造函数,形式如下:

MyFamilyObject := TMyFamily.Create(′Zhang′, ′Li′);

也许有人会问, 如果没有为自己的类提供构造函数, 它的对象能否被建立呢?答案是:
可以。在了解了构造对象的过程之后,就会明白为什么答案是“可以”。
要创建出一个对象,首先需要分配对象本身所占用的内存空间, 然后执行类的构造函
数,以初始化各数据成员、申请对象需要的资源或创建其内部包含的子对象。
编译器在执行类似 MyFamilyObject := TMyFamily.Create(‘Zhang’, ‘Li’);这样的构造函
数之前,会插入以下几行汇编代码:

test dl, dl
jz +$08
add esp, -$10
call @ClassCreate // 注意这行代码

以上代码的最后一行代码调用的是 system.pas 文件的第 8949 行的_ClassCreate 函数 (以
Delphi 6 为准),该函数具体为每个对象分配合适的内存。这个动作也就是所谓的“编译
器魔法” ( Compiler Magic) , 由这个动作完成真正的对象的内存分配, 一个对象在这个时
候已经有了外壳。
内存分配完成后是调用类的构造函数,即 TMyClass.Create(),以初始化数据成员。构
造函数由定义类的程序员编写, 也就是说, 将对象初始化成何种模样是由程序员决定的。
至此,一个对象已经诞生了。
之后,编译器会再插入以下几行汇编代码:
test dl, dl
jz +$0f
call @AfterConstruction
pop dword ptr fs:[$00000000]
add esp, $0c
其中主要的工作是调用每个对象实例的 AfterConstruction。 Borland 宣称这个调用在
Delphi 中没有用, 它的存在是为 C++ Builder 所保留的。 不过, 由于 AfterConstruction 被声
明为虚方法( virtual method),因此完全可以利用它做一些善后的工作,尤其是在编写组
件时。这些内容将在后续章节讲述,暂且不必理会。
可见,创建一个对象的步骤并不十分复杂,比之“十月怀胎”轻松了不知道多少倍。
由于对象本身所占内存的分配是由编译器完成的,因此即使没有构造函数, 对象也一
样可以被构造。构造函数的职责只是初始化对象的数据成员,没有构造函数只意味着不会
对数据成员进行初始化而已,编译器会对所有数据进行清零初始化。此外,由于 Object Pascal
中所有类 (除了 TObject 类本身)都是从 TObject 类派生,因此编译器会调用 TObject.Create()
构造函数。不过,这个函数只是一个空函数。
假如上面定义的 TMyFamily 类没有定义构造函数,则 TObject.Create 也不会对
TMyFamily 的 数 据 成 员 (FMyFatherName 、 FMyMotherName ) 进 行 初 始 化 , 因 为
TObject.Create()根本就不认识你的父、母亲!
? 析构函数与对象内存的回收
定义析构函数使用 Destructor 关键字。按惯例,析构函数名称为 Destroy。如:

type
  TMyClass = class
  Public
    Destructor Destroy(); override;
End;

之所以在析构函数声明最后加上 override 声明, 是为了保证在多态的情况下对象能正确被析构(关于多态, 将在 2.4 节中详述) 。 如果不加 override 关键字, 则编译器会给出类
似“ Method ‘Destroy’ hides virtual method of base type ‘TObject’” 的警告提示。警告的意思
是用户定义的 Destroy 隐藏了基类的虚方法 TObject.Destroy(), 那样的话, 在多态的情况下
就无法正确析构对象了(具体原因请查 2.4 节) 。
È注意: 析构函数都需要加 override 声明。
与构造函数类似, 如果在类中没有特殊的资源需要被释放,也可以不定义析构函数,
TObject 同样定义了一个空的析构函数。
在析构对象的时候,应该调用对象的 Free()方法而不是直接调用 Destroy()。

MyFamilyObject.Free();

这是因为, 在 TObject 的 Free()方法中会判断对象本身是否为 nil,如果不为 nil 则调用
对象的 Destroy(),以增加安全性。既然有这样更安全的做法,当然没有理由不这么做了。
È注意: 永远不要直接调用对象的 Destroy(),而应该是 Free()。
要销毁一个对象, 其顺序与创建对象正好相反。首先是释放对象申请的资源以及销毁
内部的子对象,之后是回收对象本身所占的内存空间。
当程序执行到诸如 MyFamilyObject.Free();这样的代码时,首先执行 TObject.Free()
方法:

procedure TObject.Free;
begin
  if Self <> nil then
    Destroy;
end;

在 TObject 的 Free()方法中, 调用了对象的析构函数。 然后, 编译器会在执行完 Free()
方法之后,插入以下几行汇编代码以完成第二个步骤(回收对象本身所占的内存空间):

call @BeforeDestruction
test dl, dl
jle +$05
call @ClassDestroy

这 些 代 码 所 做 的 工 作 与 构 造 对 象 分 配 内 存 时 所 做 的 是 对 应 的 , 其 中 所 调 用 的
_ClassDestroy 函数会精确地回收对象内存空间。
以下一个例程说明了如何使用构造函数和析构函数:

unit DllLoader;

interface
uses windows;
Type
  TDllLoader = class
  Protected
// 之所以是 protected 成员, 是为了在其派生类中具体实现加载某 DLL 时, 派生类能够访问该句柄
    FhDLL : HMODULE;
  Public
    Constructor Create(strDLLName : String);
    Destroctor Destroy(); override;
  End;

Implementation

Constructor TDllLoader.Create(strDLLName : String);
Begin
  FhDLL := LoadLibrary(strDLLName); // 构造函数中加载 DLL
  ASSERT(FhDLL <> 0);
End;

Destructor TDllLoader.Destroy();
Begin
  If FhDLL <> 0 then
  begin
    FreeLibrary(FhDLL); // 析构函数中释放 DLL
    FhDLL := 0;
  End;
End;

End.

对象所占空间大小前面为对象分配内存空间时谈到, 每个对象会占用一定的内存空间, 那么这个大小是如何确定的呢?对象的大小,就是其数据成员所占用的内存空间的总和, 其方法(函数)是不占用对象空间的。不过, 它不是一个简单的加法,还与编译器的“数据域对齐方式优化”有关,稍后会详述。

注意: 对象的大小只取决于其拥有的数据成员。
TObject 实现了一个 InstanceSize()方法, 它可以取得对象实例的大小。 下面以一个示例
说明对象在内存中的布局。首先定义一个 TMyClass,其中包含 4 个数据成员和 1 个方法。
先看一下类的定义:

Type
  TMyClass = class
  Public
    FMember1 : Integer;
    FMember2 : Integer;
    FMember3 : WORD;
    FMember4 : Integer;
    Procedure Method();
  End;

然后,在 Application 的主 Form 中放入一个 Memo 和一个 Button,并在 Button 的 OnClick
事件中写下在 Memo 中显示出对象位置的代码。该程序源代码清单如下:

unit Unit1;

interface

uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls,Forms,Dialogs, StdCtrls;

type
   TForm1 = class(TForm)
      Button1: TButton;
      Memo1: TMemo;
      Label1: TLabel;
      procedure Button1Click(Sender: TObject);
   end;
// 自定义的 TMyClass 类
   TMyClass = class
   Public
      FMember1 : Integer;
      FMember2 : Integer;
      FMember3 : WORD;
      FMember4 : Integer;
      Procedure Method();
   End;

var
   Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
var
   Obj : TMyClass;
begin
   Obj := TMyClass.Create();
   with memo1.Lines do
   begin
      Add(‘对象大小: ‘ + IntToStr(Obj.InstanceSize));
      Add(‘对象所在地址 : ‘ + IntToStr(Integer(Obj)));
      Add(‘FMember1 所在地址: ‘ + IntToStr(Integer(@Obj.FMember1)));
      Add(‘FMember2 所在地址: ‘ + IntToStr(Integer(@Obj.FMember2)));
      Add(‘FMember3 所在地址: ‘ + IntToStr(Integer(@Obj.FMember3)));
      Add(‘FMember4 所在地址: ‘ + IntToStr(Integer(@Obj.FMember4)));
   end;
   Obj.Free();
end;

{ TMyClass }
procedure TMyClass.Method;
begin
   //no code
end;
end.

Button 的 Click 事件中所做的事情是, 首先创建 TMyClass 类的实例, 然后将对象大小
以及每个数据成员的地址输出到 Memo 中。
该程序的代码和可执行文件可在配书光盘的 ObjectSize 目录下找到,运行程序并单击
“开始”按钮后,其界面如图 2.1 所示。

图 2.1 ObjectSize 程序界面

图 2.1 的 Memo 中显示出了想要的结果。也许读者会看不清图片中的结果,不妨介绍
一下:其中显示了对象大小为 20 个字节,对象所在首地址是 13443352(也许每次运行对
象被创建的地址会不同, 这没有关系, 在此主要关心的是各地址之间的差值) , FMember1
所在地址是 13443356, FMember2 为 13443360, FMember3 为 13443364, FMember4 为
13443368。 现在来分析一下:
根 据 对 象 首地 址 以 及 大小 可 以 算 出, 对 象 占 用的 内 存 空 间范 围 为 13443352 ~
13443371。 而第一个成员却在 13443356, 它与对象的首地址之间有一个 4 字节的空缺, 这
4 个字节存放的是一个指向对象的 VMT(虚方法表)的指针。关于 VMT 将在 2.4 节多态
的本质中详细讨论,此处暂且不表。
13443356~13443359 这 4 个字节即 FMember1 所占空间 ( 32 位整数)。同样,13443360~
13443363 为 FMember2 所占空间。比较容易令人疑惑的是 FMember3,计算可知,它所占
地址范围为 13443364~13443367,同样也是 4 个字节,但在此定义的 FMember3 其实是
Word 类型( 16 位) , 为什么它会占用 32 位空间呢?这与编译器的字节对齐优化有关, 编
译器会将无法合并的小于 32 位空间的数据域填充到 32 位大小,以加快存取速度。也就是
说, FMember3 同样需要占用 4 个字节空间。 可以自己试一下, 如果将以上 TMyClass 类定
义中的 FMember2 也改成 Word 类型,编译器会把 FMember2 和 FMember3 合并成一个 32
位空间,于是对象大小就变成了 16。
FMember4 所占的空间没什么意外,为 13443368~13443371。
整个对象的内存布局如图 2.2 所示。

另外,也可以看到, TMyClass 类中惟一的方法 Method()没有在对象的空间中出现。
? “类方法”与“类引用”类型
一般所称的“方法”, 都是指“对象方法”。 也就是说, 执行该方法,将可能导致对
象的状态发生改变,即该方法可以更改对象的数据成员的值。如:

其中 SetValue 即为典型的对象方法。
除了“ 对象方法” 外, 还有所谓的“类方法” ,也就是属于类级别的函数( 而非对象
级别的)。它可以改变类的状态(而非对象的状态)。
定义类方法,只需要在一般的方法声明前加上 class 关键字。 如:
class function TObject.ClassName: ShortString;
既然类方法是进行类级别的操作, 因此在类方法中是无法对对象的数据成员进行访
问的。
在 Object Pascal 中,还有一种“类之类”的类型,也就是所谓的“类引用”。一般所
称的类,是对其实例对象的抽象。定义一个类:
TMyClass = class;
而“类引用”类型却是对“类”的抽象( 元类),所以被称为“ 类之类”。 定义一个
“类之类”:
TMyClassClass = class of TMyClass;
“类之类”可以直接调用“类”的“类方法”。如:

TMyClass = class
  public
    class procedure Show();
end;
TMyClassClass = class of TMyClass;

var
  MyClass : TMyClassClass;
  MyObj : TMyClass;
begin
  MyObj := MyClass.Create();
  MyClass.Show();
  MyObj.Free();
end;

在此例中, TMyClassClass 作为 TMyClass 的元类, 可以直接调用 TMyClass 的类方法。
此前提到过的类构造函数,其实就是一个类方法,因此可以如同
MyObj := MyClass.Create();
来创建对象,其结果与
MyObj := TMyClass.Create();
完全相同。
但是, 析构函数则不是类方法,而是普通的对象方法。因为析构函数只能销毁一个对
象实例, 其操作结果并非作用于该类的所有对象。因此, 销毁对象只能通过对象来调用析
构函数而不能通过类方法:
MyObj.Free();
“类方法” 和“类引用”有什么作用呢? 它主要用在类型参数化上, 因为有时在编译
时无法得知某个对象的具体类型, 而需要调用其类方法( 如构造函数),此时可以将类型
作为一个参数来传递。在 Delphi 6 的帮助文档中,有这样一个例子:

type TControlClass = class of TControl;
function CreateControl(
  ControlClass: TControlClass;
  const ControlName: string;
  X, Y, W, H: Integer
  ): TControl;

begin
  Result := ControlClass.Create(MainForm);
  with Result do
  begin
    Parent := MainForm;
    Name := ControlName;
    SetBounds(X, Y, W, H);
    Visible := True;
  end;
end;

时间: 2024-10-05 23:54:46

Delphi 高手突破(申 旻 著)-第 2 章 面向对象编程理论基础的相关文章

Delphi 高手突破(申 旻 著)-第一章 重新认识Delphi

简单性是这个世界上最难获得的东西:它是经验的最终界限,也是天才的最终努力目标.——George Sand 您已经是一位熟练的Delphi程序员,可以运用Delphi快速地写出一个漂亮.实用的程序:您热爱Delphi:她已经成了您工作.学习中不可或缺的一部分.我假设这些都为真,那么您当初选择Delphi作为自己的首选开发工具一定有自己的理由或者原因. 至少,我自己是符合以上的所有假设的.现在,我所想和您分享的,正是我选择Delphi的理由及原因,以及我对Delphi的认识.您可以把我看作一个拥护D

Delphi高手突破(四) Delphi高级进阶

别人造砖我砌房! Delphi  高手突破     VCL——Visual Component Library,是 Delphi 的基石.Delphi 的优秀,很大程度上得益于 VCL 的优秀.VCL 是 Delphi 所提供的基本组件库,也就是所谓的 Application Framework,它对Windows API(应用程序接口)进行了全面封装,为桌面开发(不限于桌面开发)提供了整套的解决方案,使得程序员可以在不知晓 API 的情况下进行 Windows编程.不过,作为专业的程序员,不知

Delphi高手突破(三) Delphi高级进阶

第 3章  异常及错误处理 健壮的程序来自于正确的错误处理.    相信我,总会有意外的…… Delphi  高手突破     正如同现实生活中我们不可能事事如意,你所写的代码也不可能每一行都能得到正确的执行.生活中遇到不如意的事情,处理好了,雨过天晴:处理不好,情况会越变越糟,甚至一发而不可收拾,后果难料.程序设计中同样如此,所谓健壮的程序,并非不出错的程序,而是在出错的情况下能很好地处理的程序.因此,错误处理一直是程序设计领域的一个重要课题.而异常就是面向对象编程提供的错误处理解决方案.它是

delphi高手突破之异常及错误处理

什么是异常?为什么要用它? 所谓“异常”是指一个异常类的对象.Delphi的VCL中,所有异常类都派生于Exception类.该类声明了异常的一般行为.性质.最重要的是,它有一个Message属性可以报告异常发生的原因. 但需要强调的是,异常用来标志错误发生,却并不因为错误发生而产生异常.产生异常仅仅是因为遇到了raise,在任何时候,即使没有错误发生,raise都将会导致异常的发生.异常的发生,仅仅是因为raise,而非其他!采用抛出异常以处理意外情况,则可以保证程序主流程中的所有代码可用,而

Delphi的面向对象编程基础笔记

1.面向对象.一门面向对象的编程语言至少要实现以下三个OOP的概念 封装:把相关的数据和代码结合在一起,并隐藏细节.封装的好处是利用程序的模块化,并把代码和其他代码分开 继承:是指一个新的类能够从父类中获取属性和方法,这种概念能够用来建立VCL这样的多层次的对象,首先建立通用对象,然后创建这些通用对象的有专用功能的子对象.继承的好处是能够共享代码 多态性:从字面上看,是指多种形状.调用一个对象变量的方法时,实际被调用的代码与实际在变量中的对象的实例有关. 2.Object Pascal不像C++

买了本Delphi面向对象编程思想,正在看,产生些问题。

1:第33页说,Delphi通过调用类的一个构造函数来建立一个对象的实例,对象至少有一个create()的构造函数,使用时候写MyObject:=TmyObject.create即可.   但是第37页说,在方法的分类中有一项为构造方法.   Type 类名= class(基类)      constructor 构造方法名(参数)      ...    constructor create 和 create 有什么区别?    前者是否是Create的用户定义实现构造,而后者是用Tobjec

Delphi面向对象编程

一.面向对象介绍 OOP是使用独立的对象(包含数据和代码)作为应用程序模块的范例.虽然OOP不能使得代码容易编写,但是它能够使得代码易于维护.将数据和代码结合在一起,能够使定位和修复错误的工作简单化,并最大限度地减少对其他对象的影响,提高代码的性能.一般OOP都支持一下三个概念: 1)封装:把相关的数据和代码结合在一起,并隐藏了实现细节.封装的好处是有利于程序的模块化,并把代码和其他代码分开 2)继承:是指一个新的对象能够从父对象中获取属性和方法,这种概念能用来建立VCL这样的多层次的对象,首先

Delphi基本之pascal语法(第五章.函数与过程程序设计)

第五章.函数与过程程序设计一.函数(包括标准函数和自定义函数) 1.函数的定义 格式:FUNCTION 函数名(<形参表>):返回值类型: VAR <变量说明> BEGIN <函数体> END [注]:1.形参表每个参数都写明其类型: 2.有且只有一个返回值,并且要将返回值赋值给函数名. [例]:求五边形的面积 function area(a,b,c:real):real; var p:real; begin p:=(a+b+c)/2; area:=sqrt(p*(p-

Delphi基本之pascal语法(第四章.循环结构程序设计)

第四章.循环结构程序设计 一.FOR语句格式:1.FOR <循环变量>:=<初值> TO <终值> DO <语句>: 2.FOR<循环变量>:=<终值> DOWNTO <初值> DO <语句>.[例1]:输入10个数,求最大值.最小值.和.及平均值. PROGRAM ten(input,output);VAR a,s,max,min,avg:real; i:integer;BEGIN write('please