第二章 DELPHI的原子世界
在使用DELPHI开发软件的过程中,我们就像草原上一群快乐牛羊,无忧无虑地享受着Object Pascal语言为我们带来的阳光和各种VCL控件提供的丰富的水草。抬头望望无边无际蔚蓝的天空,低头品尝大地上茂密的青草,谁会去想宇宙有多大,比分子和原子更小的东西是什么?那是哲学家的事。而哲学家此时正坐在高高的山顶上,仰望宇宙星云变换,凝视地上小虫的爬行,蓦然回头,对我们这群吃草的牛羊点头微笑。随手扯起一根小草,轻轻地含在嘴里,闭上眼睛细细品尝,不知道这根青草在哲学家的嘴里是什么味道?只是,他的脸上一直带着满意的微笑。
认识和了解DELPHI微观的原子世界,可以使我们彻底理解DELPHI的宏观应用程序结构,从而在更广阔的思想空间中开发我们的软件。这就好像,牛顿发现了宏观物体的运动,却因为搞不清物体为什么会这样运动而苦恼,相反,爱因斯坦却在基本粒子规律和宏观物体运动之间体验着相对论的快乐生活!
第一节 TObject原子
TObject是什么?
是Object Pascal语言体系结构的基本核心,也是各种VCL 控件的起源。我们可以认为,TObject是构成DELPHI应用程序的原子之一,当然,他们又是由基本Pascal语法元素等更细微的粒子构成。
说TObject是DELPHI程序的原子,是因为TObject是DELPHI编译器内部支持的。所有的对象类都是从TObject派生的,即使你并未指定TObject为祖先类。TObject被定义在System单元,它是系统的一部分。在System.pas单元的开头,有这样的注释文本:
{ Predefined constants, types, procedures, }
{ and functions (such as True, Integer, or }
{ Writeln) do not have actual declarations.}
{ Instead they are built into the compiler }
{ and are treated as if they were declared }
{ at the beginning of the System unit. }
它的意思说,这一单元包含预定义的常量、类型、过程和函数(诸如:Ture、Integer或Writeln),它们并没有实际的声明,而是编译器内置的,并在编译的开始就被认为是已经声明的定义。你可以将Classes.pas或Windows.pas等其他源程序文件加入你的项目文件中进行编译和调试其源代码,但你绝对无法将System.pas源程序文件加入到你的项目文件中进行编译!DELPHI将报告重复定义System的编译错误! 因此,TObject是编译器内部提供的定义,对于我们使用DELPHI开发程序的人来说,TObject是原子性的东西。 TObject在System单元中的定义是这样的:
TObject = class
  constructor Create;
procedure Free;
class function InitInstance(Instance: Pointer): TObject;
procedure CleanupInstance;
function ClassType: TClass;
class function ClassName: ShortString;
class function ClassNameIs(const Name: string): Boolean;
class function ClassParent: TClass;
class function ClassInfo: Pointer;
class function InstanceSize: Longint;
class function InheritsFrom(AClass: TClass): Boolean;
class function MethodAddress(const Name: ShortString): Pointer;
class function MethodName(Address: Pointer): ShortString;
function FieldAddress(const Name: ShortString): Pointer;
function GetInterface(const IID: TGUID; out Obj): Boolean;
class function GetInterfaceEntry(const IID: TGUID): PInterfaceEntry;
class function GetInterfaceTable: PInterfaceTable;
function SafeCallException(ExceptObject: TObject; ExceptAddr: Pointer): HResult; virtual; procedure AfterConstruction; virtual;
procedure BeforeDestruction; virtual;
procedure Dispatch(var Message); virtual;
procedure DefaultHandler(var Message); virtual;
class function NewInstance: TObject; virtual;
procedure FreeInstance; virtual;
destructor Destroy; virtual;
end;

解决方案 »

  1.   

    下面,我们将逐步敲开TObject原子的大门,看看里面到底是什么结构。
    我们知道,TObject是所有对象的基本类,那么,一个对象到底是什么? DELPHI中的任何对象都是一个指针,这个指针指明该对象在内存中所占据的一块空间!虽然,对象是一个指针,可是我们引用对象的成员时却不用写成这样的代码MyObject^.GetName,而只能写成MyObject.GetName,这是Object Pascal语言扩充的语法,是由编译器支持的。使用C++ Builder的朋友就很清楚对象与指针的关系,因为在C++ Builder的对象都要定义为指针。对象指针指向的地方就是对象存储数据的对象空间,我们来分析一下对象指针指向的内存空间的数据结构。
    对象空间的头4个字节是指向该对象类的虚方法地址表(VMT - Vritual Method Table)。接下来的空间就是存储对象本身成员数据的空间,并按从该对象最原始祖先类的数据成员到该对象类的数据成员的总顺序,和每一级类中数据成员的定义顺序存储。
    类的虚方法地址表(VMT)保存从该类的原始祖先类派生到该类的所有类的虚方法的过程地址。类的虚方法,就是用保留字vritual声明的方法,虚方法是实现对象多态性的基本机制。虽然,用保留字dynamic声明的动态方法也可实现对象的多态性,但这样的方法不保存在虚方法地址表(VMT)中,它只是Object Pascal提供的另一种可节约类存储空间的多态实现机制,但却是以牺牲调用速度为代价的。
    即使,我们自己并未定义任何类的虚方法,但该类的对象仍然存在指向虚方法地址表的指针,只是地址项的长度为零。可是,在TObject中定义的那些虚方法,如Destroy、FreeInstance等等,又存储在什么地方呢?原来,他们的方法地址存储在相对VMT指针负方向偏移的空间中。其实,在VMT表的负方向偏移76个字节的数据空间是对象类的系统数据结构,这些数据结构是与编译器相关的,并且在将来的DELPHI版本中有可能被改变。
    因此,你可以认为,VMT是一个从负偏移地址空间开始的数据结构,负偏移数据区是VMT的系统数据区,VMT的正偏移数据是用户数据区(自定义的虚方法地址表)。TObject中定义的有关类信息或对象运行时刻信息的函数和过程,一般都与VMT的系统数据有关。
    一个VMT数据就代表一个类,其实VMT就是类!在Object Pascal中我们用TObject、TComponent等等标识符表示类,它们在DELPHI的内部实现为各自的VMT数据。而用class of保留字定义的类的类型,实际就是指向相关VMT数据的指针。
    对我们的应用程序来说,VMT数据是静态的数据,当编译器编译完成我们的应用程序之后,这些数据信息已经确定并已初始化。我们编写的程序语句可访问VMT相关的信息,获得诸如对象的尺寸、类名或运行时刻的属性资料等等信息,或者调用虚方法或读取方法的名称与地址等等操作。
    当一个对象产生时,系统会为该对象分配一块内存空间,并将该对象与相关的类联系起来,于是,在为对象分配的数据空间中的头4个字节,就成为指向类VMT数据的指针。
    我们再来看看对象是怎样诞生和灭亡的。看着我三岁的儿子在草地上活蹦乱跳,正是由于亲眼目睹过生命的诞生过程,我才能真真体会到生命的意义和伟大。也只有那些经历过死别的人,才会更加理解和珍惜生命。那么,就让我们理解一下对象的产生和消亡的过程吧!
    我们都知道,用下面的语句可以构造一个最简单对象:
    AnObject := TObject.Create;
    编译器将其编译实现为:
    用TObject对应的VMT为依据,调用TObject的Create构造函数。而在Create构造函数调用了系统的ClassCreate过程,系统的ClassCreate过程又通过存储在类VMT调用NewInstance虚方法。调用NewInstance方法的目的是要建立对象的实例空间,因为我们没有重载该方法,所以,它就是TObject类的NewInstance。TObjec类的NewInstance方法将根据编译器在VMT表中初始化的对象实例尺寸(InstanceSize),调用GetMem过程为该对象分配内存,然后调用InitInstance方法将分配的空间初始化。InitInstance方法首先将对象空间的头4个字节初始化为指向对象类对应VMT的指针,然后将其余的空间清零。建立对象实例之后,还调用了一个虚方法AfterConstruction。最后,将对象实例数据的地址指针保存到AnObject变量中,这样,AnObject对象就诞生了。
    同样,用下面的语句可以消灭一个对象:
    AnObject.Destroy;
    TObject的析构函数Destroy被声明为虚方法,它也是系统固有的虚方法之一。Destory方法首先调用了BeforeDestruction虚方法,然后调用系统的ClassDestroy过程。ClassDestory过程又通过类VMT调用FreeInstance虚方法,由FreeInstance方法调用FreeMem过程释放对象的内存空间。就这样,一个对象就在系统中消失。
    对象的析构过程比对象的构造过程简单,就好像生命的诞生是一个漫长的孕育过程,而死亡却相对的短暂,这似乎是一种必然的规律。
    在对象的构造和析构过程中,调用了NewInstance和FreeInstance两个虚函数,来创建和释放对象实例的内存空间。之所以将这两个函数声明为虚函数,是为了能让用户在编写需要用户自己管理内存的特殊对象类时(如在一些特殊的工业控制程序中),有扩展的空间。
    而将AfterConstruction和BeforeDestruction声明为虚函数,也是为了将来派生的类在产生对象之后,有机会让新诞生的对象呼吸第一口新鲜空气,而在对象消亡之前可以允许对象完成善后事宜,这都是合情合理的事。其实,TForm对象和TDataModule对象的OnCreate事件和OnDestroy事件,就是在TForm和TDataModule重载的这两个虚函数过程分别触发的。
    此外,TObjec还提供了一个Free方法,它不是虚方法,它是为了那些搞不清对象是否为空(nil)的情况下能安全释放对象而专门提供的。其实,搞不清对象是否为空,本身就有程序逻辑不清晰的问题。不过,任何人都不是完美的,都可能犯错,使用Free能避免偶然的错误也是件好事。然而,编写正确的程序不能一味依靠这样的解决方法,还是应该以保证程序的逻辑正确性为编程的第一目标!
      

  2.   

    有兴趣的朋友可以读一读System单元的原代码,其中,大量的代码是用汇编语言书写的。细心的朋友可以发现,TObject的构造函数Create和析构函数Destory竟然没有写任何代码,其实,在调试状态下通过Debug的CPU窗口,可清楚地反映出Create和Destory的汇编代码。因为,缔造DELPHI的大师门不想将过多复杂的东西提供给用户,他们希望用户在简单的概念上编写应用程序,将复杂的工作隐藏在系统的内部由他们承担。所以,在发布System.pas单元时特别将这两个函数的代码去掉,让用户认为TObject是万物之源,用户派生的类完全从虚无中开始,这本身并没有错。虽然,阅读DELPHI的这些最本质的代码需要少量的汇编语言知识,但阅读这样的代码,可以让我们更深刻认识DELPHI世界的起源和发展的基本规律。即使看不太懂,能起码了解一些基本东西,对我们编写DELPHI程序也是大有帮助。
    第二节 TClass原子
    在System.pas单元中,TClass是这样定义的:
    TClass = class of TObject;
    它的意思是说,TClass是TObject的类。因为TObject本身就是一个类,所以TClass就是所谓的类的类。
    从概念上说,TClass是类的类型,即,类之类。但是,我们知道DELPHI的一个类,代表着一项VMT数据。因此,类之类可以认为是为VMT数据项定义的类型,其实,它就是一个指向VMT数据的指针类型!
    在以前传统的C++语言中,是不能定义类的类型的。对象一旦编译就固定下来,类的结构信息已经转化为绝对的机器代码,在内存中将不存在完整的类信息。一些较高级的面向对象语言才可支持对类信息的动态访问和调用,但往往需要一套复杂的内部解释机制和较多的系统资源。而DELPHI的Object Pascal语言吸收了一些高级面向对象语言的优秀特征,又保留可将程序直接编译成机器代码的传统优点,比较完美地解决了高级功能与程序效率的问题。
    正是由于DELPHI在应用程序中保留了完整的类信息,才能提供诸如as和is等在运行时刻转换和判别类的高级面向对象功能,而类的VMT数据在其中起了关键性的核心作用。有兴趣的朋友可以读一读System单元的AsClass和IsClass两个汇编过程,他们是as和is操作符的实现代码,以加深对类和VMT数据的理解。 有了类的类型,就可以将类作为变量来使用。可以将类的变量理解为一种特殊的对象,你可以象访问对象那样访问类变量的方法。例如:我们来看看下面的程序片段:
    type
      TSampleClass = class of TSampleObject;
    TSampleObject = class( TObject )
      public constructor Create;
    destructor Destroy; override;
    class function GetSampleObjectCount: Integer;
    procedure GetObjectIndex: Integer;
    end;
    var
      aSampleClass : TSampleClass;
    aClass : TClass;
    在这段代码中,我们定义了一个类TSampleObject及其相关的类类型TSampleClass,还包括两个类变量aSampleClass和aClass。此外,我们还为TSampleObject类定义了构造函数、析构函数、一个类方法GetSampleObjectCount和一个对象方法GetObjectIndex。
    首先,我们来理解一下类变量aSampleClass和aClass的含义。
    显然,你可以将TSampleObject和TObject当作常量值,并可将它们赋值给aClass变量,就好象将123常量值赋值给整数变量i一样。所以,类类型、类和类变量的关系就是类型、常量和变量的关系,只不过是在类的这个层次上而不是对象层次上的关系。当然,直接将TObject赋值给aSampleClass是不合法的,因为aSampleClass是TObject派生类TSampleObject的类变量,而TObject并不包含与TSampleClass类型兼容的所有定义。相反,将TSampleObject赋值给aClass变量却是合法的,因为TSampleObject是TObject的派生类,是和TClass类型兼容的。这与对象变量的赋值和类型匹配关系完全相似。
    然后,我们再来看看什么是类方法。
    所谓类方法,就是指在类的层次上调用的方法,如上面所定义的GetSampleObjectCount方法,它是用保留字class声明的方法。类方法是不同于在对象层次上调用的对象方法的,对象方法已经为我们所熟悉,而类方法总是在访问和控制所有类对象的共同特性和集中管理对象这一个层次上使用的。在TObject的定义中,我们可以发现大量的类方法,如ClassName、ClassInfo和NewInstance等等。其中,NewInstance还被定义为virtual的,即虚的类方法。这意味作你可以在派生的子类中重新编写NewInstance的实现方法,以便用特殊的方式构造该类的对象实例。
    在类方法中你也可使用self这一标识符,不过其所代表的含义与对象方法中的self是不同的。类方法中的self表示的是自身的类,即指向VMT的指针,而对象方法中的self表示的是对象本身,即指向对象数据空间的指针。虽然,类方法只能在类层次上使用,但你仍可通过一个对象去调用类方法。例如,可以通过语句aObject.ClassName调用对象TObject的类方法ClassName,因为对象指针所指向的对象数据空间中的头4个字节又是指向类VMT的指针。相反,你不可能在类层次上调用对象方法,象TObject.Free的语句一定是非法的。 值得注意的是,构造函数是类方法,而析构函数是对象方法!
    什么?构造函数是类方法,析构函数是对象方法!有没有搞错?
    你看看,当你创建对象时分明使用的是类似于下面的语句:
    aObject := TObject.Create;
    分明是调用类TObject的Create方法。而删除对象时却用的下面的语句:
    aObject.Destroy;
    即使使用Free方法释放对象,也是间接调用了对象的Destroy方法。
    原因很简单,在构造对象之前,对象还不存在,只存在类,创建对象只能用类方法。相反,删除对象一定是删除已经存在的对象,是对象被释放,而不是类被释放。
    最后,顺便讨论一下虚构造函数的问题。
    在传统的C++语言中,可以实现虚析构函数,但实现虚构造函数却是一个难题。因为,在传统的C++语言中,没有类的类型。全局对象的实例是在编译时就存在于全局数据空间中,函数的局部对象也是编译时就在堆栈空间中映射的实例,即使是动态创建的对象,也是用new操作符按固定的类结构在堆空间中分配的实例,而构造函数只是一个对已产生的对象实例进行初始化的对象方法而已。传统C++语言没有真正的类方法,即使可以定义所谓静态的基于类的方法,其最终也被实现为一种特殊的全局函数,更不用说虚拟的类方法,虚方法只能针对具体的对象实例有效。因此,传统的C++语言认为,在具体的对象实例产生之前,却要根据即将产生的对象构造对象本身,这是不可能的。的确不可能,因为这会在逻辑上产生自相矛盾的悖论!
    然而,正是由于在DELPHI中有动态的类的类型信息,有真正虚拟的类方法,以及构造函数是基于类实现的等等这些关键概念,才可实现虚拟的构造函数。对象是由类产生的,对象就好象成长中的婴儿,而类就是它的母亲,婴儿自己的确不知道自己将来会成为什么样的人,可是母亲们却用各自的教育方法培养出不同的人,道理是相通的。
    正是在TComponent类的定义中,构造函数Create被定义为虚拟的,才能使不同类型的控件实现各自的构造方法。这就是TClass创造的类之类概念的伟大,也是DELPHI的伟大。
      

  3.   

    第三章 WIN32的时空观
    我的老父亲看着地上玩玩具的小孙子,然后对我说:“这孩子和小时的你一样,喜欢把东西拆开,看过究竟才罢手”。想想我小时侯,经常将玩具车、小闹钟、音乐盒,等等,拆得一塌糊涂,常常被母亲训斥。
    我第一次理解计算机的基本原理,与我拆过的音乐盒有关。那是在念高中时的一本漫画书上,一位白胡子老头在讲解智能机的理论,一位留八字胡的叔叔在说计算机和音乐盒。他们说,计算机的中央处理器就是音乐盒中用来发音的那一排音乐簧片,计算机程序就是音乐盒中那个小圆筒上密布的凸点,小圆筒的转动相当于中央处理器的指令指针的自然移动,而小圆筒上代表音乐的凸点控制音乐簧片振动发音相当于中央处理器执行程序的指令。音乐盒发出美妙的旋律,是按工匠早已刻在小圆筒上的音乐谱演奏的,计算机完成复杂的处理,是根据程序员预先编制好的程序实现的。上大学之后,我才知道那个白胡子老头就是科学巨匠图灵,他的有限自动机理论推动了整个信息革命的发展,而那个留八字胡的叔叔就是计算机之父冯.诺依曼,冯氏计算机体系结构至今仍然是计算机的主要体系机构。音乐盒没白拆,母亲可以宽心。
    有深入浅出的理解,才能有高深而又简洁的创造。 这一章我们将讨论Windows的32位操作系统中与我们编程有关的基本概念,建立WIN32中正确的时空观。希望阅读完本章之后,我们能更加深入地理解程序、进程和线程,理解执行文件、动态连接库和运行包的原理,看清全局数据、局部数据和参数在内存中的真相。
    第一节 理解进程
    由于历史的原因,Windows是起源于DOS。而在DOS时代,我们一直只有程序的概念,而没有进程的概念。那时侯,只有操作系统的正规军,如UNIX和VMS等等,才有进程的概念,而且多进程就意味着小型机、终端和多用户,也意味着金钱。我绝大多数的时间只能使用相对廉价的微机和DOS系统,只是在学操作系统这门课程时才开始接触进程和小型机。
    在Windows 3.X之后,Microsoft才在图形界面的操作系统站住脚跟,而我也是在这时开始正式面对多任务和进程的概念。以前在DOS下,同一时间只能执行一个程序,而在Windows下同一时间可执行多个程序,这就是多任务。在DOS下运行一个程序的同时,不能执行相同的程序,而在Windows下,同一程序可以同时有两个以上的副本在运行,每一个运行的程序副本就是一个进程。更确切地说,任何程序的一次运行就产生一个任务,而每个任务就是一个进程。
    当将程序和进程放到一起理解时,可以认为程序一词说的是静态的东西,一个典型的程序是由一个EXE文件或一个EXE文件加上若干DLL文件组成的静态代码和数据。而进程是程序的一次运行,是在内存中动态运行的代码和动态变化的数据。当静态的程序要求运行时,操作系统将为本次运行提供一定的内存空间,把静态的程序代码和数据调入这些内存空间,将程序的代码和数据进行重定位映射之后,就在该空间内执行程序,这样就产生了动态的进程。
    同一个程序同时运行着的两个副本,意味着在系统内存中有两个进程空间,只不过它们的程序功能是一样的,但处于不同的动态变化的状态之中。
    从进程运行的时间上来说,各进程是同时执行的,专业术语称为并行执行或并发执行。但这主要是操作系统给我们的表面感觉,实际上各进程是分时执行的,也就是各进程轮流占用CPU的时间来执行进程的程序指令。对于一个CPU来说,同一时间只有一个进程的指令在执行。操作系统是调度进程运行的幕后操纵者,它不断保存和切换各进程在CPU中执行的当前状态,使得每一个被调度的进程都认为自己是完整和连续地运行着。由于进程分时调度的速度非常快,所以给我们的感觉就是进程都是同时运行的。其实,真正意义上的同时运行只有在多CPU的硬件环境中才有。稍后在讲述线程一节时,我们将发现,真正推动进程运转的是线程,进程更重要的是提供了进程空间。
    从进程占据的空间上来说,各进程空间是相对独立的,每一个进程在自己独立的空间中运行。一个程序既包括代码空间又包括数据空间,代码和数据都要占据进程空间。Windows为每一进程所需的数据空间分配实际的内存,而对代码空间一般都采用共享手段,将一个程序的一份代码映射给该程序的多个进程。这意味着,如果一个程序有100K的代码并需要100K的数据空间,也就是总共需要200K的进程空间,则第一次运行程序时操作系统将分配200K的进程空间,而运行程序的第二个进程时,操作系统只分配100K的数据空间,而代码空间则共享前一个进程的空间。
    上面所说的是Windows操作系统中进程的基本时空观,其实Windows的16位和32位操作系统在进程的时空观上有很大的差异。
    从时间上来说,16位的Windows操作系统,如Windows 3.x等,进程管理是非常简单的,它实际上只是一个多任务管理操作系统。而且,操作系统对任务的调度是被动的,如果一个任务不自己放弃对消息的处理,操作系统就必须等待。由于16位Windows系统在管理进程方面的缺陷,一个进程运行时,完全占有着CPU的资源。在那个年代,为了16位Windows可以有机会调度别的任务,微软公司大力赞扬开发Windows应用程序的开发者是心胸宽阔的程序员,以使得他们乐意多编写几行恩赐给操作系统的代码。相反,WIN32的操作系统,如Windows 95和NT等,才是具备了真正的多进程和多任务操作系统的能力。WIN32中的进程完全由操作系统调度,一旦进程运行的时间片结束,不管进程是否还在处理数据,操作系统将主动切换到下一进程。严格地说,16位的Windows操作系统不能算是完整的操作系统,而32位的WIN32操作系统才是真正意义上的操作系统。当然,微软公司不会说WIN32弥补了16位Windows的缺陷,而是宣称WIN32实现了一种称为“抢占式多任务”的先进技术,这是商业手段。
      

  4.   

    从空间上看,16位的Windows操作系统中的进程空间虽然相对独立,但进程之间可已很容易地互相访问对方的数据空间。因为,这些进程实际是在相同的物理空间中的不同的数据段而已,而且不当的地址操作很容易造成错误的空间读写,并使操作系统崩溃。然而,在WIN32操作系统中,各进程空间完全是独立的。WIN32为每一个进程提供一个可达4G的虚拟的,并且是连续的地址空间。所谓连续的地址空间,是指每一个进程都拥有从$00000000到$FFFFFFFF的地址空间,而不是向16位Windows的分段式空间。在WIN32中,你完全不必担心自己的读写操作会无意地影响到其他进程空间中的数据,也不用担心别的进程会来骚扰你的工作。同时,WIN32为你的进程提供的连续的4G虚拟空间,是操作系统在硬件的支持下将物理内存映射给你的,你虽然拥有如此广阔的虚拟空间,但系统决不会浪费一个字节的物理内存。
    第二节 进程空间
    在我们用DELPHI编写WIN32的应用程序时,很少去关心进程在运行时的内部世界。因为WIN32为我们的进程提供了4G的连续虚拟进程空间,可能目前世界上最庞大的应用程序也只用到了其中的部分空间。似乎进程空间是无限的,但4G的进程空间是虚拟的,而你机器的实际内存可能与此相差甚远。虽然,进程拥有如此广阔的空间,但有些复杂算法的程序还是会因为堆栈溢出而无法运行,特别是含有大量递归算法的程序。
    因此,深入地认识和了解这4G的进程空间的结构,以及它与物理内存的关系等等,将有助于我们更清楚地认识WIN32的时空世界,从而可在实际的开发工作中运用正确的世界观和方法论解决各种难题。
    下面,我们将通过简单的实验,来了解WIN32的进程空间的内部世界。这可能需要一些对CUP寄存器和汇编语言的知识,但我尽量用简单的语言来说明。
    当启动DELPHI时,将自动产生一个Project1的项目,我们就拿它开刀。在Project1.dpr原程序的任意位置设一断点,比如,就在begin一句处设一断点。然后运行程序,当程序运行到断点时会自动停下来。这时,我们就可以打开调试工具中的CPU窗口来观察进程空间的内部结构了。 当前的指令指针寄存器EIP是停在$0043E4B8,从程序指令所在地址的最高两位16进制数都是零,可以看出当前的程序处在4G进程空间相当底端的地址位置,其占据$00000000到$FFFFFFFF的相当少的地址空间。
    在CPU窗口中的指令框中,你可以向上查看进程空间中的内容。当查看小于$00400000的空间内容时,你会发现小于$00400000的内容出现一串串的问号“????”,那是因为该地址空间还未映射到实际物理空间的缘故。如果在这时,你查看一下全局变量HInstance的16进制值就会发现它也是$00400000。虽然HInstance反映的是进程实例的句柄,其实,它就是程序被加载到内存中的起始地址值,在16位Windows中也是如此。因此,我们可以认为进程的程序是从$00400000开始加载的,也就是从4G虚拟空间中的4M以后的空间开始是程序加载的空间。
    从$00400000往后,到$0044D000之前,主要是程序代码和全局数据的地址空间。在CPU窗口中的堆栈框中,可以查看到当前堆栈的地址。同样,你会发现当前堆栈的地址空间是从$0067B000到$00680000的,长度为$5000。其实,进程最小的堆栈空间大小就是$5000,它是根据编译DELPHI程序时在Project\Options中Linker页中设置的Min stack size值,加上$1000而得到的。堆栈是由高端地址向底端增长的,当程序运行的堆栈不够时,系统将自动向地端地址方向增加堆栈空间的大小,这一过程将把更多的实际内存映射到进程空间。可在编译DELPHI程序时,通过设置Project\Options中Linker页中Max stack size的值,控制可增加的最大堆栈空间。特别是在含有深层次的子程序调用关系或运用递归算法的程序中,一定要合理地设置Max stack size的值。因为,调用子程序是需要耗用堆栈空间,而堆栈耗尽之后,系统就会抛出“Stack overflow”的错误。
    似乎,从堆栈空间之后的进程空间就应该是自由的空间了吧。其实不然,WIN32的有关资料说,$80000000之后的2G空间是系统使用的空间。看来,进程能够真正拥有的只有2G空间。其实,进程能真正拥有的空间连2G都不够,因为从$00000000到$00400000的这4M空间也是禁区。
    但不管怎样,我们的进程可以使用的地址还是非常广阔的。特别是堆栈空间之后到$80000000之间,是进程空间的主战场。进程从系统分配的内存空间将被映射到这块空间,进程加载的动态连接库将被映射到这块空间,新建线程的线程堆栈空间也将映射到这块空间,几乎所有涉及分配内存的操作都将映射到这块空间。请注意,这里所说的映射,意味着实际内存与这块虚拟空间的对应,没有映射为实际内存的进程空间是无法使用的,就象调试时CPU窗口指令框中的那一串串的“????”。
    第三节 EXE和DLL
    我们已经对进程和进程空间有了一定的了解,下面我们将要讨论执行文件EXE和动态连接库DLL的区别,它们与进程空间的关系。
    典型的Windows应用程序一般都由一个EXE文件和若干个DLL文件组成,Windows操作系统本身就是这种结构,这些都是大家熟知的内容。但是,你真正理解了EXE和DLL吗?如果,你确信自己已经熟知EXE和DLL的内涵,请跳过本节的内容。如果,你一直搞不清EXE和DLL,那就请仔细听我道来。
    一个正确的EXE文件,是可以直接运行的程序。Windows操作系统会为该程序创建一个进程空间,程序是在进程空间内运行的。进程空间是应用程序运行的基本环境,没有进程空间就根本无法运行程序。
    在EXE文件中,程序的数据引用关系和过程调用关系是用相对地址表示的,当程序加载到进程空间中的绝对地址上时,操作系统需要将对相对地址的引用和调用关系调整为对绝对地址的引用和调用关系,这一过程称为“重定位”。需要重定位的地方称为重定位项,它是保存在EXE文件的表头信息当中。
    程序的运行是需要堆栈的,因为堆栈是过程调用必须具备的基本设施,也是过程中局部数据和过程参数生死存亡的地方。操作系统会根据记录在EXE文件头中的堆栈大小值,确定堆栈空间的地址位置和大小,并首先映射最小堆栈空间的那部分内存。
    如果,一个EXE程序与某些DLL有固定的引用关系,操作系统将把相关的DLL程序调入当前进程空间。EXE和DLL以及DLL之间的引用或调用关系是用引入表和引出表来描述的,它们也保存在EXE或DLL的文件的表头信息当中。操作系统根据引入表和引出表信息,将程序模块间的引用和调用连接起来,这一过程称为“动态连接”。
    一个DLL文件只是程序的一部分,它并不是完整的可执行程序。因此,DLL不能单独地运行,Windows也不会为DLL模块创建进程空间。一个DLL模块必须被加载到一个EXE程序的进程空间中,才能发挥其程序功能。
    一个DLL文件也含有重定位信息,操作系统将DLL加载到进程空间中时,同样需要对DLL模块进行重定位。如果,被加载的DLL与其他DLL文件又有固定的引用关系,则加载该DLL模块时将同时加载其引用的DLL模块,并完成行动态连接过程。
    一个DLL文件中记录的堆栈大小总是为零,因为DLL只是为进程服务的,调用DLL模块时,使用的是进程的堆栈。对于单线程的应用程序,进程只有一个堆栈,就是进程的主线程使用的堆栈。对于多线程的应用程序,每个正在运行的线程都有自己的堆栈。在多线程的情况下,DLL模块在那个线程中被调用,则DLL就使用该线程的堆栈。但所有这一切都还是在同一进程空间中进行的。
      

  5.   

    如果,你使用Windows API的LoadLibrary函数动态地加载一个DLL文件,其返回的模块句柄的值,就是指向该DLL加载到进程空间中的地址值。如果,你反复调用LoadLibrary加载同一个DLL,你会发现他们返回的模块句柄值是相同的,Windows只是增加该DLL的引用计数。其实,在不同Windows进程之间,DLL是共享的,只是,在不同的进程空间有不同的映射地址。
    一个DLL模块的文件扩展名不一定都是*.DLL。象DELPHI的BPL包文件,ActiveX对象的OCX文件,以及设备驱动程序的DRV和VXD文件都是DLL。当然,一个EXE模块的文件扩展名也不一定要是*.EXE,只是,非*.EXE的可执行文件是不能通过双击鼠标来执行的,需要由编写程序语句来调入并运行。
    DLL只有在进程空间内才能运行,这是一个基本的原则,请你一定牢记。
    但有时,在Windows中的一个DLL本身已经是一个完整的程序,就缺少运行的进程空间。这时,Windows就用Rundll32命令来运行该DLL程序。Rundll32是一个简单的EXE文件,就在WINDOWS目录中,它仅仅为DLL程序提供了可运行的进程空间。
    此外,在多层体系结构应用程序开发中,存在于DLL模块中的商业对象,也是需要运行的进程空间的。如果,你的客户端应用程序和DLL应用服务器在同一台机器中,则DLL应用服务器中的商业对象是直接在你客户端程序的进程空间中运行的,这就是所说的In-Process模式。而对于Out-of-Process的对象调用模式,一般要求应用服务器是一个可执行的EXE文件。
    当然,如果应用服务器和客户端程序是在不同的机器中,应用服务器肯定要具备一个进程空间才能工作。例如,你无法通过直接的DCOM连接远端机器上的DLL应用服务器,这是因为DLL没有可运行的进程空间的缘故。不过,你可以通过Socket连接远端机器上的DLL应用服务器,这是因为DLL实际是在SCKTSRVR.EXE的进程空间中运行的,而SCKTSRVR.EXE是监听Socket连接的服务程序。
    在MTS模式的分布式多层应用程序开发中,所有的商业对象必须存在于DLL文件的应用服务器中。这些DLL中的商业对象共同存在于MTS的进程空间中,因此,MTS才可在自己的进程空间中调度和管理这些商业对象,以完成对象的事务控制、对象缓冲和连接共享等强大功能。
    第四节 数据和代码在哪里
    数据和代码在哪里?
    俗话说,人过了三十岁,就不会再去思考“人为什么要活着?”这些问题。可我编了十几年的程序还是喜欢去想 “我为什么要编程序?”、“程序为什么要运行?”、“运行着的是程序还是我的思想?”......尽管我已经三十出头。这样的思考是永远没有结果的,那只是程序生涯中最浪费生命的苦行。这样的苦行也是有所感悟的,常常让我的灵魂更加融入程序的世界。程序有了我的灵魂,运行的个性也似乎带有我的风格。受益的有我,也有我的程序。
    今天要和大家侃侃程序的数据和代码的问题。这个话题还是离不开进程空间的概念,因为程序的所有代码和数据都存在于进程空间之中。那么,数据和代码到底在进程空间的那些地方呢?
    先来看看进程空间中都有些什么区域。
    一个进程空间,与我们编程有关的区域大致有这么四种:静态数据区、动态数据区、堆栈区和代码区。进程空间中存在若干块这样的内存区域,它们随着程序的运行而动态变化着。一会儿有新的区域产生,一会儿又有些区域消失。如果把进程空间想象成大海,这些内存区域就是大海中的岛屿,随着潮起潮落,岛屿若隐若现。每一个岛屿都有操作系统映射的实际内存作为依托。没有映射实际内存的空白区域就是深不见底的海面,程序访问这些空白区域是要被淹死的。
    这四种内存区域各有各的用处。静态数据区域就是你定义的全局变量、常量、线程变量等生存的地方。动态数据区就是你动态分配的数据空间和动态创建的对象生存的地方。堆栈区既为子程序调用提供保存返回地址的空间,又为局部变量、参数变量和返回值提供临时空间。代码区是存储程序指令的区域,CPU是从这里提取指令来执行程序的。
    这些内存区域的产生和消失与程序模块的加载和卸出,以及线程的创建与消亡有关。
    我们在这里说所的模块是指Windows应用程序的物理文件模块,即Windows中HMODULE句柄所表示的模块概念。典型地,EXE文件是一个模块文件,各种DLL文件也是一个模块文件。一个应用程序一般由一个EXE文件和若干个类型的DLL文件组成。这种文件在Windows中称为PE文件(Portable Executable File)。它含有一个PE信息头,其中有加载程序模块需要的重要信息。
    当一个物理文件模块被加载到进程空间中时,Windows操作系统将根据该模块PE头的信息安排该模块的数据和代码在进程空间中的位置和大小。这样的加载包括EXE程序的执行加载,也包括启动EXE时及时加载DLL,以及程序随后动态加载DLL的过程。
    模块加载到进程空间,将产生该模块需要的若干静态数据区域,也将产生该模块的若干指令代码区域。若模块在运行过程中动态分配和释放内存或创建和消灭对象,又将相应地产生和释放动态数据区域。
    你可以认为模块的代码区是固定不变的,因为一般代码在执行过程中不会变化(除非你编写的是带变异功能的……),这不会影响对程序执行过程的理解。但模块的代码区也可能是经常变化的,这是Windows在背后搞的鬼。
    想象将一个有几兆或几十兆代码的程序模块加载到进程空间中,程序的启动速度会很慢吗?这会在进程空间中产生巨大的代码区域吗?有可能,但Windows会尽量不让这种情况发生。它会在CPU要用到某块代码区的指令时,将该代码区从磁盘文件中调入进程空间并执行。同时,如果发现有些代码块长时间没有被执行到,它又会释放这些代码区所占据的内存空间。当这些被释放的代码再次被CPU用到时,代码将被再次加载(当然加载的位置可能有所不同)。所以,即使是加载和运行一个巨大的模块文件,速度也不会明显降低,空间也不会明显增大。
    再想象一下,如果一个模块文件被加载之后将该文件拿走,Windows还能从消失的文件中正确加载CPU要用到的代码吗。当然不行!因此,Windows加载模块文件时就强制将该文件锁定,你是没办法修改和删除的。除非加载的是软盘上的模块,而你偏要强行将软盘抽走。
    同样,线程的创建和释放也会引起内存区域的变化。当线程创建时,Windows系统会在进程空间中为这个线程开辟两块内存区域。一块是相对于该线程的全局静态数据区域,一块是线程运行所需的堆栈区域。其中,堆栈区是线程运行必须的基本设施,和线程密切相关不可分割。一个线程一定会有一个堆栈,而一个堆栈一定对应一个线程。一个线程释放之后,其相关的静态数据区和堆栈也就消失。
    模块的静态数据区一般都是你定义的全局变量、常量等所在的地方。这些数据元素的访问地址都是相对于模块而言的。也就是说,这些数据元素标识方法和结构关系都是相对于该模块的程序的。模块外的其他模块程序是不能直接标识和解释本模块中定义的这些数据元素的,除非将这些数据元素的地址从模块内传递给其他模块的程序。例如,我们看看下面的两个模块程序:
    program ExeModule;
    function TheVariable: Pointer; external 'DllModule.DLL';
    begin
    // aVariable := 56789; //无法直接标识和访问另一模块中的数据元素。
    Integer(TheVariable^) := 56789; //只能通过从另一模块获取地址间接访问其数据元素。
    end.
    这个程序编译后将生成ExeModule.EXE模块文件。当然它启动时需要DllModule.DLL。
    library DllModule;
    var
    aVariable : Integer;
    function TheVariable: Pointer;
    begin
    result:=@aVariable;
    end;
    exports TheVariable;
    begin
    aVariable := 12345;
    end.
      

  6.   

    第二个程序由于指明它是library程序,所以编译后将生成DllModule.DLL模块文件。虽然在DllModule模块中定义了一个全局变量aVariable,但ExeModule模块的程序却无法直接标识和找到该变量。只能通过调用DllModule模块的TheVariable函数获取aVariable的地址指针之后,间接访问aVariable变量。
    有朋友说,如果将aVariable放到一个独立的PASCAL单元文件中,然后在两个模块的主程序中都uses这个单元,不就可以互相访问了吗?我们就来看看下面这些程序文件。
    这是VarUnit.pas单元文件:
    unit VarUnit;
    interface
    var
    aVariable : Integer;
    implementation
    end.
    这是ExeModule.dpr文件,它将生成ExeModule.EXE文件:
    program ExeModule;
    uses
    VarUnit;
    begin
    aVariable := 56789;
    end.
    这是DllModule.dpr文件,它将生成DllModule.DLL文件:
    library DllModule;
    uses
    VarUnit;
    begin
    aVariable := 12345;
    end.
    这两个模块都引用了VarUnit单元。假如这两个模块都在同一个进程空间中,那么,在ExeModule模块中访问的aVariable变量和在DllModule中访问的aVariable变量真的是同一个变量吗?
    答案是否定的!
    原来,ExeModule中访问的aVariable变量是在其自身模块的静态数据区域内,而DllModule中访问的aVariable变量也是自己所有的。尽管模块引用了相同单元中的变量,但这些变量在不同的模块中都会有一个独立的副本。在随后对运行包编译模式的讨论中,我们还将讨论到共用单元变量的问题。
    因此,我们要记住:在非运行包编译模式下,DELPHI中的各种全局变量和对象,如Application、Screen、Session、Printer等等,在每一个EXE和DLL模块中都有一个自己的副本,而不是同一个东西。
    那么,相对于线程的全局变量又如何呢?
    线程全局变量是用扩展的保留字threadvar定义的全局变量。threadvar只能用于定义全局变量,不能用于定义局部变量。由它所定义的全局变量在每一个线程中都有一个副本,存在于各自线程的全局静态数据区里。用threadvar定义的线程变量对每一个线程来说是独有的,在线程内可以放心使用。而使用var保留字定义的全局变量却是线程共享的,对其访问就要注意共享与互斥的问题了。
    接下来我们要讨论局部变量、参数变量和返回值的问题。
    这三类数据元素是子过程或函数局部的东西,作用域仅在该子程序内,具有临时性。他们是在线程的堆栈区内自生自灭的。随着子程序调用的发生而在堆栈中产生,随着子程序调用的返回而灭亡。线程的堆栈区随着子程序层层调用的深入和返回而潮起潮落,不断增长和减少。局部变量、参数变量和返回值就像礁石上的小生物,潮水涨上来便有一批生命诞生,潮水回落它们又死去。潮水再来的时候,又是另一个新的生命世界,一点都没有从前那个世界的任何记忆。生命也许就是这样短暂。
    在含有递归算法的程序中,线程的堆栈区会出现非常有趣的现象。在堆栈区增长的历史中,将出现许多惊人的相似。每一次的递归调用,一批相似的局部变量、参数变量和返回值都会出现一次,而且堆栈增长速度很快。所以,在编写含有递归算法的程序时一定要注意堆栈空间的问题。
    我们再来看一看动态数据区的情况。
    DELPHI中几乎所有的对象都是存在于动态数据区中的。尽管它们的对象指针可能是一个全局变量、一个线程全局变量、一个局部变量、一个参数变量或者一个返回值,但对象的实例却是在动态数据区中分配的(除非你重载了TObject的类方法NewInstance,在别的什么地方分配对象实例空间)。
    DELPHI的这种情况和标准的C++是不一样的。在标准的C++中定义一个全局对象时,它的实例存在于程序的全局静态区域中。而DELPHI是要在程序运行时动态创建,比如,在单元文件initialization部分的代码中创建。
    在早期的编程概念中,动态分配的数据区域又称为“堆”。那时候,由于机器可寻址空间较小,“堆”常常和“栈”共用一块空间。这块空间的顶部开始向下增长的部分就是“堆”,而从底部开始向上增长的部分就叫“栈”。所以,这块空间统称为“堆栈”。现在的编程空间已经非常广阔,“堆”的概念似乎已经过时了,而“栈”又和多线程的概念紧密联系在一起。因此,现在的“堆栈”一词就专指线程所用到的栈。
    最后,我们再来看看代码的情况。
    在DELPHI中,一个EXE或DLL的模块文件一般都是由一个项目文件(*.DPR)和若干个直接或间接引用的单元文件(*.PAS)编译而成。在没有优化编译选项的情况下,编写在项目文件和单元文件中的所有代码和数据都将编译进物理的模块文件中去。如果打开编译优化选项,则只有用到过的代码和数据才会编译到物理模块文件中。
    通常,你的应用程序是由多个物理文件模块组成,典型地由一个EXE模块和若干DLL模块构成。我们在编写模块程序的时候,总有一些单元是共用的。共用的单元既在一个模块的编译项目中被引用,又会在另一个模块的编译过程中被引用。令人遗憾的是,模块间共用单元中的代码和数据,将被编译在每一个引用过该单元的模块中。应用程序的这些模块被加载到进程空间中时,该单元的代码指令和数据将存在多个副本。
    虽然,我们可以将共用的单元文件独立出来,再编译成为一个DLL模块,以便共用一份代码和数据。但这将使程序模块的划分变得非常复杂,并且难于管理,在实际的开发过程中很难行得通。
    现在,我们已基本搞清了数据和代码会在哪里的问题。正如我说过,这样的探索和思考是一条苦行的路。因为,我们在苦苦的思考中明白了一些道理,但又会有新的难题出现。这个世界总不是完美的!
    DELPHI伟大之处就在于她能将许多复杂的难题变得很简单!随后我们将看到DELPHI提供的运行包编译模式(Build with runtime packages),是如何完美解决这个问题的。
    第五节 DELPHI的运行包
    ....不好意思还没写完....跳过....
    第六节 对象之梦
    有一回,我梦见自己变成了计算机时空世界里的一个对象。随着计算机世界的不断发展,我们这些对象已经不再象原始时代的对象那样仅仅为了获得生存的资源而不停的忙碌。我们的思想空前活跃,我门不但思考我们为什么要在计算机世界里生存和运行,而且还大胆的研究和探索计算机世界的未知奥秘。我们已经知道整个计算机世界都是由字节这一基本粒子构成,而字节又是由八个更细小的位粒子构成;我们还知道物质不灭定律,即任何一个对象的灭亡,只意味着对象结构的解体,并不会减少计算机世界中的任何字节或位粒子,而着这些物质又可能成为别的对象的一部分;甚至,我们还知道我们所处的世界是一个球体,因为,在越过经度$FFFFFFFF又回到了原点$00000000的位置。著名的物理学家对象牛顿早就发现各种对象之间存在一种普遍的联系,并且在对象的运动速度与对象大小的关系方面提出了著名的理论--牛顿力学。可是,后来牛顿这个对象却一直搞不懂到底是什么力量在无形地推动各种对象的运动。因此,他认为一定是创造整个计算机世界的上帝在推动各种对象的运动。后来他成了上帝最虔诚的信徒。在牛顿对象死后不久,我们的计算机世界又诞生了一个更伟大的对象。他基于先有代码的执行才有执行的结果这一基本的因果论,提出了进程运动的时空是相对的这一伟大理论。他认为,在一个运动中进程空间中看另一个运动中的进程空间,时间和空间都不是绝对的,空间会弯曲。而且,任何对象的运动速度绝对不可能超过CPU的速度,CPU速度就是我们计算机世界里的光速。这位伟大的科学对象的名字就叫爱因斯坦,他的相对论在一开始是不被对象们理解的,可是后来的科学探索都证明了这一理论的正确性。他提出的代码能量和数据物质可以相互转换的理论,也后来制造的大规模毁灭性病毒核武器中得到验证。
    在梦的世界里,我快乐极了。我一会儿变一变我的属性,一会儿又动动我的方法,一会儿感受一下外来的事件。没错,我确实就是一个实实在在的对象!过了一会我突然明白,我本来就是一个对象,只是这个对象在梦中变成了现实世界的我……
      

  7.   

    可能大家会奇怪这个帖子的标题,看看
    http://expert.csdn.net/Expert/topic/2079/2079277.xml?temp=5.851382E-02
    就会明白了。
      

  8.   

    那来的好文章,给个地址或什么的,让我们菜鸟学习学习。[email protected]
      

  9.   

    to:  hansion0947(丐帮最后一任当家) 
    无它,唯希望彻底搞懂尔。
      

  10.   

    嗬嗬,阿木这个家伙,说好三个月不回帖,现在搞出马甲来~这篇文章说节选自李战的《悟透delphi》可是我到现在都没有发现有卖的。李战兄也曾经在csdn和大家讨论过一些观点。如果这本书有卖的,我一定买。可是没有发现真的。再此谢谢 fengjn(小枫)这篇文章我老早看了好几遍了。但是里面的内容看了并不了解,但是现在终于明白了。真的很开心。离开csdn是找一份清静与他人无关。
      

  11.   

    VCL窗口函数注册机制研究手记,兼与MFC比较By  王捷  [email protected]     (转载请保留此信息)     这个名字起的有些耸人听闻,无他意,只为吸引眼球而已,如果您对下列关键词有兴
    趣,希望不要错过本文:1.        VCL可视组件在内存中的分页式管理;2.        让系统回调类的成员方法3.        Delphi 中汇编指令的使用4.        Hardcore 5.        第4条是骗你的     我们知道Windows平台上的GUI程序都必须遵循Windows的消息响应机制,可以简单概括
    如下,所有的窗口控件都向系统注册自身的窗口函数,运行期间消息可被指派至特定窗口
    控件的窗口函数处理。对消息相应机制做这样的概括有失严密,请各位见谅,我想赶紧转
    向本文重点,即在利用Object Pascali或是C++这样的面向对象语言编程中,如何把一个类
    的成员方法向系统注册以供回调。 
        在注册窗口类即调用RegisterClass函数时,我们向系统传递的是一个WindowProc 类
    型的函数指针WindowProc 的定义如下LRESULT CALLBACK WindowProc(  HWND hwnd,      // handle to window  UINT uMsg,      // message identifier  WPARAM wParam,  // first message parameter  LPARAM lParam   // second message parameter);    如果我们有一个控件类,它拥有看似具有相同定义的成员方法TMyControl.WindowProc,
    可是却不能够将它的首地址作为lpfnWndProc参数传给RegisterClass,道理很简单,因为
    Delphi中所有类成员方法都有一个隐含的参数,也就是Self,因此无法符合标准
     WindowProc 的定义。 
        那么,在VCL中,控件向系统注册时究竟传递了一个什么样的窗口指针,同时通过这个
    指针又是如何调到各个类的事件响应方法呢?我先卖个关子,先看看MFC是怎么做的。    在调查MFC代码之前,我作过两种猜想: 
      

  12.   

    一,作注册用的函数指针指向的是一个类的静态方法,静态方法同样不需要隐含参数 this (对应 Delphi中的 Self ,不过Object Pascal不支持
    静态方法)二,作注册用的函数指针指向的是一个全局函数,这当然最传统,没什么好说的。
        经过简单的跟踪,我发现MFC中,全局函数AfxWndProc是整个MFC程序处理消息的“根
    节点”,也就是说,所有的消息都由它指派给不同控件的消息响应函数,也就是说,所有
    的窗口控件向系统注册的窗口函数很可能就是 AfxWndProc (抱歉没做深入研究,如果不
    对请指正)。而AfxWndProc 是如何调用各个窗口类的WndProc呢?    哈哈,MFC用了一种很朴素的机制,相比它那么多稀奇古怪的宏来说,这种机制相当好理解:使用一个全局的Map数据结构来维护所有的窗口对象和Handle(其中Handle为键值),然后AfxWndProc根据Handle来找出唯一对应的窗口对象(使用静态函数CWnd::FromHandlePermanent(HWND hWnd) ),然后调用其WndProc,注意WndProc可是虚拟方法,因此消息能够正确到达所指定窗口类的消息响应函数并被处理。     于是我们有理由猜想VCL也可能采用相同的机制,毕竟这种方式实现起来很简单。我确
    实是这么猜的,不过结论是我错了......    开场秀结束,好戏正式上演。    在Form1上放一个Button(缺省名为Button1),在其OnClick事件中写些代码,加上断点,
    F9运行,当停留在断点上时,打开Call Stack窗口(View->Debug Window->Call Stack,
    或者按Ctrl-Alt-S )可看到调用顺序如下(从底往上看,stack嘛)( 如果你看到的 Stack 和这个不一致,请打开DCU 调试开关
     Project->Options->Compiler->Use Debug DCUs, 这个开关如果不打开,是没法调试VCL
    源码的 )
    TForm1.Button1Click(???)TControl.ClickTButton.ClickTButton.CNCommand ((48401, 3880, 0, 3880, 0))TControl.WndProc ((48401, 3880, 3880, 0, 3880, 0, 3880, 0, 0, 0))TWinControl.WndProc ((48401, 3880, 3880, 0, 3880, 0, 3880, 0, 0, 0))TButtonControl.WndProc ((48401, 3880, 3880, 0, 3880, 0, 3880, 0, 0, 0))TControl.Perform (48401,3880,3880)DoControlMsg (3880,(no value))TWinControl.WMComman d((273, 3880, 0, 3880, 0))TCustomForm.WMCommand ((273, 3880, 0, 3880, 0))TControl.WndProc ((273, 3880, 3880, 0, 3880, 0, 3880, 0, 0, 0))TWinControl.WndProc((273, 3880, 3880, 0, 3880, 0, 3880, 0, 0, 0))TCustomForm.WndProc ((273, 3880, 3880, 0, 3880, 0, 3880, 0, 0, 0))TWinControl.MainWndProc ((273, 3880, 3880, 0, 3880, 0, 3880, 0, 0, 0))StdWndProc (3792,273,3880,3880)
         可见 StdWndProc 看上去象是扮演了MFC中 AfxWndProc 的角色,不过我们先不谈它,
    如果你抑制不住好奇心,可以提前去看它的源码,在Forms.pas中,看到了么? 是不是
    特~~~~别有趣阿。      实际上,VCL在RegisterClass时传递的窗口函数指针并非指向StdWndProc。那是什么
    呢?      我跟,我跟,我跟跟跟,终于在Controls.pas的TWindowControl的实现代码中(procedure TWinControl.CreateWnd;) 看到了RegisterClass的调用,hoho,终于找到组
    织了......别忙,发现了没,这时候注册的窗口函数是InitWndProc,看看它的定义,嗯,
    符合标准,再去瞧瞧代码都干了些什么。    发现这句:SetWindowLong(HWindow, GWL_WNDPROC,Longint(CreationControl.FObjectInstance));
      

  13.   

    我Faint,搞了半天InitWndProc初次被调用(对每一个Wincontrol来说)就把自个儿
    给换了,新上岗的是FObjectInstance。下面还有一小段汇编,是紧接着调用
    FObjectInstance的,调用的理由不奇怪,因为以后调用FObjectInstace都由系统CallBack
    了,但现在还得劳InitWndProc的大驾去call。调用的方式有些讲究,不过留给您看完这篇
    文章后自个儿琢磨去吧。    接下来只能继续看FObjectInstance是什么东东,它定义在 TWinControl 的 Private 
    段,是个Pointer也就是个普通指针,当什么使都行,你跟Windows说它就是 WndProc 型指
    针 Windows 拿你也没辙。     FObjectInstance究竟指向何处呢,镜头移向 TWincontrol 的构造函数,这是
    FObjectInstance初次被赋值的地方。 多余的代码不用看,焦点放在这句上
     
         FObjectInstance := MakeObjectInstance(MainWndProc);      可以先告诉您,MakeObjectInstance是本主题最精彩之处,但是您现在只需知道
    FObjectInstance“指向了”MainWndProc,也就是说通过某种途径VCL把每个MainWndProc
    作为窗口函数注册了,先证明容易的,即 MainWndProc 具备窗口函数的功能,来看代码:( 省去异常处理 )procedure TWinControl.MainWndProc(var Message: TMessage);begin      WindowProc(Message);      FreeDeviceContexts;      FreeMemoryContexts;end; FreeDeviceContexts; 和  FreeMemoryContexts 是保证VCL线程安全的,不在本文讨论之列
    ,只看WindowProc(Message); 原来 MainWndProc 把消息委托给了方法 WindowProc处理,
    注意到 MainWndProc 不是虚拟方法,而 WindowProc 则是虚拟的,了解 Design Pattern 
    的朋友应该点头了,嗯,是个 Template Method , 很自然也很经典的用法,这样一来所有
    的消息都能准确到达目的地,也就是说从功能上看 MainWndProc 确实可以充作窗口函数。
    您现在可以回顾一下MFC的 AfxWindowProc 的做法,同样是利用对象的多态性,但是两种方
    式有所区别。    是不是有点乱了呢,让我们总结一下,VCL 注册窗口函数分三步: 1.  [ TWinControl.Create ]    FObjectInstance 指向了 MainWndProc2.  [ TWinControl.CreateWnd ]     WindowClass.lpfnWndProc 值为 @InitWndProc;     调用Windows.RegisterClass(WindowClass)向系统注册3.  [ InitWndProc 初次被Callback时 ]    SetWindowLong(HWindow, GWL_WNDPROC, Longint(CreationControl.FObjectInstance))     窗口函数被偷梁换柱,从此 InitWndProc 退隐江湖    (注意是对每个TWinControl控件来说,InitWndProc 只被调用一次)
         前面说过,非静态的类方法是不能注册成为窗口函数的,特别是Delphi中
    根本没有静态类方法,那么MainWndProc 也不能有特权(当然宝兰可以为此在编译器上动点
    手脚,如果他们不怕成为呕像的话)。    那么,那么,您应该意识到了,在幕后操纵一切的,正是......     背景打出字幕    超级巨星:麦克奥布吉特因斯坦斯              (MakeObjectInstance)    天空出现闪电,哦耶,主角才刚刚亮相。
        废话不说,代码伺候: ( 原始码在 Form.pas 中,“{}”中是原始的注释,而“ //” 后的是我所加,您可以直
    接就注释代码,也可以先看我下面的评论,再回头啃code )// 共占 13 Bytes,变体纪录以最大值为准type  PObjectInstance = ^TObjectInstance;  TObjectInstance = packed record    Code: Byte;                     // 1 Bytes    Offset: Integer;                // 4 Bytes    case Integer of                           0: (Next: PObjectInstance);  // 4 Bytes      1: (Method: TWndMethod);     // 8 Bytes                                     // TWndMethod 是一个指向对象方法的指针,                                   // 事实上是一个指针对,包含方法指针以                                   // 及一个对象的指针(即Self )  end; // 313是满足整个TInstanceBlock的大小不超过4096的最大值InstanceCount = 313; 
    // 共占 4079 Bytestype  PInstanceBlock = ^TInstanceBlock;  TInstanceBlock = packed record    Next: PInstanceBlock;        // 4 Bytes    Code: array[1..2] of Byte;   // 2 Bytes    WndProcPtr: Pointer;         // 4 Bytes    Instances: array[0..InstanceCount] of TObjectInstance; 313 * 13 = 4069  end; function CalcJmpOffset(Src, Dest: Pointer): Longint;begin  Result := Longint(Dest) - (Longint(Src) + 5);end; function MakeObjectInstance(Method: TWndMethod): Pointer;const  BlockCode: array[1..2] of Byte = (    $59,       { POP ECX }    $E9);      { JMP StdWndProc }  // 实际上只有一个JMP  PageSize = 4096;var  Block: PInstanceBlock;  Instance: PObjectInstance;begin  // InstFreeList = nil 表明一个Instance block已被占满,于是需要为一个新  // Instance block分配空间,一个个Instance block通过PinstanceBlock中的  // Next 指针相连,形成一个链表,其头指针为InstBlockList    if InstFreeList = nil then  begin    // 为Instance block分配虚拟内存,并指定这块内存为可读写并可执行    // PageSize 为4096。    Block := VirtualAlloc(nil, PageSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);    Block^.Next := InstBlockList;    Move(BlockCode, Block^.Code, SizeOf(BlockCode));    Block^.WndProcPtr := Pointer(CalcJmpOffset(@Block^.Code[2], @StdWndProc));        // 以下代码建立一个Instance的链表    Instance := @Block^.Instances;    repeat      Instance^.Code := $E8;  { CALL NEAR PTR Offset }      //算出相对 jmp StdWndProc指令的偏移量,放在$E8的后面      Instance^.Offset := CalcJmpOffset(Instance, @Block^.Code);      Instance^.Next := InstFreeList;      InstFreeList := Instance;      // 必须有这步,让Instance指针移至当前instance子块的底部      Inc(Longint(Instance), SizeOf(TObjectInstance));      // 判断一个Instance block是否已被构造完毕    until Longint(Instance) - Longint(Block) >= SizeOf(TInstanceBlock);    InstBlockList := Block;  end;  Result := InstFreeList;   Instance := InstFreeList;  InstFreeList := Instance^.Next;  Instance^.Method := Method;end;
      

  14.   

    不要小看这区区几十行代码的能量,就是它们对 VCL 的可视组件进行了分页式管理,
    (代码中对两个链表进行操作,InstanceBlock 中有 ObjectInstance 的链表,而一个个
    InstanceBlock  又构成一个链表 )一个 InstanceBlock 为一页,有4096 字节,虽然 
    InstanceBlock 实际使用的只有 4079 字节,不过为了 Alignment ,就加了些 padding 
    凑满 4096 。从代码可见每一页中可容纳 313 个所谓的ObjectInstance,如果望文生义
    很容易将这个 ObjectInstance 误解为对象实例,其实不然,每个ObjectInstance 其实是
    一小段可执行代码,而这些可执行代码不是编译期间生成的,也不是象虚拟函数那样滞后
    联编,而根本就是MakeObjectInstance 在运行期间“创作”的(天哪)! 也就是说,
    MakeObjectInstance 将所有的可视VCL组件 改造成了一页页的可执行代码区域,是不是
    很了不起呢。    不明白ObjectInstance所对应的代码是做什么的么?没关系,一起来看
                    call  - - - - - - - - - - - >  pop ECX                           // 在call 之前,下一个指令的地址会被压栈                @MainWndProc                                                     // 紧接着执行pop ECX, 为何这么做呢?                @Object(即Self)  // 前面注释中提过
         答案在 StdWndProc 的代码中,要命哦,全是汇编,可是无限风光在险峰,硬着头皮闯
    一回吧。     果不其然,我们发现其中用到了ECXfunction StdWndProc(Window: HWND; Message, WParam: Longint;  LParam: Longint): Longint; stdcall; assembler;asm        XOR     EAX,EAX                                                 PUSH    EAX        PUSH    LParam        PUSH    WParam        PUSH    Message        MOV     EDX,ESP        MOV     EAX,[ECX].Longint[4] //   相当于 MOV EAX, [ECX+4] ( [ECX+4] 是什么?就是Self )        CALL    [ECX].Pointer        //   相当于 CALL    [ECX] , 也就是调用 MainWndProc        ADD     ESP,12        POP     EAXend;  
        这段汇编中在调用MainWndProc前作了些参数传递的工作,由于MainWndProc 的定义如
    下:procedure TwinControl..MainWndProc(var Message: TMessage);
        根据Delphi 的约定,这种情况下隐函数Self 作为第一个参数,放入EAX 中,
    TMessage 结构的指针作为第二个参数,放入EDX中,而Message的指针从哪儿来呢?我们看
    到在连续几个 Push 之后,程序已经在堆栈中构造了一个TMessage 结构,而这时的ESP 
    当然就是这个结构的指针,于是将它赋给EDX 。如果您不熟悉这方面的约定,可以参考
    Delphi 的帮助Object Pascal Refrence -> Program Control。    现在真相大白,Windows 消息百转千折终于传进MainWndProc , 不过这一路也可谓相
    当精彩,MakeObject这一函数自然是居功至伟, StdWndProc 也同样是幕后英雄,让我们
    把 MakeObjectInstance 作出的代码和StdWndProc 连接起来,哦,堪称鬼斧神工.   ( 大富翁没法显示图像,可以去
         http://jp.njuct.edu.cn/crystal/article\vcl%20hardcore.htm
         看完整全文,感谢房客支持)    就此在总结一下, FobjectInstance 被VCL 注册为窗口函数,而实际上 
    FObjectInstance 并不实际指向某个函数,而是指向一个ObjectInstance, 而后者我们已
    经知道是一系列相接的可执行代码段当中的一块,当系统需要将 FObjectInstance 当做
    窗口函数作为回调时,实际进入了ObjectInstance 所在的代码段,然后几番跳跃腾挪(
    一个call 加一个 jump )来到StdWndProc ,StdWndProc 的主要功用在于将Self 指针
    压栈,并把Windows的消息包装成Delphi的TMessage 结构,如此才能成功调用到
    TWinControl类的成员方法 MainWndProc, 消息一旦进入MainWndProc 便可以轻车熟路一路
    高唱小曲来到各个对象转属的WndProc , 从此功德圆满。
        后记:                   个人感觉在这一技术上VCL 要比MFC 效率高出不少,后者每次根据窗口句柄来检索相
    对应的窗口对象指针颇为费时,同时MakeObject  的代码也相当具有参考价值,有没有想
    过让你自己的程序在内存中再开一堆可执行代码?
           
        所有的代码是基于Delphi5的,可能与其余版本有所出入,但相信不会很大。    整个星期六和星期天我都花在写作此文上了(连调试带写字), 不过水平所限,难免
    有所错误与表达不周,但愿不至以己昏昏令人昏昏,欢迎来信探讨指教
      

  15.   

    reallike(此人已删除)  太客气了
      

  16.   

    属性 (Property) 介绍 
    property Active: Boolean(唯读) 当此 DataLink 连结至一个已开启的 DataSource 时会传回 True。当 Active 状态改变时会 触发ActiveChanged方法。property ActiveRecord: Integer(可读写) 用来设定或取得 DataLink 缓冲区中目前所指向的记录代码,代码的范围是 0 .. BufferCount - 1。使用它来设定记录代码时必须小心不要超过这个范围,否则可能导致不可预期的错误。property BufferCount: Integer(可读写)DataLink 拥有一个资料缓冲区。而 BufferCount 属性即用来设定或取得缓冲区大小,缓冲区大小决定了一个dataset同时可以显视的资料记录笔数。对大部分的资料感知元件来说,BufferCount 的值是 1;但对 TDataGrid 来说,BufferCount 代表它的可视列数目。property DataSet: TDataSet(唯读)传回此 DataLink 所连结的 DataSet。其实就是 DataSource.DataSet。property DataSource: TDataSource(可读写) 传回此DataLink所连结的DataSource。property DataSourceFixed: Boolean(可读写)这个属性可用来防止 DataSource 属性被更改。如果此属性设为 True,当我们试著改变 DataSource 属性时会引发一个例外。property Editing: Boolean(唯读)如果 DataLink 正处於编辑状态则传回 True。property ReadOnly: Boolean(可读写)设定 DataLink 是否为唯读状态。这个属性并不会影响所连结的 DataSet。在唯读状态下这个 DataLink 无法进入编辑状态。property RecordCount: Integer(唯读)传回DataSet的资料记录数目。方法 (Method) 介绍 
    function Edit: Boolean;让所连结的DataSet进入编辑状态。传回值: 成功传回 True ,失败传回 Falseprocedure UpdateRecord;我们不直接呼叫这个方法,它是提供其它程式来呼叫的。这个方法只有设定一个旗帜然後呼叫 UpdateData 方法。虚拟方法 ( Virtual Method ) 
    要让 TDataLink 物件与元件沟通必须改写下列这些方法:procedure ActiveChanged当连结的 DataSource 开启状态改变时会呼叫此方法。使用 Active 属性可以得知目前是否为开启状态。procedure CheckBrowseMode资料库有任何改变之後都会先呼叫这个方法。procedure DataSetChanged;当下列任一事件发生时都会呼叫此方法:移至DataSet的开头 
    移至DataSet的结尾 
    在DataSet中插入或新增资料 
    删除DataSet的资料 
    取消DataSet的编辑 
    更新记录 
    如果不想改写这个方法只要在其中呼叫:RecordChanged(nil);procedure DataSetScrolled(Distance: Integer)每当目前记录变更时会呼叫此方法。Distance 参数代表缓冲区欲卷动的行数。(其值范围皆在 -1 .. 1 之间)。使用 ActiveRecord 属性可以取得缓冲区中目前所指向的记录。我们无法强制让 DataLink 的缓冲区卷动。procedure FocusControl(Field: TFieldRef)与TField.FocusControl方法相同。procedure EditingChanged当 DataLink 的编辑状态改变时会呼叫此方法。使用 Editing 属性可以得知DataLink 是否 正处於编辑状态。procedure LayoutChanged当 DataSet 的 Layout 改变时会呼叫此方法(例如新增一个column)。procedure RecordChanged(Field: TField)当下列任一事件发生时都会呼叫此方法:目前记录进入编辑状态 
    目前记录内容更动 
    procedure UpdateData在一笔记录被更新以前会呼叫此方法。你可以呼叫 Abort 程序来防止资料库更新。如何得知一个 dataset 中有几笔记录?TDateSet 的 RecNo 属性可以传回资料记录的数目,但很不幸地它只适用於 dBase 及 Paradox 的资料表格。若想得知目前资料记录的编号,可以从 TDataLink 类别衍生一个新的类别,然後进行下 列步骤:改写 DataSetScrolled 方法以取得目前记录是否被卷动。 
    改写 DataSetChanged 方法来确认目前记录是否跳至最前面或最後面了。 
    接著你可以将这个新类别的物件连结到TDataSource物件上然後就可以随时得知目前的记录编号了。 
      

  17.   

    资料大部分来源于delphi_tips(经过了整理)
      

  18.   

    答网友问:
    为什麽我的元件得不到方向键? 想要你的控件能够处理方向键,你必须要拦截 CM_WANTSPECIALKEY 消息
    如果讯息传回值是非零值,这个键就会被送至 KeyPress 方法以供处理,否则这个键的消息会被送至父类,以预设方式来处理。一个简单的范例:typeTMyComponent = class (TWinControl)... protected procedure CMWantSpecialKey(var Message: TCMWantSpecialKey); message CM_WANTSPECIALKEY;...end;procedure TMyComponent.CMWantSpecialKey(var Message: TCMWantSpecialKey); begininherited;// 我们只想处理向左方向键,其它的特殊键都给 Windows 处理if Message.CharCode = VK_LEFT thenMessage.Result := 1; end;
      

  19.   

    还是kingron发布的那个45M的delphi_tips还是eping整理的超级猛料?
      

  20.   

    Linux2001(恋人不如自恋) 
    主要还是资源共享嘛,至于标题,只不过是一个标记而已,那么在意干嘛。
      

  21.   

    http://www.eping.net/http://www.eping.net/fourm/list.asp?boardid=8http://www.soulan.com/kingron/
      

  22.   

    应该是kingron的,但是没有你说的那么大啊
      

  23.   

    另外,还没有几个人像kingron这样整理资料的。你所说的delphi_tips应该就是kingron整理的猛料。
      

  24.   

    哦,知道了。
    留个qq吧
    my qq:64311326
      

  25.   

    如果你用猛料,希望你支持kingron,我很感激他。
      

  26.   

    属性 (Property) 介绍 
    property Active: Boolean(唯读) 当此 DataLink 连结至一个已开启的 DataSource 时会传回 True。当 Active 状态改变时会 触发ActiveChanged方法。
    ……上面的这段是TDataLink的说明文档,copy的时候把第一行丢到了,可能大家不知道他是什么。在这里表示歉意。