【转】变量、指针、内存的关系

Delphi 中的所有类都是从 TObject 继承而来的,都具有 TObject 的所有特性,TObject 是所有类的根类。

我们可以在 System 单元中找到 TObject 的定义,但是这个定义并不完整,我们只能对 TObject 有一个大概的了解,因为 TObject 的核心功能是在编译器里面实现的,我们看不到具体实现代码。虽然如此,仍然有高手通过跟踪调试对 TObject 的核心功能有了一定的了解。在看过几位高手的解说之后,我对 TObject 也有了一定的认识,在这里总结一下,有助于以后学习 Delphi,虽然几位大师对 TObject 的讲解有细微不同,但是大体上都是一致的。

首先 TObject 是什么?TObject 是一个类啊,是整个 Delphi 的基石,没有 TObject 就没有 Delphi,那我们首先了解一下 Delphi 在编译的过程中是如何处理“类”的。

TObject 是 Delphi 定义的类,它和我们自己定义的类没什么区别,结构都是一样的。一个类定义好后,可以得到与这个类相关的很多信息,例如,我们定义了如下的一个 TMyObject 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
unit MyUnit;
 
interface
 
type
 
  TMyObject = class(TObject)
  public
    Data1: string;   { Sizeof(string)   =4 }
    Data2: Cardinal; { Sizeof(Cardinal) =4 }
    Data3: Boolean{ Sizeof(Bolean)   =1 }
    Data4: TDate;    { Sizeof(TDate)    =8 }
    function Method1(S: string): string;
    function Method2(I: Integer): Integer; virtual;
    function Method3: Boolean; dynamic;
    procedure Method4; dynamic;
  end;

从上面的定义中我们可以得到哪些信息呢?

1、类名:TMyObject

2、父类:TObject

3、数据名:Data1、Data2、Data3、Data4

4、方法名:Method1、Method2、Method3、Methoid4

5、所在单元名:MyUnit

6、存放所有数据所需要的总空间:4 + 4 + 1 + 8 = 17 字节(当然,编译器会进行优化处理,比如整数对齐)

7、各个数据的类型,各个方法的参数类型,返回值类型,动态还是静态等信息

8、当然还有各个方法的实现代码,编译后就成了机器码。

这些信息在 Delphi 编译程序的时候都会被编译到程序文件中,程序在运行的过程中可以很轻易的得到这些信息。这就是所谓的“运行时信息”(当然,“运行时信息”不止这些,还有其它)。

(这一段不一定准确,但简单有助于理解)在我们运行这个程序的时候,Windows 会把程序调入内存中执行,此时 TMyObject 就存在于内存中了,那么根据程序的入口地址就很容易推算出 TMyObject 的内存地址和各个方法的内存地址(Delphi 在编译的时候就已经算好了类和各个方法的相对位置,程序在载入内存时,各个代码的相对位置是不会变的,否则就乱套了)。所以此时虽然你还没开始使用 TMyObject,但是它的结构已经很清晰明了了,就像你看上面的源代码一样清楚,想要什么都可以随时找到。但是不会创建 TMyObject 的数据部分,因为数据部分是留给各个对象用的,类本身不需要数据。到此为止,程序还没有执行具体的功能,只是刚刚载入内存。下面我们就来看看通过“类”来创建“对象”的过程。

假如我们在程序中写入如下代码(程序中有窗体 TForm1(Form1),有按钮 TButton(Button1),有上面的 TMyObject 类),然后重新编译,看看程序会做些什么:

1
2
3
4
5
6
procedure TForm1.Button1Click(Sender: TObject);
var
  MyObj: TMyObject;
begin
  MyObj := TMyObject.Create;
end;

当我们按下 Button1,程序开始执行,当程序执行到 MyObj: TMyObject; 的时候,程序会分配 4 个字节(一个指针的大小)的内存空间用来存放 MyObj 这个变量(这个变量只是一个指针,指向一个 TMyObject 类型的对象,此时对象还没创建,所以它为 nil)。

当程序执行到 MyObj := TMyObject.Create; 的时候,就开始创建对象,怎么创建?是不是把 TMyObject 整个复制一份出来作为对象使用?当然不是,那多浪费啊?只需要将 TMyObject 中定义的数据部分给对象就可以了,为什么呢?因为对象的作用就是处理数据,除了处理除数据,它不干别的事情。用一个 TMyObject 可以创建出无数个对象,而每个对象对数据的处理结果都不一样,但是它们处理数据所用到的方法却是一模一样的,都是 TMyObject 中的方法,所以,当它们需要用某个方法来处理数据的时候,只需要去 TMyObject 那里找就可以了,没有必要把相同的方法给每个对象都复制一份。这就是“类”和“对象”在内存中的存在形式。

那内存是如何分配的呢?之前不是说了吗?“类”在编译的时候,就已经计算好了存放所有数据所需要的总空间(我们刚才算出来的是 17 个字节),此时只需要申请这么多内存就可以了,然后把申请到的内存的地址告诉给 MyObj 变量,那么 MyObj 变量就指向这块内存了,也就是指向这个对象了。

原来对象就是一块用来存放数据的内存块,这就完了吗?当然不是,只有一块空空的内存,对象怎么知道 TMyObject 在哪儿,这么去找相应的方法呢?所以还必须把 TMyObject 的地址告诉给这个对象,所以对象的内存并不是只有数据区域,它还需要额外的 4 个字节用来存储 TMyObject 的地址。实际上对象内存块最开始的 4 个字节存放的就是 TMyObject 的地址,之后的内存才用来存放数据。所以,MyObj 变量是直接指向 Addr(TMyObject)。下面我们以窗体类 TForm1 为例来验证一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
procedure TForm1.Button1Click(Sender: TObject);
var
  pTForm1, pForm1, pSelf: Pointer;
begin
    pTForm1 := Pointer(TForm1);
    pForm1 := Pointer(Form1);
    pSelf := Pointer(Self);
 
    Memo1.Clear;
    Memo1.Lines.Add('Form1    ' + IntToStr(Integer(pForm1)));
    Memo1.Lines.Add('Self     ' + IntToStr(Integer(pSelf)));
    Memo1.Lines.Add('');
    Memo1.Lines.Add('TForm1   ' + IntToStr(Integer(pTForm1)));
    Memo1.Lines.Add('');
    Memo1.Lines.Add('Form1^   ' + IntToStr(Integer(pForm1^)));
    Memo1.Lines.Add('Self^    ' + IntToStr(Integer(pSelf^)));
end;
1
2
3
4
5
6
7
8
9
{ 运行结果 }
 
Form1    17253152  { 对象 }
Self     17253152  { 对象 }
 
TForm1   5326932   { 类 }
 
Form1^   5326932   { 指向类 }
Self^    5326932   { 指向类 }

除了分配内存,程序还要做一些其它的工作,比如初始化类的接口表等,这些太复杂的就不研究了。

我们刚才说了“TMyObject 在内存中的结构已经很清晰明了了,就像你看上面的源代码一样清楚”,但是这只是电脑对此很很清楚而已,我们并不清楚,Delphi 并没有说明类是如何存在于内存中的,是如何工作的,所以我们不得而知,但是有很多人做过研究,说类的起始地址就是“虚拟方法表(VMT)”的地址,在“虚拟方法表(VMT)”的最前面存放了父类的“虚拟方法表(VMT)”的地址,接着又存放了“动态方发表(DMT)”的地址,然后是各个虚拟方法的地址,然后又是静态方法的地址。我大概看懂了各位大师的讲解,但是还没弄懂“静态方法”的地址是不是和“虚拟方法”的地址放在一起。所以“类”的内存结构对我来说还是很模糊,于是我用代码做了测试,不过结果又是一番景象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
unit Form1Unit;
 
interface
 
uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics,
  Controls, Dialogs, Forms, StdCtrls;
 
type
  TForm1 = class(TForm)
    Button1: TButton;
    Memo1: TMemo;
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;
 
  TMyObject = class(TObject)
  public
    Data1: string;   { Sizeof(string)   =4 }
    Data2: Cardinal; { Sizeof(Cardinal) =4 }
    Data3: Boolean{ Sizeof(Bolean)   =1 }
    Data4: TDate;    { Sizeof(TDate)    =8 }
  published
    function Method1(S: string): string;
    function Method2(I: integer): integer; virtual;
    function Method3: Boolean; dynamic;
    procedure Method4; dynamic;
  end;
 
var
  Form1: TForm1;
 
implementation
 
{$R *.dfm}
 
function TMyObject.Method1(S: string): string;
begin
  Result := S + 'ABC';
end;
 
function TMyObject.Method2(I: integer): integer;
begin
  Result := I + 123;
end;
 
function TMyObject.Method3: Boolean;
begin
  Result := True;
end;
 
procedure TMyObject.Method4;
begin
  Method3;
end;
 
{ 将字符串延伸到指定长度 }
function FormatStrLen(Str: string; Len: Cardinal = 18): string;
begin
  while Length(Str) < Len do
    Str := Str + ' ';
  Result := Str;
end;
 
procedure TForm1.Button1Click(Sender: TObject);
var
  MyObj: TMyObject;
  pCur: PCardinal;
  I: integer;
begin
  MyObj := TMyObject.Create;
  try
    Memo1.Clear;
 
    // 获取类的地址
    Memo1.Lines.Add(FormatStrLen('pTObject') + IntToStr(Cardinal(TObject)));
    Memo1.Lines.Add(FormatStrLen('pTMyObject') + IntToStr(Cardinal(TMyObject)));
 
    Memo1.Lines.Add('');
 
    // 获取 VMT 所指的内容
    Memo1.Lines.Add(FormatStrLen('pTMyObject^') + IntToStr(PCardinal(TMyObject)^));
 
    // 循环获取 VMT 后面的地址所指的内容
    pCur := PCardinal(TMyObject);
    for I := 1 to 30 do
    begin
      Inc(pCur);
      Memo1.Lines.Add(FormatStrLen('pTMyObject' + IntToStr(I) + '^') +
        IntToStr(pCur^));
    end;
 
    Memo1.Lines.Add('');
 
    // 循环获取 VMT 前面的地址所指的内容
    pCur := PCardinal(TMyObject);
    for I := -1 downto -30 do
    begin
      Dec(pCur);
      Memo1.Lines.Add(FormatStrLen('pTMyObject' + IntToStr(I) + '^') +
        IntToStr(pCur^));
    end;
 
    Memo1.Lines.Add('');
 
    { 获取各个方法的地址 }
    Memo1.Lines.Add(FormatStrLen('Method1') +
      IntToStr(Cardinal(MyObj.MethodAddress('Method1'))));
    Memo1.Lines.Add(FormatStrLen('Method2') +
      IntToStr(Cardinal(MyObj.MethodAddress('Method2'))));
    Memo1.Lines.Add(FormatStrLen('Method3') +
      IntToStr(Cardinal(MyObj.MethodAddress('Method3'))));
    Memo1.Lines.Add(FormatStrLen('Method4') +
      IntToStr(Cardinal(MyObj.MethodAddress('Method4'))));
  finally
    MyObj.Free;
  end;
end;
 
end.

通过分析内存,很难分析出“类”在内存中是如何组织的,而且 Delphi 在发展的过程中也会对类的存储结构进行调整和改良,所以我们还是不要纠结于类的存储形式。我们只需要使用 Delphi 给我们提供的方法来访问类信息就可以了。

到此为止,一个对象就被创建好了,这就是 TObject 的对象创建过程。因为我们并没有为 TMyObject 编写 Create 函数,所以 TMyObject.Create 调用的是其父类 TObject 的 Create 方法,我们把 TObject 的对象创建过程说完了。

与创建对象相关的函数有(平时只使用 Create 就可以了):

1
2
3
4
5
TObject.Create            { 构造函数 }
TObject.NewInstance       { 分配内存 }
TObject.InitInstance      { 初始化对象,设置接口表 }
TObject.InstanceSize      { 获取对象所需的内存大小 }
TObject.AfterConstruction { 对象创建完毕后要执行的过程,供用户覆盖使用 }

关于对象的销毁,调用 TObject.Free 以后,对象就没有了,就这么简单。Free 方法其实是调用了 Destroy 方法来销毁对象,Destroy 又调用了 ClassDestroy 函数来销毁对象(这个操作的执行代码是写在编译器里面的,所以我们看不到源程序),ClassDestroy 又调用 FreeInstance,FreeInstance 则先调用 CleanupInstance 释放对象的特殊类型变量,然后再释放对象所在的内存空间,然后,对象就没了。与销毁对象相关的函数有(平时使用 Free 就可以了,Destroy 主要用于被子类改写):

1
2
3
4
5
TObject.Free              { 判断对象是否为 nil 并调用 Destroy 销毁对象 }
TObject.Destroy           { 析构函数 }
TObject.FreeInstance      { 释放对象内存 }
TObject.CleanupInstance   { 释放为对象分配的特殊类型的变量空间 }
TObject.BeforeDestruction { 对象销毁之前要执行的过程,供用户覆盖使用 }

对象的识别:

1
2
3
4
5
6
7
TObject.ClassName         { 类方法  :获取类名称 }
TObject.ToString          { 对象方法:获取类名称 }
TObject.ClassNameIs       { 类方法  :判断类名称是否与指定的名称相同 }
TObject.ClassParent       { 类方法  :获取父类的类型 }
TObject.ClassType         { 对象方法:获取对象的类类型 }
TObject.InheritsFrom      { 类方法  :判断当前类是否继承自指定的类 }
TObject.Equals            { 对象方法:判断对象是否相等 }

取对象的相关信息:

1
2
3
4
5
6
7
8
9
10
TObject.ClassInfo         { 类方法  :返回指向类信息的指针 }
TObject.MethodAddress     { 类方法  :返回类的 published 的方法的地址 }
TObject.MethodName        { 类方法  :返回类的 published 的方法的名字 }
TObject.FieldAddress      { 对象方法:返回类的 published 的属性的地址 }
TObject.GetInterface      { 对象方法:检索一个指定了“GUID”或“接口名称”的接口 }
TObject.GetInterfaceEntry { 类方法  :获取指定的接口信息 }
TObject.GetInterfaceTable { 类方法  :获取接口表的地址 }
TObject.SafeCallException { 对象方法:处理 safecall 调用约定的方法使用的例外 }
TObject.UnitName          { 类方法  :获取类所在的单元的名称 }
TObject.GetHashCode       { 对象方法:获取对象的 HASH 值,实际实现为对象的指针 }

ClassInfo 返回的是一个 Pointer 类型的指针,要使用 ClassInfo 的返回值,需要引用 TypInfo 单元或 ObjAuto 单元,然后将 ClassInfo 的返回值转换成 PTypeInfo 类型,然后再调用相关函数获取“类”的详细信息。

关于对象的消息处理(Dispatch),还是看李战老师的《Delphi 的原子世界 - 第五节》吧,讲的很好,我这里只写一个简单的测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
unit Form1Unit;
 
interface
 
uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics,
  Controls, Dialogs, Forms, StdCtrls;
 
type
  TForm1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;
 
const
  { 我们自定义的消息 }
  UM_Text1 = WM_USER + 1;
 
type
 
  { 我们定义的消息结构,用它来存放消息以便在各个对象之间传递 }
  TTextMsg = record
    Msg: Cardinal;
    Text: String;
  end;
 
  { 自定义类,用来测试消息处理 }
  TMyObject = class(TObject)
  private
    { 用于处理 UM_Text1 消息的方法 }
    procedure WMTest1(var Msg: TTextMsg); message UM_Text1;
  public
    { 默认消息处理方法 }
    procedure DefaultHandler(var Msg); override;
  end;
 
var
  Form1: TForm1;
 
implementation
 
{$R *.dfm}
 
{ 收到消息后该怎么办,我们这里仅做简单显示,并反馈 }
procedure TMyObject.WMTest1(var Msg: TTextMsg);
begin
  ShowMessage('TMyObject 的对象收到消息:' + Msg.Text);
  Msg.Text := '消息已经收到,谢谢!' { 通过 Msg.Text 反馈消息 }
end;
 
{ 默认消息处理函数,消息可以是任意类型 }
procedure TMyObject.DefaultHandler(var Msg);
begin
  { 由于不知道接收到的消息长什么样子,所以将消息当作整数处理 }
  ShowMessage('这个消息没人处理:' + IntToStr(Integer(Msg)));
end;
 
{ 通过按钮向对象发送消息 }
procedure TForm1.Button1Click(Sender: TObject);
var
  MyObj: TMyObject; { 声明对象,用来接收消息 }
  Msg: TTextMsg; { 声明消息,用来传递 }
  I: Integer;
  S: String;
begin
 
  MyObj := TMyObject.Create;
  try
    Msg.Msg := UM_Text1; { 填写消息类型 }
    Msg.Text := '注意保重身体!'; { 填写消息内容 }
    MyObj.Dispatch(Msg); { 发送 UM_Text1 消息,让 MyObj 来处理 }
    ShowMessage('Button1 收到对方的反馈:' + Msg.Text);
 
    Msg.Msg := 99999;
    Msg.Text := 'Ping...';
    MyObj.Dispatch(Msg); { 乱发消息,让 MyObj 来处理 }
 
    I := 0;
    MyObj.Dispatch(I); { 乱发消息,让 MyObj 来处理 }
 
    S := 'ABC';
    MyObj.Dispatch(S); { 乱发消息,让 MyObj 来处理 }
  finally
    MyObj.Free;
  end;
end;
 
end.

总结一下:TObject 实现了对象的创建和销毁,使对象可以被正确识别,提供了丰富的运行时类型信息(RTTI),实现了对象的消息分派机制。

原文地址:https://www.cnblogs.com/AirLoveHardware/p/2957758.html