Delphi類和組件-TObject淺析


[摘要]Delphi 中的所有類都是從 TObject 繼承而來的,都具有 TObject 的所有特性,TObject 是所有類的根類,本文詳細介紹TObject。

Delphi 版本:

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),實現了對象的消息分派機制。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章