天方夜谭VCL:开门
天方夜谭VCL: 开门
虫虫前言
如果你爱他,让他学VCL,因为那是天堂。
如果你恨他,让他学VCL,因为那是地狱。
──《天方夜谭VCL》传说很久很久以前,中国和印度之间有个岛。那里的国王每天娶一个女子,过夜后就杀,闹得鸡犬不宁,最后宰相的女儿自愿嫁入宫。第一晚,她讲了一个非常有意思的故事,国王听入了迷,第二天没有杀她。此后她每晚讲一个奇特的故事,一直讲到第一千零一夜,国王终于幡然悔悟。这就是著名的《一千零一夜》,也就是《天方夜谭》。印度和中国陆地接壤,那么相信传说中所指的岛,必然是在南中国海-马六甲海峡-印度洋某个地方。现在我也算是在这其间的一个海岛上,正值夜晚,也就借借“天方夜谭”的大名吧。
初中我最喜欢的编程环境是Turbo C 2.0,高一开始用Visual Basic。后来用了没多久就发现,如果想做一个稍微复杂的东西,就需要不停地查资料来调用API,得在最前面作一个长得可怕的API函数声明。于是我开始怀念简洁的C语言。有位喜欢用Delphi的师哥,知道我极为愤恨Pascal,把我引向C++ Builder。即使对于C++中的继承、多态这些简单概念都还是一知半解,我居然也开始用VCL编一些莫名其妙的小程序(VCL上手倒真容易),开始熟悉VCL的结构,同时也了解了MFC和SDK,补习C++的基础知识。后来我才觉得,VCL易学易用根本是个谎言。其实VCL相当难学,甚至比MFC更麻烦。
不知道为什么,C++ Builder的资料出奇地少,也许正是这个原因,C++ Builder论坛上的人情味也特别浓。不管是我初学VCL时常问些莫名其妙易做图问题的天极论坛,还是现在我经常驻足的CSDN,C++ Builder论坛给人的感觉总是很温馨。每次C++ Builder都比同等版本Delphi晚出,每次用C++还不得不看Object Pascal的脸色,我想这是很多人心里的感受。CLX已经出现在Delphi6中,C++ Builder6的发布似乎还遥遥无期。CLX会代替VCL吗?看来似乎不会,后面还会提到。我也看过不少要号召把VCL用C++改写的帖子,往往雷声大雨点小。看看别人老外,说干就干,一个FreeCLX项目就这么启动了。
用MFC的人比用VCL的运气好,他们有Microsoft的支持,有Inside Visual C++、Programming Windows 95 with MFC、MFC Internals这些天王巨星的英文名著和中文翻译,也有诸如侯捷先生的《深入浅出MFC》(即Dissecting MFC)这些出色的中文原创作品。使用Delphi的人也远比使用C++ Builder的命好,关于Delphi的精彩资料远远比C++ Builder多,很无奈,真的很无奈。
C++ View杂志的主编向我约稿,我很为难,因为时间和技术水平都成问题。借用侯捷先生一句话,要拒绝和你住在同一个大脑同一个躯壳的人日日夜夜旦旦夕夕的请求,是很困难的。于是我下决心,写一系列分析VCL内部原理的文章。所谓“天方夜谭”,当然对初学者不会有立杆见影的帮助,甚至于会让您觉得“无聊”。这些文章面向的朋友应该比较熟悉VCL,有一定C++的基础(当然会Object Pascal和汇编更好),比如希望知道VCL底层运作机制的朋友,和希望自己开发应用框架或者想用C++重写VCL的朋友。同时我更希望大家交流一下解剖应用框架的经验,让我们不局限于VCL或者MFC,能站在更高的角度看问题,共同提高自己的能力。
在深入探讨VCL之前,先得把VCL主要的性质说一下。
- 同SmallTalk和Java所带的框架一样,VCL是Object Pascal的一部分,也就是说语言和框架之间没有明确的界限。比如Java带有JDK,任写一个类都是java.lang.Object的子类。VCL和Object Pascal是同样的道理。当然,Object Pascal为了兼容以前的Pascal,依然允许某个类没有任何父类,但本系列文章将不再考虑这种情形。
- 同大多数框架一样,VCL采取的是单根结构。也就是说,VCL的结构是以一棵TObject为根的继承树,除TObject外的所有VCL类都是TObject直接或间接的子类。
- 由于Object Pascal的语言特性,整个结构中只使用单继承。
所以,VCL的本质是一个Object Pascal类库,提供了Object Pascal和C++两个接口。在剖析的过程中,请时刻牢记这一点。
文章的组织结构是就事论事,一次一个话题。由于VCL并不像MFC是一个独立的框架,它与Object Pascal、IDE、编译器结合非常紧密,所以在剖析过程中不免会提到汇编。当然不会汇编的朋友也不用怕,我会把汇编代码都解释清楚,并尽量用C++改写。
文中有很多图是表示类的内存结构,如图所示。其中方框表示一个变量,两端伸出表示还有若干个变量,椭圆标注是说明虚线圆圈中的整个对象(在后面虚线圆圈不会画出)。
图1 图例
文中的程序,如非特别说明,均可以在Console Application模式下(如果使用了VCL类则需要复选“Use VCL”)编译通过。
开门
倒霉者如愚公,开门就见太行、王屋山。在一怒之下他开始移山,最后幸亏天神帮忙搬走了。中国人不喜欢开门见山的性格可能就是愚公传下来的,说话做事老爱绕弯。当然我也不能免俗,前面废话了一大堆,现在接着来。
提起RTTI(runtime type identification,运行时间类型辨别),相信大家都很熟悉。C++的RTTI功能相当有限,主要由typeid和dynamic_cast提供[1]。至于这两者的实现方式[2],不是我们今天的话题,我们所关注的,乃是VCL所提供的“高级”RTTI的底层机制。
熟悉框架的朋友都知道,框架往往会提供“高级”的RTTI功能。我曾看过一个论调,说Java和Object Pascal比C++好,原因是因为它们的RTTI更“高级”。且不论滥用RTTI极为有害,事实上,C++用宏(macro)亦可以模拟出相同功能的RTTI[3]。
不过对于VCL类来说,您清楚其RTTI机制的运作情况吗?对于如下
class A: public TObject { ... } ... A* p = new A;为什么p->ClassName();就能返回类A的名字“A”呢?
为什么A::ClassName(p->ClassParent())就可以返回A的基类名“TObject”呢?
为什么……?
其实这都是编译器暗箱操作的结果。说白了,编译器先在某个地方把类名写好,到时候去取出来就行。关键在于,如何去取出来呢?显然有指针指向这些数据,那么这些指针放在什么地方呢?
记得《阿里巴巴和四十大盗》的故事吧?宝藏是早就存在的,如果知道口诀“芝麻,开门吧”,就可以拿到宝藏。同样,类的相关信息是编译器帮我们写好了的,我们所关心的,就是如何获取这些信息的“口诀”。
不过这一切,要从虚函数开始,我们得先复习一下C/C++的对象模型。
虚拟函数表VFT
C语言提供了基于对象(Object-Based)的思维模型,其对象模型非常清晰。比如
struct A { int i; char c; }; |
图 2 结构的内存布局 |
在32位系统上,变量i占用4个字节,变量c占用1个字节。编译器可能还会在后面添加3个字节补齐。那么,sizeof(A)就是8。
C++提供了面向对象(Object-Oriented)的思维模型,其对象模型建立在C的基础上。对于没有虚函数的类,其模型与C中的结构(struct)完全一样。但如果存在虚函数,一般在类实体的某个部分会存在一个指针vptr,指向虚拟函数表VFT(Virtual Function Table)的入口。显然,对于同一个类的所有对象,这个vptr都是相同的。例如
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(); };当我们作如下调用的时候
A* p; ... p->f2();程序本身并不知道它会调用A::f还是B::f或是其它函数,只是通过类实体中的vptr,查到VFT的入口,再在入口中查询函数地址,进行调用。由于Borland C++编译器把vptr放在类实体的头部,因此下面均有此假设。
为了更充分地说明问题,我们从汇编级来分析一下。假设我们采用的是Borland C++编译器。
p->f2();这句的汇编代码是
mov eax,[ebp-0x04] push eax mov edx,[eax] call dword ptr [edx+0x04] pop ecx
图3 C++类实体的内存布局
第一句ebp-0x04是指针变量p的地址,第一句是把p所指向的对象的地址传送到eax;
第二句不用管它;
第三句是把对象头部的指针vptr传到edx,即已取得VFT的入口;
第四句是关键,edx再加4(32位系统上一个指针占4个字节),也就是调用了从VFT入口算起的第二个函数指针,即B::f2;
第五句不用管它。
相信大家对VFT和C++的对象模型有一个更深刻的认识吧?对于VFT的实现,各个编译器是不一样的。有兴趣的朋友不妨可以自行探索一下Microsft Visual C++和GCC的实现方法,比较一下它们的异同。
知道了VFT的结构,那么想想下面这个程序的结果是什么。
#include我想您应该能理解其中*(void**)&a吧?这是取得vptr的值,也就是a所在内存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; }
补充:软件开发 , Delphi ,