⭐ 欢迎来到虫虫下载站! | 📦 资源下载 📁 资源专辑 ℹ️ 关于我们
⭐ 虫虫下载站

📄 天方夜谭vcl:开门.htm

📁 csdn10年中间经典帖子
💻 HTM
📖 第 1 页 / 共 2 页
字号:
<!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-&gt;ClassName();就能返回类A的名字“A”呢? 
<BR>为什么A::ClassName(p-&gt;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-&gt;f2();
</PRE>程序本身并不知道它会调用A::f还是B::f或是其它函数,只是通过类实体中的vptr,查到VFT的入口,再在入口中查询函数地址,进行调用。由于Borland 
C++编译器把vptr放在类实体的头部,因此下面均有此假设。 
<P>为了更充分地说明问题,我们从汇编级来分析一下。假设我们采用的是Borland C++编译器。 <PRE>p-&gt;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 &lt;&lt; *(void**)&amp;a &lt;&lt; endl;
        cout &lt;&lt; *(void**)&amp;b &lt;&lt; endl;
}
</PRE>我想您应该能理解其中*(void**)&amp;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-&gt;ClassType(), * d = q-&gt;ClassType();
        cout &lt;&lt; a &lt;&lt; ' ' &lt;&lt; b &lt;&lt; endl;
        cout &lt;&lt; c &lt;&lt; ' ' &lt;&lt; d &lt;&lt; endl;
        cout &lt;&lt; __classid(A) &lt;&lt; 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&amp;Paste自Include\Vcl\systobj.h文件。 <PRE>class FObject
{
public:
        FObject(); /* Body provided by VCL {} */
        Free();
        TClass		ClassType();
        void		CleanupInstance();
        void *		FieldAddress(const ShortString &amp;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 &amp;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 + -