?? 天方夜譚vcl:開門.htm
字號:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<!-- saved from url=(0047)http://www.c-view.org/journal/003/vcl_chong.htm -->
<HTML><HEAD><TITLE>天方夜譚VCL:開門</TITLE>
<META content="text/html; charset=gb2312" http-equiv=Content-Type><LINK
href="天方夜譚VCL:開門.files/style.css" rel=stylesheet type=text/css>
<META content="MSHTML 5.00.3315.2870" name=GENERATOR></HEAD>
<BODY>
<SCRIPT src="天方夜譚VCL:開門.files/header.js"></SCRIPT>
<CENTER>
<H3><A href="http://www.c-view.org/tech/framework/vcl_chong.htm">天方夜譚VCL</A>:
開門</H3>蟲蟲<BR></CENTER>
<H4>前言</H4>
<P align=right>如果你愛他,讓他學VCL,因為那是天堂。<BR>如果你恨他,讓他學VCL,因為那是地獄。<BR>──《天方夜譚VCL》
<P>傳說很久很久以前,中國和印度之間有個島。那里的國王每天娶一個女子,過夜后就殺,鬧得雞犬不寧,最后宰相的女兒自愿嫁入宮。第一晚,她講了一個非常有意思的故事,國王聽入了迷,第二天沒有殺她。此后她每晚講一個奇特的故事,一直講到第一千零一夜,國王終于幡然悔悟。這就是著名的《一千零一夜》,也就是《天方夜譚》。印度和中國陸地接壤,那么相信傳說中所指的島,必然是在南中國海-馬六甲海峽-印度洋某個地方。現在我也算是在這其間的一個海島上,正值夜晚,也就借借“天方夜譚”的大名吧。
<P>初中我最喜歡的編程環境是Turbo C 2.0,高一開始用Visual
Basic。后來用了沒多久就發現,如果想做一個稍微復雜的東西,就需要不停地查資料來調用API,得在最前面作一個長得可怕的API函數聲明。于是我開始懷念簡潔的C語言。有位喜歡用Delphi的師哥,知道我極為憤恨Pascal,把我引向C++
Builder。即使對于C++中的繼承、多態這些簡單概念都還是一知半解,我居然也開始用VCL編一些莫名其妙的小程序(VCL上手倒真容易),開始熟悉VCL的結構,同時也了解了MFC和SDK,補習C++的基礎知識。后來我才覺得,VCL易學易用根本是個謊言。其實VCL相當難學,甚至比MFC更麻煩。
<P>不知道為什么,C++ Builder的資料出奇地少,也許正是這個原因,C++
Builder論壇上的人情味也特別濃。不管是我初學VCL時常問些莫名其妙白癡問題的天極論壇,還是現在我經常駐足的CSDN,C++
Builder論壇給人的感覺總是很溫馨。每次C++ Builder都比同等版本Delphi晚出,每次用C++還不得不看Object
Pascal的臉色,我想這是很多人心里的感受。CLX已經出現在Delphi6中,C++
Builder6的發布似乎還遙遙無期。CLX會代替VCL嗎?看來似乎不會,后面還會提到。我也看過不少要號召把VCL用C++改寫的帖子,往往雷聲大雨點小。看看別人老外,說干就干,一個FreeCLX項目就這么啟動了。
<P>用MFC的人比用VCL的運氣好,他們有Microsoft的支持,有Inside Visual C++、Programming Windows 95
with MFC、MFC Internals這些天王巨星的英文名著和中文翻譯,也有諸如侯捷先生的《深入淺出MFC》(即Dissecting
MFC)這些出色的中文原創作品。使用Delphi的人也遠比使用C++ Builder的命好,關于Delphi的精彩資料遠遠比C++
Builder多,很無奈,真的很無奈。
<P>C++
View雜志的主編向我約稿,我很為難,因為時間和技術水平都成問題。借用侯捷先生一句話,要拒絕和你住在同一個大腦同一個軀殼的人日日夜夜旦旦夕夕的請求,是很困難的。于是我下決心,寫一系列分析VCL內部原理的文章。所謂“天方夜譚”,當然對初學者不會有立桿見影的幫助,甚至于會讓您覺得“無聊”。這些文章面向的朋友應該比較熟悉VCL,有一定C++的基礎(當然會Object
Pascal和匯編更好),比如希望知道VCL底層運作機制的朋友,和希望自己開發應用框架或者想用C++重寫VCL的朋友。同時我更希望大家交流一下解剖應用框架的經驗,讓我們不局限于VCL或者MFC,能站在更高的角度看問題,共同提高自己的能力。
<P>在深入探討VCL之前,先得把VCL主要的性質說一下。
<UL>
<LI>同SmallTalk和Java所帶的框架一樣,VCL是Object
Pascal的一部分,也就是說語言和框架之間沒有明確的界限。比如Java帶有JDK,任寫一個類都是java.lang.Object的子類。VCL和Object
Pascal是同樣的道理。當然,Object Pascal為了兼容以前的Pascal,依然允許某個類沒有任何父類,但本系列文章將不再考慮這種情形。
<LI>同大多數框架一樣,VCL采取的是單根結構。也就是說,VCL的結構是以一棵TObject為根的繼承樹,除TObject外的所有VCL類都是TObject直接或間接的子類。
<LI>由于Object Pascal的語言特性,整個結構中只使用單繼承。 </LI></UL>
<P>所以,VCL的本質是一個Object Pascal類庫,提供了Object Pascal和C++兩個接口。在剖析的過程中,請時刻牢記這一點。
<P>文章的組織結構是就事論事,一次一個話題。由于VCL并不像MFC是一個獨立的框架,它與Object
Pascal、IDE、編譯器結合非常緊密,所以在剖析過程中不免會提到匯編。當然不會匯編的朋友也不用怕,我會把匯編代碼都解釋清楚,并盡量用C++改寫。
<P>文中有很多圖是表示類的內存結構,如圖所示。其中方框表示一個變量,兩端伸出表示還有若干個變量,橢圓標注是說明虛線圓圈中的整個對象(在后面虛線圓圈不會畫出)。
<P align=center><IMG align=top src="天方夜譚VCL:開門.files/vcl01.gif"><BR>圖1 圖例
<P>文中的程序,如非特別說明,均可以在Console Application模式下(如果使用了VCL類則需要復選“Use VCL”)編譯通過。
<H4>開門</H4>
<P>倒霉者如愚公,開門就見太行、王屋山。在一怒之下他開始移山,最后幸虧天神幫忙搬走了。中國人不喜歡開門見山的性格可能就是愚公傳下來的,說話做事老愛繞彎。當然我也不能免俗,前面廢話了一大堆,現在接著來。
<P>提起RTTI(runtime type
identification,運行時間類型辨別),相信大家都很熟悉。C++的RTTI功能相當有限,主要由typeid和dynamic_cast提供[<A
href="http://www.c-view.org/journal/003/vcl_chong.htm#11"
name=1>1</A>]。至于這兩者的實現方式[<A
href="http://www.c-view.org/journal/003/vcl_chong.htm#22"
name=2>2</A>],不是我們今天的話題,我們所關注的,乃是VCL所提供的“高級”RTTI的底層機制。
<P>熟悉框架的朋友都知道,框架往往會提供“高級”的RTTI功能。我曾看過一個論調,說Java和Object
Pascal比C++好,原因是因為它們的RTTI更“高級”。且不論濫用RTTI極為有害,事實上,C++用宏(macro)亦可以模擬出相同功能的RTTI[<A
href="http://www.c-view.org/journal/003/vcl_chong.htm#33" name=3>3</A>]。
<P>不過對于VCL類來說,您清楚其RTTI機制的運作情況嗎?對于如下 <PRE>class A: public TObject
{
...
}
...
A* p = new A;
</PRE>為什么p->ClassName();就能返回類A的名字“A”呢?
<BR>為什么A::ClassName(p->ClassParent())就可以返回A的基類名“TObject”呢? <BR>為什么……?
<P>其實這都是編譯器暗箱操作的結果。說白了,編譯器先在某個地方把類名寫好,到時候去取出來就行。關鍵在于,如何去取出來呢?顯然有指針指向這些數據,那么這些指針放在什么地方呢?
<P>記得《阿里巴巴和四十大盜》的故事吧?寶藏是早就存在的,如果知道口訣“芝麻,開門吧”,就可以拿到寶藏。同樣,類的相關信息是編譯器幫我們寫好了的,我們所關心的,就是如何獲取這些信息的“口訣”。
<P>不過這一切,要從虛函數開始,我們得先復習一下C/C++的對象模型。
<H5>虛擬函數表VFT</H5>
<P>C語言提供了基于對象(Object-Based)的思維模型,其對象模型非常清晰。比如
<P>
<TABLE>
<TBODY>
<TR>
<TD><PRE>struct A
{
int i;
char c;
};
</PRE></TD>
<TD width=100></TD>
<TD align=middle><IMG src="天方夜譚VCL:開門.files/vcl02.gif"><BR><BR>圖 2 結構的內存布局
</TD></TR></TBODY></TABLE><BR>在32位系統上,變量i占用4個字節,變量c占用1個字節。編譯器可能還會在后面添加3個字節補齊。那么,sizeof(A)就是8。
<P>C++提供了面向對象(Object-Oriented)的思維模型,其對象模型建立在C的基礎上。對于沒有虛函數的類,其模型與C中的結構(struct)完全一樣。但如果存在虛函數,一般在類實體的某個部分會存在一個指針vptr,指向虛擬函數表VFT(Virtual
Function Table)的入口。顯然,對于同一個類的所有對象,這個vptr都是相同的。例如 <PRE>class A
{
private:
int i;
char c;
public:
virtual void f1();
virtual void f2();
};
class B: public A
{
public:
virtual void f1();
virtual void f2();
};
</PRE>當我們作如下調用的時候 <PRE>A* p;
...
p->f2();
</PRE>程序本身并不知道它會調用A::f還是B::f或是其它函數,只是通過類實體中的vptr,查到VFT的入口,再在入口中查詢函數地址,進行調用。由于Borland
C++編譯器把vptr放在類實體的頭部,因此下面均有此假設。
<P>為了更充分地說明問題,我們從匯編級來分析一下。假設我們采用的是Borland C++編譯器。 <PRE>p->f2();
</PRE>這句的匯編代碼是 <PRE>mov eax,[ebp-0x04]
push eax
mov edx,[eax]
<B>call dword ptr [edx+0x04]</B>
pop ecx
</PRE>
<P align=center><IMG src="天方夜譚VCL:開門.files/vcl03.gif"><BR><BR>圖3 C++類實體的內存布局
<P>第一句ebp-0x04是指針變量p的地址,第一句是把p所指向的對象的地址傳送到eax; <BR>第二句不用管它;
<BR>第三句是把對象頭部的指針vptr傳到edx,即已取得VFT的入口;
<BR>第四句是關鍵,edx再加4(32位系統上一個指針占4個字節),也就是調用了從VFT入口算起的第二個函數指針,即B::f2; <BR>第五句不用管它。
<P>相信大家對VFT和C++的對象模型有一個更深刻的認識吧?對于VFT的實現,各個編譯器是不一樣的。有興趣的朋友不妨可以自行探索一下Microsft
Visual C++和GCC的實現方法,比較一下它們的異同。
<P>知道了VFT的結構,那么想想下面這個程序的結果是什么。 <PRE>#include <IOSTREAM>
using namespace std;
class A
{
int c;
virtual void f();
public:
A(int v = 0) { c = v;}
};
void main()
{
A a, b(20);
cout << *(void**)&a << endl;
cout << *(void**)&b << endl;
}
</PRE>我想您應該能理解其中*(void**)&a吧?這是取得vptr的值,也就是a所在內存空間的前4個字節,一個指針。下面我們還會使用類似的語句。
<P>無庸質疑,結果是輸出兩個完全相同的值。前面我們已經說過,對于同一個類的所有對象,其vptr值都是相同的。
<P>那么這個VFT到底有什么作用呢?現在看來,似乎就是儲存虛函數的地址。
<H5>虛擬方法表VMT</H5>
<P>如何通過類的實體來找到類的相關RTTI信息呢?顯然,VFT是同一個類的所有實體共享的數據,而RTTI正好也是。那么,把RTTI放在VFT里,就是個不錯的選擇。
<P>往哪兒放呢?VFT從入口開始往后是各個虛函數的指針,那么RTTI只能放在兩個地方:入口以前或者所有虛函數指針之后。顯然,放在入口以前更好,至少我們不用關心虛函數的多少,RTTI的位置也可以相對確定。
<P>VCL就采用了這個辦法來放置RTTI,不過把VFT換了名字,叫虛擬方法表VMT(Virtual Method
Table)。VMT的結構是怎樣的呢?Borland所提供的幫助文件里沒有任何相關資料,不過我們在Include\Vcl\system.hpp中就能找到如下蛛絲馬跡。
<PRE>static const Shortint vmtSelfPtr = 0xffffffb4;
static const Shortint vmtIntfTable = 0xffffffb8;
static const Shortint vmtAutoTable = 0xffffffbc;
static const Shortint vmtInitTable = 0xffffffc0;
static const Shortint vmtTypeInfo = 0xffffffc4;
static const Shortint vmtFieldTable = 0xffffffc8;
static const Shortint vmtMethodTable = 0xffffffcc;
static const Shortint vmtDynamicTable = 0xffffffd0;
static const Shortint vmtClassName = 0xffffffd4;
static const Shortint vmtInstanceSize = 0xffffffd8;
static const Shortint vmtParent = 0xffffffdc;
static const Shortint vmtSafeCallException = 0xffffffe0;
static const Shortint vmtAfterConstruction = 0xffffffe4;
static const Shortint vmtBeforeDestruction = 0xffffffe8;
static const Shortint vmtDispatch = 0xffffffec;
static const Shortint vmtDefaultHandler = 0xfffffff0;
static const Shortint vmtNewInstance = 0xfffffff4;
static const Shortint vmtFreeInstance = 0xfffffff8;
static const Shortint vmtDestroy = 0xfffffffc;
</PRE>注意這些常數值中的負數采用的是補碼表示法。求一個負數的補碼,先寫出相應正數的補碼表示,再按位求反,最后(在最低位)加1即可。對于求32位負數的補碼,也可以用它本身減去0xffffffff再減1即可。以0xfffffffc為例,0xfffffffc
– 0xffffffff – 1 = –
0x04,這就是結果。我們還可以從Borland提供的原始碼Source\Vcl\system.pas獲得,其中就是用負數表示。
<P>看著這份表格,從這些變量名中,我們已經猜到了其大概的分布情況。這些數字之間的間隔都是[<A
href="http://www.c-view.org/journal/003/vcl_chong.htm#44"
name=4>4</A>],可以猜想這些都是指針:函數指針或者數據指針。從這些常數的名字我們就可以知道它們的作用,比如vmtClassName自然就是儲存類名的指針。入口0以前,就是VCL對象的關鍵數據。無疑,它們蘊涵了TObject乃至VCL對象關鍵的秘密,也就是VMT的分布結構。
<P>這以上只是我們的推測,我們還應該驗證一下。我們知道的事實是,每一個對象必然都包含了其所屬類的相關信息。比如任何一個C++類的實體,都包含一個指向虛擬函數表VFT的指針。VCL類的實體必然也包含一個指向虛擬方法表VMT的指針。
<PRE>#include <VCL.H>
#include <IOSTREAM>
using namespace std;
class A: public TObject
{
int x;
virtual void f1() {}
virtual void f2() {}
public:
A(int v = 0): x(v) {}
};
void main()
{
A* p = new A;, * q = new A(100);
void* a = *(void**)p, * b = *(void**)q;
void* c = p->ClassType(), * d = q->ClassType();
cout << a << ' ' << b << endl;
cout << c << ' ' << d << endl;
cout << __classid(A) << endl;
delete p;
delete q;
}
</PRE>結果很有意思,輸出的五個指針地址完全一樣!a和b相同,從前面的例子我們就可以知道。然而TObject的ClassType方法和__classid操作符的返回值也跟這兩者相同,這就有點意思了。查查幫助就可以知道,__classid是C++
Builder中新增的擴展關鍵字,返回類的VMT的入口地址;而TObject的ClassType方法則是返回對象的類信息,返回類型是TClass(也就是TMetaClass*)。這說明,每個VCL類實體的頭部包含的指針,就是指向VMT的入口地址。而這個位置,也就是TObject的成員函數ClassType的返回值,亦即運算符__classid返回的類A的信息,只不過這個返回值是以TClass(即TMetaClass*)的形式存在。
<P align=center><IMG src="天方夜譚VCL:開門.files/vcl04.gif"><BR><BR>圖4 VCL類的VMT入口
<P>我們已經知道了VMT的結構,現在又找到了其入口,此時的興奮不亞于阿里巴巴知道“芝麻,開門吧”這句咒語時的感受。既然知道了開門的咒語,還不趕快進去拿寶藏?
<H5>牛刀小試</H5>
<P>乘著東風,我們來模擬一下VCL簡單的RTTI功能。為方便起見,我們仿造TObject,寫一個類FObject(呵呵,如果把TObject看成True
Object,我們的FObject就是False
Object)。要問下面這段代碼從哪里來?大部分都Copy&Paste自Include\Vcl\systobj.h文件。 <PRE>class FObject
{
public:
FObject(); /* Body provided by VCL {} */
Free();
TClass ClassType();
void CleanupInstance();
void * FieldAddress(const ShortString &Name);
/* class method */
static TObject * InitInstance(TClass cls, void *instance);
static ShortString ClassName(TClass cls);
static bool ClassNameIs(TClass cls, const AnsiString string);
static TClass ClassParent(TClass cls);
static void * ClassInfo(TClass cls);
static long InstanceSize(TClass cls);
static bool InheritsFrom(TClass cls, TClass aClass);
static void * MethodAddress(TClass cls, const ShortString &Name);
static ShortString MethodName(TClass cls, void *Address);
/* Hack: GetInterface is an untyped out object parameter and
* so is mangled as a void*. In practice, however, it is
?? 快捷鍵說明
復制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號
Ctrl + =
減小字號
Ctrl + -