《真是想不到系列》

每次看大师的东西到了精彩之处,我就会拍案叫绝:"哇噻,真是想不到!"。在经过很多次这种感慨之后,我发现只要我们动了脑筋,我们自己也能有让别人想不到的东西。于是想到要把这些想不到的东拿出来和大家一起分享,希望抛砖引玉,能引出更多让人想不到的东西。 真是想不到系列之一:VB到底为我们做了什么? 关键字:VB、底层、WIN32、API、COM 难度:中级 要求:熟悉VB,会用VC调试器,了解WIN32 SDK、COM。 VB一直以来被认为有以下优缺点:优点是上手快、开发效率高;缺点是能力有限,运行效率低。这正是有些软件把VB做为首选语言,而有些软件肯定不会用VB 做的原因。而很多VC,DELPHI的程序员都认为VB里搞开发不自由,它让我们做事变容易的同时,也让我们发挥的余地越来越小。的确,简单和功能强大这 两者本身就是一对矛盾。那怕一行代码不写,仅仅起动运行一个空窗体这样简单动作,VB在底下就为我们做了大量复杂的工作(决不仅仅是注册窗口类、显示窗 口、起动消息循环这么简单),这些工作对程序员是透明的。我们在感谢VB开发小组对我们程序员体贴入微的同时,不禁也要责怪为什么在文档中对这些底层的动 作只字未提,虽然这些动作对最终的程序也许并无影响,但我们拥有知情权,更何况这些动作有时的确会影响我们的工作(我将在本系列后面的《VB多线程》中谈 到这种影响)。 然而,所有希望从本文得到"未公开技术秘密"的朋友你将会很失望,因为我能够知道的和你一样多,我们所能做的一切就是站在外面来猜VB在里面做了什么?所 以我决不是要带大家一起去将VB反向工程,而是想通过猜想VB的内部工作来将一些原来比较模糊的概念搞清楚。作为一个系列的第一篇文章,它的目的是为了后 面的深入打下基础,所以我会在需要的时候指出我们必须掌握的知识点,如果你不清楚,请及时地学习相关书籍来补课,具体见《参考书目》。 最后,要声明我在本文中所做的各种实验和推断仅是我个人的观点,不能保证其正确性,并且不承担任何相关的法律责任。 好,开始吧!首先准备好我们的武器,我下面要使用的工具主要有:VB6中文企业版+SP5(废话),还有SPY++、Dependency Walk和OLE Viewer(以下简称SPY和DEPEND和OLEVIEW,SPY在VB光盘的common/tools/vb/下的SPY目录中,OLEVIEW是 其下OLETOOLS目录中的OLEVIEW.EXE,注意其下还有一个OLE2VW32.EXE功能类似,不过本文所指的是OLEVIEW.EXE,还 Denpend在其下的Unsupprt/DEPEND里)。还要用用VC(上面提的工具在VC里有),因为我们还要看看VB生成的代码,搞VB高级开发 的朋友一定要会用VC调试器,懂点汇编更好。当然,本文的重点不在这儿,所以没有VC也不要紧。 打开VB6新建一标准EXE工程,在"工程"->"引用"对话框里应该已有四个引用,简单点就是:1、Visual Basic For Application(VBA) 2、VB运行时对象库 3、VB对象库 4、OLE自动化。前面三个是任何VB工程都必须的,你想不要都不行,不信你试着去掉对它们的引用。那么这三个核心类型库各有什么用,在最终生成的可执行 程序中扮演怎样的角色,这是本文要分析的第一个问题。 1)VB、VBA、VBS的区别你搞清楚了吗? 首先VBS不应该和VB、VBA放在一起比较,它是微软按照自己定义的ActiveX Scripting规范完全从头开始写成的脚本语言,虽然它的语法结构和VB非常相似,但VBS仅仅依靠自动化对象来扩充其功能(只有后期绑定),它不能 用implements来实现接口,不可能在VBS里直接使用API,没有VarPtr这样能得到指针的函数,而VBS缺少的这些功能正是VB和VBA所 特有的。当然,这不是说VBS不如VB或VBA,Windows已经为VBS提供了足够强大的功能,我们可以用VBS来做脚本COM组件,而且借自动化对 象的能力VBS可以说能力无限,所以有病毒用VBS来写,对程序员来说VBS最重要的功能莫过于可以给自己的软件提供宏功能,就象VC中提供的VBS宏功 能那样。注意,VBS是Free的,这和在Office中使用VBA来提供宏功能不同,要集成VBA需要价格不低的许可证费用,关于脚本语言可参见 MSDN中Platform SDK/Tools and Languages/Scripting。(在本系列后面的文章《脚本功能》中我会实做一个用VBS来提供宏功能的小软件) 那么VB和VBA又有什么不同呢?好吧,眼见为实,开始我们的实验吧! 如果装了Office 2000以上版本,那么打开OLEVIEW,点击File下的View TypeLib查看位于E:/Program Files/Common Files/Microsoft Shared/VBA/VBA6下的VBE6.dll的类型库,再用同样的方法看看MSVBVM60.dll的类型库,你会发现它们的类型库基本上一模一 样,除了VBE6多了一个VBEGlobal接口和实现这个接口的Global对象,这个Global对象我们也可以在VBA编程环境(比如用WORD的 VB编辑器)中用对象浏览器看到。它有二个方法Load和UnLoad,还有一个UserForms属性,这是因为VBA6使用MS Form 2.0 Form设计器(FM20.dll)来设计和使用UserForm窗体(而在VB6中,我们可以使用多个设计器。比如通过使用MS Form 2.0 Form设计器,我们就能在VB中使用VBA所使用的UserForm用户窗体)。和VBA的Global对象类似,在VB中也有GLobal对象,从 VB的对象浏览器中可以知道它在vb6.olb这个类型库中,这个类型库就是每个工程都必须引用的VB对象库,所有的VB内置对象都在这里。而VBA的 UserForm中使用的对象都在FM20.dll中。 除了上述不同外,VB和VBA还有一个最大的不同,就是VBA不能生成EXE可执行文件,但可以猜想在IDE环境中VBA和VB都要把代码编译成p-code来执行,后面我将用实验来证明的确是这样,虽然在具体的实现上VB和VBA有很大的不同。 从上面的分析上可以看到VB和VBA还是有很大不同的,这种不同主要体现在编程环境和对象结构上,但在本质上它们之间却有着不可割舍的血源关系。如果刚才你仔细地观察了MSVBVM60.dll的类型库,你就会发现如下的片断: // Generated .IDL file (by the OLE/COM Object Viewer) [ dllname("VBA6.DLL"), uuid(35BFBDA0-2BCC-1069-82D5-00DD010EDFAA), helpcontext(0x000f6ec4) ] module Strings { [entry(0x60000000), helpcontext(0x000f665f)] short _stdcall Asc([in] BSTR String); [entry(0x60000001), helpcontext(0x000f6e9f)] BSTR _stdcall _B_str_Chr([in] long CharCode); …………… } 什么?在MSVBVM60.dll中的对象其方法却定义在VBA6.DLL中?!VB安装目录下不就有个VBA6.DLL吗?再用OLEVIEW看看它, 哇噻,真是想不到它居然和MSVBVM60.DLL的一模一样。怎么回事?赶快再拿出DEPEND来看看VBA6.dll、MSVBVM60.dll和 VBE6.dll这三个DLL的输出函数。哈,又有新发现,我们可以发现在三个DLL的输出函数中从编号512到717绝大部分都是一模一样的一些以 rtc开头的函数,比如595的rtcMsgBox(rtc是什么?应该是Run Time Component? Control? Code?有谁知道吗?),这说明三个DLL都有着相同的运行时VBA函数。 我们再用DEPEND来观察一下VB6.EXE, 我们可以发现VB6.EXE引入了VBA6.DLL中一些它特有的以Eb和Tip开头的函数,从这些函数的名称上可以发现它们的功能都是IDE相关的,比 如79的EbShowCode和82的TipDeleteModule。VB6.EXE恰恰没有引入任何rtc开头的函数(注意一)。我们再来看看 MSVBVM60.DLL,随便找一个用了MsgBox函数的编译后的文件,用DEPEND来观察它,就会发现它引入MSVBVM60.DLL输出的 595号rtcMsgBox函数(注意二)。并且引入MSVBVM60.DLL中很多以下划线开头的函数,比如__vbaVarAbs(注意三)。其实从 这个三个"注意"中我们已经可以进行一些猜想,无论对错,你可以先想想。 如果你没有跟着我做实验,而仅仅是看这篇文章的话,我猜想你应该有点昏了。如果你自己动手做了这些实验,现在你应该充满了疑问而急侍看到结论。所以请一定要亲手试一试,学习研究问题的方法比看结论更重要。 到这里至少我们可以得出结论:VB和VBA本就是同宗的姐妹,只不过姐姐VB的功夫要比妹妹VBA历害些。不过姐姐只会单打独斗是女强人;妹妹却只会傍大 款。姐姐有生育能力,是真正的女人;妹妹却不会生崽,但深谱相夫之道,一番教导指挥之下可使她老公增色不少,而VBS呢,也是大户人家的女儿,不过没有 VB和VBA姐妹优秀的血统,娇小玲珑干不得粗活只能指挥些自动听话的对象来干活,她乐于助人品德好不象VBA那样只认大款,VB、VBA、vbs三个女 人我都喜欢。 2)Native Code(本地代码)到底做了什么? 打起精神,我们再深入一步。用OLEVIEW得到的类型库还不能正确的反映各对象方法对应的DLL中的函数入口,你应该已经发现用OLEVIEW得到的 IDL文件中各个方法的entry属性值都是0x600000XX这样的假东西。要得到类型库中各方法在DLL中的真正入口,我们需要自己来写段程序。 即使在VB中我们也可以非常容易地获取类型库信息,再加上点COM初始化和调用代码,我们就能用自己的代码实现VB6才引入的CallByName函数 (在本系列后面的《Hack COM》中我会更深入谈谈COM,作为一名VB程序员对COM的理解非常重要)。由于本文的关键不是指导如何在VB里使用类型库,所以下面提供的方法尽量 从简。 新建一个标准EXE工程,添加对TypeLib Infomation的引用,在Form中放一个名为lblInfo的标签,然后添加如下代码: 注释:程序1 Private Sub Form_Load() Dim oTLInfo As TypeLibInfo Dim oMemInfo As MemberInfo Dim sDllName As String Dim sOrdinal As Integer Set oTLInfo = TLI.TypeLibInfoFromFile("MSVBVM60.DLL") lblInfo = "MATH模块包含以下方法:" & vbCrLf For Each oMemInfo In oTLInfo.TypeInfos.NamedItem("Math").Members With oMemInfo .GetDllEntry sDllName, vbNullString, sOrdinal lblInfo = lblInfo & .Name _ & "定义在" & sDllName & "中," _ & "其编号为" & sOrdinal _ & vbCrLf End With Next End Sub 运行以后我们就可以知道MATH模块中的Abs方法定义在VBA6.DLL中,其编号为656。在DEPEND中查看VBA6.DLL中编号为656的函数,果然就是rtcAbsVar,用VBE6.DLL试试结果相同。 还记得前面的注意一吧,VB6.EXE没有引入rtc开头的函数这说明在IDE环境中执行的VBA方法实际上是通过COM调用VBA对象库中的方法(跟踪 p-code是噩梦,所以我无法验证它用的是什么绑定方式)。而注意二中提到的最终可执行程序中引入了rtcMsgBox,如我们所料最终的程序会直接调 用它,这要比COM调用快一点,但在跟踪最终程序时,我发现rtcMsgBox内部却是经过了二万五千里长征后才会去调用MessageBoxA这个 API,其间有多次对其它对象的COM调用,慢!可能是因为显示的是模态对话框,在多进程多线程环境有很多需要考虑的因素吧,如果你是疯狂在意效率的程序 员,你应该试试用API来重写MsgBox,绝对快不少。再来看看注意三,让我们把以下的程序编译成使用本地代码的"程序2.EXE"(为了后面的实验, 可以在工程属性的编译选项卡中将它设成"无优化"和"生成符号化调试信息"程序2.EXE""): 注释:程序2 Private Declare Sub DebugBreak Lib "kernel32" () Private Sub Main() Dim i As Long, j As Long Dim k i = &H1234 DebugBreak k = 1234 j = Abs(k) j = Abs(i) MsgBox "ss" j = VarPtr(i) End Sub 用DEPEND观察"程序2.EXE",我们可以发现"程序2.EXE"并没有如我们预期的一样在引入595的rtcMsgBox的同时引入656的 rtcAbsVar,相反它引入了__vbaVarAbs和__vbaI4Abs,看看函数名就知道一个针对的是Variant,一个针对的是long。 这说明VB在最终生成的代码中对象Abs这样的可以进一步针对不同类型优化的VBA函数进行了相应的处理,观察一下所有以__vba开头的函数绝大部分都 是那些最基本最常用的VBA函数,可以说__vba开头的VBA函数是rtc开头的VBA函数的优化版本,它们基本上是VB开发小组重新写的,绝大多数在 函数内部实现自身功能,而rtc开头的函数大多数是调用COM服务对象来完成工作。从这么多__vba开头的函数上可以看出VB小组在Native Code(本地代码)的优化上下了不少功夫,这决对不是吹牛。它的确高度优化了不少科学计算相关的函数,以ABS为例Native Code要比p-code快4倍以上。但是并不是所有的计算函数都经过了这样的优化,比如Rnd函数,它就没有对应的__vba开头的优化函数,而是直接 对应到rtcRandomNext函数上,虽然rtcRandomNext也已经优化过,但内部依然用了COM调用,还是不如自己重写的快,我不明白为什 么VB开发小组没有考虑为它写一个对应的__vbaRnd。 不要以为上面的分析没有意义,因为我们可以从现象看本质,也可以从本质来解释现象。比如我们再做一个实验,给你的代码加入一个类模块,你可以试试声明一个 和内部方法同名的公有的方法(这是一个很有用的技术,在本系列后面的《错误处理》中我们会用到这种方法),比如我们可以声明一个Public Function Rnd(x) as single,同样我们可以自己写一个同名的MsgBox。但是你试试能不能声明一个Public Function abs(x) ,这时VB肯定会弹出一个莫名其妙的编译错误提示框告诉你"缺少标识符",这种错误发生在你的函数名和VB关键字冲突的时候。但是为什么同样是MATH模 块中的函数,abs是关键字,rnd却不是,VB文档里是不会告诉你为什么的,但如果你认真的看了我上面的实验分析,我们就能猜想这是因为VB对需要进一 步优化的函数已经做了高度优化处理,VB开发小组为了保护他们的劳动成果,并显示他们对自己优化技术的自信,而禁止我们重写这些函数,同时VB开发小组也 承认还有些函数有待进一步优化,所以准许我们重写之。在这里我要提出一个伟大的猜想:凡是能够被重写的函数就能够被优化,就象凡是大于2的偶数就能够被分 解成两个质因数的和一样。 说到优化,还应该谈谈直接API调用和使用API类型库的差别,还必须谈谈VB所使用的后端优化器(和VC用的是一样的优化器),还想谈谈如何尽最大可能来使用vTable绑定……(准备在本系列中另写一篇《优化》来谈这些问题)。 看了本地代码,我们再来看看p-code,要是你看了MSDN中关于p-code的原理,你肯定会头大。平心而论p-code真是一个了不起的技术,代码 大小平均可以缩小50%。我们把程序2编译成p-code看看,还是用DEPEND来观察,发现它并没有引入__vba开头函数(没有使用优化的VBA函 数?),却引入了CallEngine这样的东西(肯定是为了调用p-code伪码解释引擎),而且和Native Code一样都引入了rtcMsgBox(编译生成的p-code在调用MsgBox时应该比在IDE环境中运行的p-code快)。 如果你迫不及待地运行了程序2,你就会发现它将弹出一个应用程序错误对话框,说程序发生异常。别怕,这是因为调用了DebugBreak这个API的缘 故,这个API其实就是产生一个Int 3中断,使得我们能够中断程序执行。如果你装了VC这样的支持即时调试的调试器,你可以在错误对话框中点击"取消",这样可以起动调试器来调试程序。我就 是这样跟踪程序运行的。如果你想看看VB生成的程序反汇编代码可以自己试试,我们可以用同样的技术在VB或VBA的IDE中来中断程序执行,比如我们完全 可以在Word的VB编辑器中运行上面程序2的代码,从而中断于Word的进程中,并可观察到VBA生成的p-code代码。比如VB和VBA在IDE中 生成的p-code代码就会发现它们这间有很大的不同。 所以,IDE中运行的程序和最终生成的程序是完全不同的。用SPY++看看你在IDE中运行的窗体,你会发现它在VB的主线程下,也就是说在IDE中你用 程序做出的窗体和VB IDE工作窗口一样属于VB IDE,你的程序在IDE中运行时申请的资源也属于VB IDE。有些程序在IDE中运行会让IDE死掉(在VB5中写纯API多线程就千万别在IDE中运行,定死无疑,相比之下VB6的IDE健壮得多)。还有 些程序可能在IDE中能正常工作,但生成EXE后就工作不了。总之,在写系统程序时要考虑到这种不同可能引起的问题。 3)VB的编译技术,要我怎么夸你,又要我怎么骂你。 看了上面对Native Code的高度评价,你可能会对VB做出的东西更有信心了,腰板更直了。是的,作为VB程序员没有什么需要害羞的,一个功力深厚的VB程序员理应拿比普通 VC程序员更多的工资,因为他的生产力是VC程序员的好几倍,而做出的程序在质量上和VC做的相差无几。 甚至有大师开玩笑说VB的内置对象就是用VB写出的,比如我们可以自己写Form.cls、Label.ctl,呵呵,我们还真不能排除这种可能性(虽然用VB不可能直接生成vb6.olb)。如果真是这样,看来VB小组自己都对自己的编译优化技术非常有信心。 实际上我们看看VB安装目录下的C2.exe的属性,再看看VC的C2.DLL的属性,就会发现它们是同一个东西,同样Link.exe也是VC的,所以 我们完全可以对VB程序的后端优化编译器以及联结放心了。它们根本就是VC开发小组东西,或者VB、VC都是同一个编译器开发小组在做编译模块。总之,我 们可以壮着胆说我们VB做的程序其二次优化和联结用的是和VC一样的技术,嘿嘿,你有的我也有,我有的你没有的(纯属诡辩)。 还有,没有任何编译器比VB编译器更快,因为在IDE中VB就是一种解释型语言,这才是VB开发效率高的关键,快得几乎感觉不得编译过程。其请求时编译,后台编译技术更是一只独秀,厉害啊!想想看,别的语言的程序员有多少时间花在了等待代码编译和重新联结上啊! 不要高兴得太早,因为最终的目的还是要生成可执行文件。在VB中没有分块编译和增量联结的功能,VB在生成可执行程序时总是编译所有模块并完全重新联结, 而在别的编译语言中我们可以仅编译最近修改过的文件(分块编译),联结时将新生成的代码附在可执行程序的后面,并将原来的代吗标记为作废(增量联结,最终 的可执行程序会越来越大,但联结时间大大缩短)。做实验看看,会发现在VB中每次生成可执行文件所花时间都是相同的。我不知VB开发小组为什么不提供分块 编译和增量联结的功能,可能VB开发小组认为生成可执行文件在VB中不是经常要做的工作。但是实际上这种理由是说不过去的,因为如前面所说IDE中运行程 序和最终程序有很大不同,如我们要经常编译出可执行文件才能真正对它进行Profile,又如我们要调试多线程程序不能在VB IDE中做,在这些情况下每次修改后都要重新生成可执行文件,我们浪费了不少时间去编译已编译过的代码,联结已联结过的程序。我猜想这是因为VB生成可执 行程序时进行了全局优化,所以必须得全部重新编译联结。但提供一个新的功能让我们能够生成不进行全局优化的可以分块编译的调试版本,对Vb开发小组应该不 是难事吧!(我有一个变通的解决方案,还在试验中) 在来看看VB6安装目录下的VBAEXE6.lib,怎么只有1k大一点,可以猜想里 面应该不会有代码,多半是些象vTable这样的函数地址跳转表,或者是些全局常量,我也不知道。但至少说明VB可以用静态联结库了,为什么不把这个功能 提供给我们,让我们有更多的选择。 再做个实验看看,做一个标准EXE工程,里面只有一个标准模块,模块里面只一个Sub Main,Sub Main里面什么也没有,将它生成为EXE文件。看看,嚯,有16k多。你要是有时间跟踪这个什么也不做的程序看看,就会知道它要做很多事,初始化Err 和App对象,准备COM调用,准备VB、VBA对象库,甚至为使用ActiveX控制也做了准备,嘿嘿,看服务多周到。你必须得用VB对象库中的控制, 不用也不行。你再多找几个EXE工程看看,有很多东西相同,都是一个模子做出的,而且你没有选择模子自由。ActiveX工程也是一样,都是Dual双接 口,你做的ActiveX控制都必须要躲在一个Extender Object后面。是的,在VB里有很多东西你没有选择的自由。如果需要这种自由要么不用VB,要么就得采取一些未公开的非官方的古怪的技巧(本系列文章 最重要的目的之一,就是介绍这样的非官方技巧)。 这又到文章开头说的,VB让我们做事情变得容易的同时也让我们失去了不少自由。在最终代码的生成上则也采取了公式化的做法。当然,我们应该全面地来看待这 个问题,如同生产线上生产的东西不一定比手工的精致,群养的家禽不如野味好吃的道理一样,如果需要精致的野味,意味着更多的劳动和更大的成本,这和VB所 追求的更容易更便宜的目标是相违背的。 4)VB程序员也得有HACK精神。 本文的最后这个标题是严重离题了,但我想在此为本系列文章定下一个充满HACK精神的基调。HACK精神是什么?没有准确的定义,我的理解是:HACK精 神 = 总想探寻未知领域的好奇心 + 凡事总想知道为什么的研究欲 + 总想拿出自己的东西的创新精神 + 解决问题的耐心和恒心。 VB的程序员也一样需要这种精神。 最后,我们都知道VB开发小组已经达上.NET的快车飞起来了,不能不说VB6以后再没有VB的新版本了。微软已经用.NET为我们划出了新的圈子, VB.NET是这个新圈子里的新产物。在圈子里面我们能够飞得更高,但是圈子外面的天空更大,所以我依然乐意站在圈子外,虔诚地祈祷真正的VB7的诞生, 阿门。 VB真是想不到系列之二:VB《葵花宝典》--指针技术关键字:VB、指针、动态内存分配、效率、安全 难度:中级至高级 要求:熟悉VB,掌握基本的C,了解汇编,了解内存分配原理。 想当年东方不败,黑木崖密室一战,仅凭一根绣花针独战四大高手,神出鬼没,堪称天下武林第一高手。若想成为VB里的东方不败,熟习VB《葵花宝典》,掌握VB指针技术,乃是不二的法门。 欲练神功,引刀……,其实掌握VB指针技术,并不需要那么痛苦。因为说穿了,也就那么几招,再勤加练习,终可至神出鬼没之境。废话少说,让我们先从指针的定义说起。 一、指针是什么? 不需要去找什么标准的定义,它就是一个32位整数,在C语言和在VB里都可以用Long类型来表示。在32位Windows平台下它和普通的32位长整型 数没有什么不同,只不过它的值是一个内存地址,正是因为这个整数象针一样指向一个内存地址,所以就有了指针的概念。 有统计表明,很大一部分程序缺陷和内存的错误访问有关。正是因为指针直接和内存打交道,所以指针一直以来被看成一个危险的东西。以至于不少语言,如著名的 JAVA,都不提供对指针操作的支持,所有的内存访问方面的处理都由编译器来完成。而象C和C++,指针的使用则是基本功,指针给了程序员极大的自由去随 心所欲地处理内存访问,很多非常巧妙的东西都要依靠指针技术来完成。 关于一门高级的程序设计语言是不是应该取消指针操作,关于没有指针操作算不算一门语言的优点,我在这里不讨论,因为互联网上关于这方面的没有结果的讨论, 已经造成了占用几个GB的资源。无论最终你是不是要下定决心修习指针技术《葵花宝典》,了解这门功夫总是有益处的。 注意:在VB里,官方是不鼓励使用什么指针的,本文所讲的任何东西你都别指望取得官方的技术支持,一切都要靠我们自己的努力,一切都更刺激! 让我们开始神奇的VB指针探险吧! 二、来看看指针能做什么?有什么用? 先来看两个程序,程序的功能都是交换两个字串: 【程序一】:注释:标准的做法SwapStr Sub SwapStr(sA As String, sB As String) Dim sTmp As String sTmp = sA: sA = sB: sB = sTmp End Sub 【程序二】:注释:用指针的做法SwapPtr Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" _ (Destination As Any, Source As Any, ByVal Length As Long) Sub SwapPtr(sA As String, sB As String) Dim lTmp As Long CopyMemory lTmp, ByVal VarPtr(sA), 4 CopyMemory ByVal VarPtr(sA), ByVal VarPtr(sB), 4 CopyMemory ByVal VarPtr(sB), lTmp, 4 End Sub 你是不是以为第一个程序要快,因为它看着简单而且不用调用API(调用API需要额外的处理,VB文档明确指出大量调用API将降低程序性能)。但事实 上,在VB集成环境中运行,程序二要比程序一快四分之一;而编译成本机代码或p-code,程序二基本上要比程序一快一倍。下面是两个函数在编译成本机代 码后,运行不同次数所花时间的比较: 运行100000次,SwapStr需要170毫秒,SwapPtr需要90毫秒。 运行200000次,SwapStr需要340毫秒,SwapPtr需要170毫秒。 运行2000000次,SwapStr需要3300毫秒,SwapPtr需要1500毫秒。 的确,调用API是需要额外指令来处理,但是由于使用了指针技术,它没有进行临时字串的分配和拷贝,因此速度提高了不少。 怎么样,想不到吧!C/C++程序员那么依赖指针,无非也是因为使用指针往往能更直接的去处理问题的根源,更有驾驭一切的快感。他们不是不知道使用指针的危险,他们不是不愿意开卫星定位无级变速的汽车,只是骑摩托更有快感,而有些地方只有摩托才走得过去。 和在C里类似,在VB里我们使用指针也不过三个理由: 一是效率,这是一种态度一种追求,在VB里也一样; 二是不能不用,因为操作系统是C写的,它时刻都在提醒我们它需要指针; 三是突破限制,VB想照料我们的一切,VB给了我们很强的类型检查,VB象我们老妈一样,对我们关心到有时我们会受不了,想偶尔不听妈妈的话吗?你需要指针! 但由于缺少官方的技术支持,在VB里,指针变得很神秘。因此在C里一些基本的技术,在VB里就变得比较困难。本文的目的就是要提供给大家一种简单的方法, 来将C处理指针的技术拿到VB里来,并告诉你什么是可行的,什么可行但必须要小心的,什么是可能但不可行的,什么是根本就不可能的。 三、 程咬金的三板斧 是的,程序二基本上就已经让我们看到VB指针技术的模样了。总结一下,在VB里用指针技术我们需要掌握三样东西:CopyMemory,VarPtr/StrPtr/ObjPtr, AdressOf. 三把斧头,程咬金的三板斧,在VB里Hack的工具。 1、CopyMemory 关于CopyMemory和Bruce McKinney大师的传奇,MSDN的Knowledge Base中就有文章介绍,你可以搜索"ID: Q129947"的文章。正是这位大师给32位的VB带来了这个可以移动内存的API,也正是有了这个API,我们才能利用指针完成我们原来想都不敢想的 一些工作,感谢Bruce McKinney为我们带来了VB的指针革命。 如CopyMemory的声明,它是定义在Kernel32.dll中的RtlMoveMemory这个API,32位C函数库中的memcpy就是这个 API的包装,如MSDN文档中所言,它的功能是将从Source指针所指处开始的长度为Length的内存拷贝到Destination所指的内存处。 它不会管我们的程序有没有读写该内存所应有的权限,一但它想读写被系统所保护的内存时,我们就会得到著名的Access Violation Fault(内存越权访问错误),甚至会引起更著名的general protection (GP) fault(通用保护错误) 。所以,在进行本系列文章里的实验时,请注意随时保存你的程序文件,在VB集成环境中将"工具"->"选项"中的"环境"选项卡里的"启动程序时" 设为"保存改变",并记住在"立即"窗口中执行危险代码之前一定要保存我们的工作成果。 2、VatPtr/StrPtr/ObjPtr 它们是VB提供给我们的好宝贝,它们是VBA函数库中的隐藏函数。为什么要隐藏?因为VB开发小组,不鼓励我们用指针嘛。 实际上这三个函数在VB运行时库MSVBVM60.DLL(或MSVBVM50.DLL)中是同一个函数VarPtr(可参见我在本系列第一篇文章里介绍的方法)。 其库型库定义如下: [entry("VarPtr"), hidden] long _stdcall VarPtr([in] void* Ptr); [entry("VarPtr"), hidden] long _stdcall StrPtr([in] BSTR Ptr); [entry("VarPtr"), hidden] long _stdcall ObjPtr([in] IUnknown* Ptr); 即然它们是VB运行时库中的同一个函数,我们也可以在VB里用API方式重新声明这几个函数,如下: Private Declare Function ObjPtr Lib "MSVBVM60" Alias "VarPtr" _ (var As Object) As Long Private Declare Function VarPtr Lib "MSVBVM60" _ (var As Any) As Long (没有StrPtr,是因为VB对字符串处理方式有点不同,这方面的问题太多,在本系列中另用一篇《VB字符串全攻略》来详谈。 顺便提一下,听说VB.NET里没有这几个函数,但只要还能调用API,我们就可以试试上面的几个声明,这样在VB.NET里我们一样可以进行指针操作。 但是请注意,如果通过API调用来使用VarPtr,整个程序二SwapPtr将比原来使用内置VarPtr函数时慢6倍。) 如果你喜欢刨根问底,那么下面就是VarPtr函数在C和汇编语言里的样子: 在C里样子是这样的: long VarPtr(void* pv){ return (long)pv; } 所对就的汇编代码就两行: mov eax,dword ptr [esp+4] ret 4 注释:弹出栈里参数的值并返回。 之所以让大家了解VarPtr的具体实现,是想告诉大家它的开销并不大,因为它们不过两条指令,即使加上参数赋值、压栈和调用指令,整个获取指针的过程也 就六条指令。当然,同样的功能在C语言里,由于语言的直接支持,仅需要一条指令即可。但在VB里,它已经算是最快的函数了,所以我们完全不用担心使用 VarPtr会让我们失去效率!速度是使用指针技术的根本要求。 一句话,VarPtr返回的是变量所在处的内存地址,也可以说返回了指向变量内存位置的指针,它是我们在VB里处理指针最重要的武器之一。 3、ByVal和ByRef ByVal传递的参数值,而ByRef传递的参数的地址。在这里,我们不用去区别传指针/传地址/传引用的不同,在VB里,它们根本就是一个东西的三种不同说法,即使VB的文档里也有地方在混用这些术语(但在C++里的确要区分指针和引用) 初次接触上面的程序二SwapPtr的朋友,一定要搞清在里面的CopyMemory调用中,在什么地方要加ByVal,什么地方不加(不加ByVal就是使用VB缺省的ByRef) 准确的理解传值和传地址(指针)的区别,是在VB里正确使用指针的基础。 现在一个最简单的实验来看这个问题,如下面的程序三: 【程序三】:注释:体会ByVal和ByRef Sub TestCopyMemory() Dim k As Long k = 5 Note: CopyMemory ByVal VarPtr(k), 40000, 4 Debug.Print k End Sub 上面标号Note处的语句的目的,是将k赋值为40000,等同于语句k=40000,你可以在"立即"窗口试验一下,会发现k的值的确成了40000。 实际上上面这个语句,翻译成白话,就是从保存常数40000的临时变量处拷贝4个字节到变量k所在的内存中。 现在我们来改变一个Note处的语句,若改成下面的语句: Note2: CopyMemory ByVal VarPtr(k), ByVal 40000, 4 这句话的意思就成了,从地址40000拷贝4个字节到变量k所在的内存中。由于地址40000所在的内存我们无权访问,操作系统会给我们一个Access Violation内存越权访问错误,告诉我们"试图读取位置0x00009c40处内存时出错,该内存不能为注释:Read注释:"。 我们再改成如下的语句看看。 Note3: CopyMemory VarPtr(k), 40000, 4 这句话的意思就成了,从保存常数40000的临时变量处拷贝4个字节到到保存变量k所在内存地址值的临时变量处。这不会出出内存越权访问错误,但k的值并没有变。 我们可以把程序改改以更清楚的休现这种区别,如下面的程序四: 【程序四】:注释:看看我们的东西被拷贝到哪儿去了 Sub TestCopyMemory() Dim i As Long, k As Long k = 5 i = VarPtr(k) NOTE4: CopyMemory i, 40000, 4 Debug.Print k Debug.Print i i = VarPtr(k) NOTE5: CopyMemory ByVal i, 40000, 4 Debug.Print k End Sub 程序输出: 5 40000 40000 由于NOTE4处使用缺省的ByVal,传递的是i的地址(也就是指向i的指针),所以常量40000拷贝到了变量i里,因此i的值成了40000,而k 的值却没有变化。但是,在NOTE4前有:i=VarPtr(k),本意是要把i本身做为一个指针来使用。这时,我们必须如NOTE5那样用ByVal来 传递指针i,由于i是指向变量k的指针,所以最后常量40000被拷贝了变量k里。 希望你已经理解了这种区别,在后面问题的讨论中,我还会再谈到它。 4、AddressOf 它用来得到一个指向VB函数入口地址的指针,不过这个指针只能传递给API使用,以使得API能回调VB函数。 本文不准备详细讨论函数指针,关于它的使用请参考VB文档。 5、拿来主义。 实际上,有了CopyMemory,VarPtr,AddressOf这三把斧头,我们已经可以将C里基本的指针操作拿过来了。 如下面的C程序包括了大部分基本的指针指针操作: struct POINT{ int x; int y; }; int Compare(void* elem1, void* elem2){} void PtrDemo(){ //指针声明: char c = 注释:X注释:; //声明一个char型变量 char* pc; long* pl; //声明普通指针 POINT* pPt; //声明结构指针 void* pv; //声明无类型指针 int (*pfnCastToInt)(void *, void*);//声明函数指针: //指针赋值: pc = &c; //将变量c的地址值赋给指针pc pfnCompare = Compare; //函数指针赋值。 //指针取值: c = *pc; //将指针pc所指处的内存值赋给变量c //用指针赋值: *pc = 注释:Y注释: //将注释:Y注释:赋给指针pc所指内存变量里。 //指针移动: pc++; pl--; } 这些对指针操作在VB里都有等同的东西, 前面讨论ByVal和ByRef时曾说过传指针和传地址是一回事,实际上当我们在VB里用缺省的ByRef声明函数参数时,我们已经就声明了指针。 如一个C声明的函数:long Func(char* pc) 其对应的VB声明是:Function Func(pc As Byte) As Long 这时参数pc使用缺省的ByRef传地址方式来传递,这和C里用指针来传递参数是一样。 那么怎么才能象C里那样明确地声明一个指针呢? 很简单,如前所说,用一个32位长整数来表达指针就行。在VB里就是用Long型来明确地声明指针,我们不用区分是普通指针、无类型指针还是函数指针,通 通都可用Long来声明。而给一个指针赋值,就是赋给它用VarPar得到的另一个变量的地址。具体见程序五。 【程序五】:同C一样,各种指针。 Type POINT X As Integer Y As Integer End Type Public Function Compare(elem1 As Long, elem2 As Long) As Long 注释: End Function Function FnPtrToLong(ByVal lngFnPtr As Long) As Long FnPtrToLong = lngFnPtr End Function Sub PtrDemo() Dim l As Long, c As Byte, ca() As Byte, Pt As POINT Dim pl As Long, pc As Long, pv As Long, pPt As Long, pfnCompare As Long c = AscB("X") pl = VarPtr(l) 注释:对应C里的long、int型指针 pc = VarPtr(c) 注释:对应char、short型指针 pPt = VarPtr(Pt) 注释:结构指针 pv = VarPtr(ca(0)) 注释:字节数组指针,可对应任何类型,也就是void* pfnCompare = FnPtrToLong(AddressOf Compare) 注释:函数指针 CopyMemory c, ByVal pc, LenB(c) 注释:用指针取值 CopyMemory ByVal pc, AscB("Y"), LenB(c) 注释:用指针赋值 pc = pc + LenB(c) : pl = pl - LenB(l) 注释:指针移动 End Sub 我们看到,由于VB不直接支持指针操作,在VB里用指针取值和用指针赋值都必须用CopyMemory这个API,而调用API的代价是比较高的,这就决 定了我们在VB里使用指针不能象在C里那样自由和频繁,我们必须要考虑指针操作的代价,在后面的"指针应用"我们会再变谈这个问题。 程序五中关于函数指针的问题请参考VB文档,无类型指针void*会在下面"关于Any的问题"里说。 程序五基本上已经包括了我们能在VB里进行的所有指针操作,仅此而已。 下面有一个小测试题,如果现在你就弄懂了上面程咬金的三板斧,你就应该能做得出来。 上面提到过,VB.NET中没有VarPtr,我们可以用声明API的方式来引入MSVBVM60.DLL中的VarPtr。现在的问题如果不用VB的运行时DLL文件,你能不能自己实现一个ObjPtr。答案在下一节后给出。 四、指针使用中应注意的问题 1、关于ANY的问题 如果以一个老师的身份来说话,我会说:最好永远也不要用Any!是的,我没说错,是永远!所以我没有把它放在程咬金的三板斧里。当然,这个问题和是不是应 该使用指针这个问题一样会引发一场没有结果的讨论,我告诉你的只是一个观点,因为有时我们会为了效率上的一点点提高或想偷一点点懒而去用Any,但这样做 需要要承担风险。 Any不是一个真正的类型,它只是告诉VB编译器放弃对参数类型的检查,这样,理论上,我们可以将任何类型传递给API。 Any在什么地方用呢?让我们来看看,在VB文档里的是怎么说的,现在就请打开MSDN(Visual Studio 6自带的版本),翻到"Visual Basic文档"->"使用Visual Basic"->"部件工具指南"->"访问DLL和Windows API"部分,再看看"将 C 语言声明转换为 Visual Basic 声明"这一节。文档里告诉我们,只有C的声明为LPVOID和NULL时,我们才用Any。实际上如果你愿意承担风险,所有的类型你都可以用Any。当 然,也可以如我所说,永远不要用Any。 为什么要这样?那为什么VB官方还要提供Any?是信我的,还是信VB官方的?有什么道理不用Any? 如前面所说,VB官方不鼓励我们使用指针。因为VB所标榜的优点之一,就是没有危险的指针操作,所以的内存访问都是受VB运行时库控制的。在这一点上, JAVA语言也有着同样的标榜。但是,同JAVA一样,VB要避免使用指针而得到更高的安全性,就必须要克服没有指针而带来的问题。VB已经尽最大的努力 来使我们远离指针的同时拥有强类型检查带来的安全性。但是操作系统是C写的,里面到处都需要指针,有些指针是没有类型的,就是C程序员常说的可怕的 void*无类型指针。它没有类型,因此它可以表示所有类型。如CopyMemory所对应的是C语言的memcpy,它的声明如下: void *memcpy( void *dest, const void *src, size_t count ); 因memcpy前两个参数用的是void*,因此任何类型的参数都可以传递给他。 一个用C的程序员,应该知道在C函数库里这样的void*并不少见,也应该知道它有多危险。无论传递什么类型的变量指针给上面memcpy的void*,C编译器都不会报错或给任何警告。 在VB里大多数时候,我们使用Any就是为了使用void*,和在C里一样,VB也不对Any进行类型检查,我们也可以传递任何类型给Any,VB编译器也都不会报错或给任何警告。 但程序运行时会不会出错,就要看使用它时是不是小心了。正因为在C里很多错误是和void*相关的,所以,C++鼓励我们使用satic_cast来明确指出这种不安全的类型的转换,已利于发现错误。 说了这么多C/C++,其实我是想告诉所有VB的程序员,在使用Any时,我们必须和C/C++程序员使用void*一样要高度小心。 VB里没有satic_cast这种东西,但我们可以在传递指针时明确的使用long类型,并且用VarPtr来取得参数的指针,这样至少已经明确地指出我们在使用危险的指针。如程序二经过这样的处理就成了下面的程序: 【程序五】:注释:使用更安全的CopyMemory,明确的使用指针! Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (ByVal Destination As Long, ByVal Source As Long, ByVal Length As Long) Sub SwapStrPtr2(sA As String, sB As String) Dim lTmp As Long Dim pTmp As Long, psA As Long, psB As Long pTmp = VarPtr(lTmp): psA = VarPtr(sA): psB = VarPtr(sB) CopyMemory pTmp, psA, 4 CopyMemory psA, psB, 4 CopyMemory psB, pTmp, 4 End Sub 注意,上面CopyMemory的声明,用的是ByVal和long,要求传递的是32位的地址值,当我们将一个别的类型传递给这个API时,编译器会报错,比如现在我们用下面的语句: 【程序六】:注释:有点象【程序四】,但将常量40000换成了值为1的变量. Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (ByVal Destination As Long, ByVal Source As Long, Length As Long) Sub TestCopyMemory() Dim i As Long,k As Long, z As Interger k = 5 : z = 1 i = VarPtr(k) 注释:下面的语句会引起类型不符的编译错误,这是好事! 注释:CopyMemory i, z, 4 注释:应该用下面的 CopyMemory i, ByVal VarPtr(z), 2 Debug.Print k End Sub 编译会出错!是好事!这总比运行时不知道错在哪儿好! 象程序四那样使用Any类型来声明CopyMemory的参数,VB虽然不会报错,但运行时结果却是错的。不信,你试试将程序四中的40000改为1,结 果i的值不是我们想要的1,而是327681。为什么在程序四中,常量为1时结果会出错,而常量为40000时结果就不错? 原因是VB对函数参数中的常量按Variant的方式处理。是1时,由于1小于Integer型的最大值32767,VB会生成一个存储值1的 Integer型的临时变量,也就是说,当我们想将1用CopyMemroy拷贝到Long型的变量i时,这个常量1是实际上是Integer型临时变 量!VB里Integer类型只有两个字节,而我们实际上拷贝了四个字节。知道有多危险了吧!没有出内存保护错误那只是我们的幸运! 如果一定要解释一下为什么i最后变成了327681,这是因为我们将k的低16位的值5也拷贝到了i值的高16位中去了,因此有5*65536+1= 327681。详谈这个问题涉及到VB局部变量声明顺序,CopyMemory参数的压栈顺序,long型的低位在前高位在后等问题。如果你对这些问题感 兴趣,可以用本系列第一篇文章所提供的方法(DebugBreak这个API和VC调试器)来跟踪一下,可以加深你对VB内部处理方式的认识,由于这和本 文讨论的问题无关,所以就不详谈了。到这里,大家应该明白,程序三和程序四实际上有错误!!!我在上面用常量40000而不用1,不是为了在文章中凑字 数,而是因为40000这个常量大于32767,会被VB解释成我们需要的Long型的临时变量,只有这样程序三和程序四才能正常工作。对不起,我这样有 意的隐藏错误只是想加深你对Any危害的认识。 总之,我们要认识到,编译时就找到错误是非常重要的,因为你马上就知道错误的所在。所以我们应该象程序五和程序六那样明确地用long型的ByVal的指针,而不要用Any的ByRef的指针。 但用Any已经如此的流行,以至很多大师们也用它。它唯一的魅力就是不象用Long型指针那样,需要我们自己调用VarPtr来得到指针,所有处理指针的 工作由VB编译器来完成。所以在参数的处理上,只用一条汇编指令:push [i],而用VarPtr时,由于需要函数调用,因此要多用五条汇编指令。五条多余的汇编指令有时的确能我们冒着风险去用Any。 VB开发小组提供Any,就是想用ByRef xxx As Any来表达void* xxx。我们也完全可以使用VarPtr和Long型的指针来处理。我想,VB开发小组也曾犹豫过是公布VarPtr,还是提供Any,最后他们决定还是 提供Any,而继续隐瞒VarPtr。的确,这是个两难的决定。但是经过我上面的分析,我们应该知道,这个决定并不符合VB所追求的"更安全"的初衷。因 为它可能会隐藏类型不符的错误,调试和找到这种运行时才产生的错误将花贵更多的时间和精力。 所以我有了"最好永远不要用Any"这个"惊人"的结论。 不用Any的另一个好处是,简化了我们将C声明的API转换成VB声明的方式,现在它变成了一句话:除了VB内置的可以进行类型检查的类型外,所以其它的类型我们都应该声明成Long型。 2、关于NULL的容易混淆的问题 有很多文章讲过,一定要记在心里: VbNullChar 相当于C里的注释:/0注释:,在用字节数组构造C字串时常用它来做最后1个元素。 vbNullString 这才是真正的NULL,就是0,在VB6中直接用0也可以。 只有上面的两个是API调用中会用的。还有Empty、Null是Variant,而Nothing只和类对象有关,一般API调用中都不会用到它们。 另:本文第三节曾提出一个小测验题,做出来了吗?现在公布正确答案: 【测验题答案】 Function ObjPtr(obj as Object) as long Dim lpObj As Long CopyMemory lpObj, Obj, 4 ObjectPtr = lpObj End Function 五、VB指针应用 如前面所说VB里使用指针不象C里那样灵活,用指针处理数据时都需要用CopyMemory将数据在指针和VB能够处理的变量之间来回拷贝,这需要很大的额外开销。因此不是所有C里的指针操作都可以移值到VB里来,我们只应在需要的时候才在VB里使用指针。 1、动态内存分配:完全不可能、可能但不可行,VB标准 在C和C++里频繁使用指针的一个重要原因是需要使用动态内存分配,用Malloc或New来从堆栈里动态分配内存,并得到指向这个内存的指针。在VB里我们也可以自己 用API来实现动态分配内存,并且实现象C里的指针链表。 但我们不可能象C那样直接用指针来访问这样动态分配的内存,访问时我们必须用CopyMemory将数据拷贝到VB的变量内,大量的使用这种技术必然会降 低效率,以至于要象C那样用指针来使用动态内存根本就没有可行性。要象C、PASCAL那样实现动态数据结构,在VB里还是应该老老实实用对象技术来实 现。 本文配套代码中的LinkedList里有完全用指针实现的链表,它是使用HeapAlloc从堆栈中动态分配内存,另有一个调用 FindFirstUrlCacheEntry这个API来操作IE的Cache的小程序IECache,它使用了VirtualAlloc来动态分配内 存。但实际上这都不是必须的,VB已经为我们提供了标准的动态内存分配的方法,那就是: 对象、字符串和字节数组 限于篇幅,关于对象的技术这里不讲,LinkedList的源代码里有用对象实现的链表,你可以参考。 字符串可以用Space$函数来动态分配,VB的文档里就有详细的说明。 关于字节数组,这里要讲讲,它非常有用。我们可用Redim来动态改变它的大小,并将指向它第一个元素的指针传给需要指针的API,如下: dim ab() As Byte , ret As long 注释:传递Null值API会返回它所需要的缓冲区的长度。 ret = SomeApiNeedsBuffer(vbNullString) 注释:动态分配足够大小的内存缓冲区 ReDim ab(ret) As Byte 注释:再次把指针传给API,此时传字节数组第一个元素的指针。 SomeApiNeedsBuffer(ByVal VarPtr(ab(1))) 在本文配套程序中的IECache中,我也提供了用字节数组来实现动态分配缓冲区的版本,比用VirtualAlloc来实现更安全更简单。 2、突破限制 下面是一个突破VB类型检查来实现特殊功能的经典应用,出自Bruce Mckinney的《HardCore Visual Basic》一书。 将一个Long长整数的低16位作为Interger型提取出来, 【程序七】 注释:标准的方法,也是高效的方法,但不容易理解。 Function LoWord(ByVal dw As Long) As Integer If dw And &H8000& Then LoWord = dw Or &HFFFF0000 Else LoWord = dw And &HFFFF& End If End Function 【程序八】 注释:用指针来做效率虽不高,但思想清楚。 Function LoWord(ByVal dw As Long) As Integer CopyMemory ByVal VarPtr(LoWord), ByVal VarPtr(dw), 2 End Function 3、对数组进行批量操作 用指针进行大批量数组数据的移动,从效率上考虑是很有必要的,看下面的两个程序,它们功能都是将数组的前一半数据移到后一半中: 【程序九】:注释:标准的移动数组的做法 Private Sub ShitArray(ab() As MyType) Dim i As Long, n As Long n = CLng(UBound(ab) / 2) For i = 1 To n Value(n + i) = Value(i) Value(i).data = 0 Next End Sub 【程序十】:注释:用指针的做法 Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" _ (ByVal dest As Long, ByVal source As Long, ByVal bytes As Long) Private Declare Sub ZeroMemory Lib "kernel32" Alias "RtlZeroMemory" _ (ByVal dest As Long, ByVal numbytes As Long) Private Declare Sub FillMemory Lib "kernel32" Alias "RtlFillMemory" _ (ByVal dest As Long, ByVal Length As Long, ByVal Fill As Byte) Private Sub ShitArrayByPtr(ab() As MyTpye) Dim n As Long n = CLng(UBound(ab) / 2) Dim nLenth As Long nLenth = Len(Value(1)) 注释:DebugBreak CopyMemory ByVal VarPtr(Value(1 + n)), _ ByVal VarPtr(Value(1)), n * nLenth ZeroMemory ByVal VarPtr(Value(1)), n * nLenth End Sub 当数组较大,移动操作较多(比如用数组实现HashTable)时程序十比程序九性能上要好得多。 程序十中又介绍两个在指针操作中会用到的API: ZeroMemory是用来将内存清零;FillMemory用同一个字节来填充内存。当然,这两个API的功能,也完全可以用CopyMemory来完 成。象在C里一样,作为一个好习惯,在VB里我们也可以明确的用ZeroMemory来对数组进行初始化,用FillMemory在不立即使用的内存中填 入怪值,这有利于调试。 4、最后的一点 当然,VB指针的应用决不止这些,还有什么应用就要靠自己去摸索了。对于对象指针和字符串指针的应用我会另写文章来谈,做为本文的结束和下一篇文章《VB字符串全攻略》的开始,我在这里给出交换两个字符串的最快的方法: 【程序十一】注释:交换两个字符串最快的方法 Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" _ (Destination As Any, Source As Any, ByVal Length As Long) Sub SwapStrPtr3(sA As String, sB As String) Dim lTmp As Long Dim pTmp As Long, psA As Long, psB As Long pTmp = StrPtr(sA): psA = VarPtr(sA): psB = VarPtr(sB) CopyMemory ByVal psA, ByVal psB, 4 CopyMemory ByVal psB, pTmp, 4 End Sub 对不起,为了一点点效率,又用了Any!关于StrPtr,下一篇文章我会来谈。 自己来试试吧!欲练神功,赶快行动! VB真是想不到系列之三:VB指针葵花宝典之函数指针关键字:VB、HCAK、指针、函数指针、效率、数组、对象、排序 难度:中级 要求:熟悉VB,了解基本的排序算法,会用VC更好。引言: 不知大家在修习过本系列第二篇《VB指针葵花宝典》后有什么感想,是不是觉得宝典过于偏重内功心法,而少了厉害的招式。所以,今天本文将少讲道理,多讲招式。不过,还是请大家从名门正派的内功心法开始学起,否则会把九阴真经练成九阴白骨爪。 今天,我们重点来谈谈函数指针的实际应用。 接着上一篇文章,关于字串的问题,听CSDN上各位网友的建议,我不准备写什么《VB字符串全攻略》了,关于BSTR的结构,关于调用API时字串在 UNICODE和ANSI之间的转换问题,请参考MSDN的Partial Books里的《Win32 API Programming with Visual Basic》里的第六章《Strings》。今天就让我们先忘掉字符串,专注于函数指针的处理上来。 一、函数指针 AddressOf得到一个VB内部的函数指针,我们可以将这个函数指针传递给需要回调这个函数的API,它的作用就是让外部的程序可以调用VB内部的函数。 但是VB里函数指针的应用,远不象C里应用那么广泛,因为VB文档里仅介绍了如何将函数指针传递给API以实现回调,并没指出函数指针诸多神奇的功能,因为VB是不鼓励使用指针的,函数指针也不例外。 首先让我们对函数指针的使用方式来分个类。 1、回调。这是最基本也是最重要的功能。比如VB文档里介绍过的子类派生技术,它的核心就是两个API:SetWindowLong和CallWindowProc。 我们可以使SetWindowLong这个API来将原来的窗口函数指针换成自己的函数指针,并将原来的窗口函数指针保存下来。这样窗口消息就可以发到我 们自己的函数里来,并且我们随时可以用CallWindowProc来调用前面保存下来的窗口指针,以调用原来的窗口函数。这样,我们可以在不破坏原有窗 口功能的前提下处理钩入的消息。 具体的处理,我们应该很熟悉了,VB文档也讲得很清楚了。这里需要注意的就是CallWindowProc这个API,在后面我们将看到它的妙用。 在这里我们称回调为让"外部调用内部的函数指针"。 2、程序内部使用。比如在C里我们可以将C函数指针作为参数传递给一个需要函数指针的C函数,如后面还要讲到的C库函数qsort,它的声明如下: #define int (__cdecl *COMPARE)(const void *elem1, const void *elem2) void qsort(void *base, size_t num, size_t width, COMPARE pfnCompare); 它需要一个COMPARE类型函数指针,用来比较两个变量大小的,这样排序函数可以调用这个函数指针来比较不同类型的变量,所以qsort可以对不同类型的变量数组进行排序。 我们姑且称这种应用为"从内部调用内部的函数指针"。 3、调用外部的函数 也许你会问,用API不就是调用外部的函数吗?是的,但有时候我们还是需要直接获取外部函数的指针。比如通过LoadLibrary动态加载DLL,然后 再通过GetProcAddress得到我们需要的函数入口指针,然后再通过这个函数指针来调用外部的函数,这种动态载入DLL的技术可以让我们更灵活的 调用外部函数。 我们称这种方式为"从内部调用外部的函数指针" 4、不用说,就是我们也可控制"从外部调用外部的函数指针"。不是没有,比如我们可以加载多个DLL,将其中一个DLL中的函数指针传到另一个DLL里的函数内。 上面所分的"内"和"外"都是相对而言(DLL实际上还是在进程内),这样分类有助于以后我们谈问题,请记住我上面的分类,因为以后的文章也会用到这个分类来分析问题。 函数指针的使用不外乎上面四种方式。但在实际使用中却是灵活多变的。比如在C++里继承和多态,在COM里的接口,都是一种叫vTable的函数指针表的巧妙应用。使用函数指针,可以使程序的处理方式更加高效、灵活。 VB文档里除了介绍过第一方式外,对其它方式都没有介绍,并且还明确指出不支持“Basic 到 Basic”的函数指针(也就是上面说的第二种方式),实际上,通过一定的HACK,上面四种方式均可以实现。今天,我们就来看看如何来实现第二种方式, 因为实现它相对来说比较简单,我们先从简单的入手。至于如何在VB内调用外部的函数指针,如何在VB里通过处理vTable接口函数指针跳转表来实现各种 函数指针的巧妙应用,由于这将涉及COM内部原理,我将另文详述。 其实VB的文档并没有说错,VB的确不支持“Basic 到 Basic”的函数指针,但是我们可以绕个弯子来实现,那就是先从"Basic到API",然后再用第一种方式"外部调用内部的函数指针"来从"API到 BASIC",这样就达到了第二种方式从"Basic 到 Basic"的目的,这种技术我们可以称之为"强制回调",只有VB里才会有这种古怪的技术。 说得有点绕口,但是仔细想想窗口子类派生技术里CallWindowProc,我们可以用CallWindowProc来强制外部的操作系统调用我们原来的保存的窗口函数指针,同样我们也完全可以用它来强制调用我们内部的函数指针。 呵呵,前面说过要少讲原理多讲招式,现在我们就来开始学习招式吧! 考虑我们在VB里来实现和C里一样支持多关键字比较的qsort。完整的源代码见本文配套代码,此处仅给出函数指针应用相关的代码。 注释:当然少不了的CopyMemory,不用ANY的版本。 Declare Sub CopyMemory Lib "kernel32" Alias _ "RtlMoveMemory" (ByVal dest As Long, ByVal source As Long, _ ByVal numBytes As Long) 注释:嘿嘿,看下面是如何将CallWindowProc的声明做成Compare声明的。 Declare Function Compare Lib "user32" Alias _ "CallWindowProcA" (ByVal pfnCompare As Long, ByVal pElem1 As Long, _ ByVal pElem2 As Long, ByVal unused1 As Long, _ ByVal unused2 As Long) As Integer 注释:注:ByVal xxxxx As Long ,还记得吧!这是标准的指针声明方法。 注释:声明需要比较的数组元素的结构 Public Type TEmployee Name As String Salary As Currency End Type 注释:再来看看我们的比较函数 注释:先按薪水比较,再按姓名比较 Function CompareSalaryName(Elem1 As TEmployee, _ Elem2 As TEmployee, _ unused1 As Long, _ unused2 As Long) As Integer Dim Ret As Integer Ret = Sgn(Elem1.Salary - Elem2.Salary) If Ret = 0 Then Ret = StrComp(Elem1.Name, Elem2.Name, vbTextCompare) End If CompareSalaryName = Ret End Function 注释:先按姓名比较,再按薪水比较 Function CompareNameSalary(Elem1 As TEmployee, _ Elem2 As TEmployee, _ unused1 As Long, _ unused2 As Long) As Integer Dim Ret As Integer Ret = StrComp(Elem1.Name, Elem2.Name, vbTextCompare) If Ret = 0 Then Ret = Sgn(Elem1.Salary - Elem2.Salary) End If CompareNameSalary = Ret End Function 最后再看看我们来看看我们最终的qsort的声明。 Sub qsort(ByVal ArrayPtr As Long, ByVal nCount As Long, _ ByVal nElemSize As Integer, ByVal pfnCompare As Long) 上面的ArrayPtr是需要排序数组的第一个元素的指针,nCount是数组的元素个数,nElemSize是每个元素大小,pfnCompare就是我们的比较函数指针。这个声明和C库函数里的qsort是极为相似的。 和C一样,我们完全可以将Basic的函数指针传递给Basic的qsort函数。 使用方式如下: Dim Employees(1 To 10000) As TEmployee 注释:假设下面的调用对Employees数组进行了赋值初始化。 Call InitArray() 注释:现在就可以调用我们的qsort来进行排序了。 Call qsort(VarPtr(Employees(1)), UBound(Employees), _ LenB(Employees(1)), AddressOf CompareSalaryName) 注释:或者先按姓名排,再按薪水排 Call qsort(VarPtr(Employees(1)), UBound(Employees), _ LenB(Employees(1)), AddressOf CompareNameSalary) 聪明的朋友们,你们是不是已经看出这里的奥妙了呢?作为一个测验,你能现在就给出在qsort里使用函数指针的方法吗?比如现在我们要通过调用函数指针来比较数组的第i个元素和第j个元素的大小。 没错,当然要使用前面声明的Compare(其实就是CallWindowProc)这个API来进行强制回调。 具体的实现如下: Sub qsort(ByVal ArrayPtr As Long, ByVal nCount As Long, _ ByVal nElemSize As Integer, ByVal pfnCompare As Long) Dim i As Long, j As Long 注释:这里省略快速排序算法的具体实现,仅给出比较两个元素的方法。 If Compare(pfnCompare, ArrayPtr + (i - 1) * nElemSize, _ ArrayPtr + (j - 1) * nElemSize, 0, 0) > 0 Then 注释:如果第i个元素比第j个元素大则用CopyMemory来交换这两个元素。 End IF End Sub 招式介绍完了,明白了吗?我再来简单地讲解一下上面Compare的意思,它非常巧妙地利用了CallWindowProc这个API。这个API需要五 个参数,第一个参数就是一个普通的函数指针,这个API能够强马上回调这个函数指针,并将这个API的后四个Long型的参数传递给这个函数指针所指向的 函数。这就是为什么我们的比较函数必须要有四个参数的原因,因为CallWindowProc这个API要求传递给的函数指针必须符合WndProc函数 原形,WndProc的原形如下: LRESULT (CALLBACK* WNDPROC) (HWND, UINT, WPARAM, LPARAM); 上面的LRESULT、HWND、UINT、WPARAM、LPARAM都可以对应于VB里的Long型,这真是太好了,因为Long型可以用来作指针嘛! 再来看看工作流程,当我们用AddressOf CompareSalaryName做为函数指针参数来调用qsort时,qsort的形参pfnCompare被赋值成了实参 CompareSalaryName的函数指针。这时,调用Compare来强制回调pfnCompare,就相当于调用了如下的VB语句: Call CompareSalaryName(ArrayPtr + (i - 1) * nElemSize, _ ArrayPtr + (j - 1) * nElemSize, 0, 0) 这不会引起参数类型不符错误吗?CompareSalaryName的前两个参数不是TEmployee类型吗?的确,在VB里这样调用是不行的,因为 VB的类型检查不会允许这样的调用。但是,实际上这个调用是API进行的回调,而VB不可能去检查API回调的函数的参数类型是一个普通的Long数值类 型还是一个结构指针,所以也可以说我们绕过了VB对函数参数的类型检查,我们可以将这个Long型参数声明成任何类型的指针,我们声明成什么,VB就认为 是什么。所以,我们要小心地使用这种技术,如上面最终会传递给CompareSalaryName函数的参数"ArrayPtr + (i - 1) * nElemSize"只不过是一个地址,VB不会对这个地址进行检查,它总是将这个地址当做一个TEmployee类型的指针,如果不小心用成了 "ArrayPtr + i * nElemSize",那么当i是最后一个元素时,我们就会引起内存越权访问错误,所以我们要和在C里处理指针一样注意边界问题。 函数指针的巧妙应用这里已经可见一斑了,但是这里介绍的方法还有很大的局限性,我们的函数必须要有四个参数,更干净的做法还是在VC或Delphi里写一 个DLL,做出更加符合要求的API来实现和CallWindowProc相似的功能。我跟踪过CallWindowProc的内部实现,它要做许多和窗 口消息相关的工作,这些工作在我们这个应用中是多余的。其实实现强制回调API只需要将后几个参数压栈,再call第一个参数就行了,不过几条汇编指令而 已。 正是因为CallWindowProc的局限性,我们不能够用它来调用外部的函数指针,以实现上面说的第三种函数指针调用方式。要实现第三种方式, Matt Curland大师提供了一个噩梦一般的HACK方式,我们要在VB里凭空构造一个IUnknown接口,在IUnknown接口的vTable原有的三 个入口后再加入一个新入口,在新入口里插入机器代码,这个机器代码要处理掉this指针,最后才能调用到我们给的函数指针,这个函数指针无论是内部的还是 外部的都一样没问题。在我们深入讨论COM内部原理时我会再来谈这个方法。 另外,排序算法是个见仁见智的问题,我本来想,在本文提供一个最通用性能最好的算法,这种想法虽好,但是不可能有在任何情况下都“最好”的算法。本文提供 的用各种指针技术来实现的快速排序方法,应该比用对象技术来实现同样功能快不少,内存占用也少得多。可是就是这个已经经过了我不少优化的快速排序算法,还 是比不了ShellSort,因为ShellSort实现上简单。从算法的理论上来讲qsort应该比ShellSort平均性能好,但是在VB里这不一 定(可见本文配套代码,里面也提供了VBPJ一篇专栏的配套代码ShellSort,非常得棒,本文的思想就取自这个ShellSort)。 但是应当指出无论是这里的快速排序还是ShellSort,都还可以大大改进, 因 为它们在实现上需要大量使用CopyMemroy来拷贝数据(这是VB里使用指针的缺点之一)。其实,我们还有更好的方法,那就是Hack一下VB的数组 结构,也就是COM自动化里的SafeArray,我们可以一次性的将SafeArray里的各个数组元素的指针放到一个long型数组里,我们无需 CopyMemroy,我们仅需交换Long型数组里的元素就可以达到实时地交换SafeArray数组元素指针的目的,数据并没有移动,移动的仅仅是指 针,可以想象这有快多。在下一篇文章《VB指针葵花宝典之数组指针》中我会来介绍这种方法。 后记: 我学习所以我快乐。 VB真是想不到系列之四:VB指针葵花宝典之SafeArray 关键字:VB、HCAK、指针、SafeArray、数组指针、效率、数组、排序 难度:中级或高级 要求:熟悉VB,了解基本的排序算法,会用VC更好。引言: 上回说到,虽然指针的运用让我们的数组排序在性能上有了大大的提高,但是CopyMemory始终是我们心里一个挥之不去的阴影,因为它还是太慢。在C里 我们用指针,从来都是来去自如,随心所欲,四两拨千斤;而在VB里,我们用指针却要瞻前顾后,哪怕一个字节都要用到CopyMemory乾坤大挪移,真 累。今天我们就来看看,能不能让VB里的指针也能指哪儿打哪儿,学学VB指针的凌波微步。 各位看官,您把茶端好了。 一、帮VB做点COM家务事 本系列开张第一篇里,我就曾说过VB的成功有一半的功劳要记到COM开发小组身上,COM可是M$公司打的一手好牌,从OLE到COM+,COM是近十年来M$最成功技术之一,所以有必要再吹它几句。 COM组件对象模型就是VB的基础,Varinat、String、Current、Date这些数据类型都是COM的,我们用的CStr、CInt、 CSng等Cxxx函数根本就是COM开发小组写的,甚至我们在VB里用的数学函数,COM里都有对应的VarxxxDiv、VarxxxAdd, VarxxxAbs。嘿嘿,VB开发小组非常聪明。我们也可以说COM的成功也有VB开发小组和天下无数VB程序员的功劳,Bill大叔英明地将COM和 VB捆绑在一起了。 所以说,学VB而不需要了解COM,你是幸福的,你享受着VB带给你的轻松写意,她把那些琐碎的家务事都干了,但同时你又是不幸的,因为你从来都不曾了解 你爱的VB,若有一天VB对你发了脾气,你甚至不知该如何去安慰她。所以,本系列文章将拿出几大篇来教大家如何帮VB做点COM方面的家务事,以备不时之 需。 想一口气学会所有COM家务事,不容易,今天我们先拿数组来开个头,更多的技术我以后再一一道来。 二、COM自动化里的SafeArray 就象洗衣机、电饭堡、吹尘器,VB洗衣服、做饭、打扫卫生都会用到COM自动化。它包含了一切COM里通用的东西,所有的女人都能用COM自动化来干家 务,无论是犀利的VC、温柔的VB、还是小巧的VBScript,她们都能用COM自动化,还能通过COM自动化闲话家常、交流感情。这是因为COM自动 化提供了一种通用的数据结构和数据转换传递的方式。而VB的数据结构基本上就是COM自动化的数据结构,比如VB里的数组,在COM里叫做 SafeArray。所以在VB里处理数组时我们要清楚的知道我们是在处理SafeArray,COM里的一种安全的数组。 准备下厨,来做一道数组指针排序的菜,在看主料SafeArray的真实结构这前,先让我们来了解一下C里的数组。 在C和C++里一个数组指针和数组第一个元素的指针是一回事,如对下: #include using namespace std; int main() { int a[10]; cout << "a = " << a << endl; cout << "&a[0] =" << &a[0] << endl; } ///:~ 可以看到结果a和&a[0]是相同的,这里的数组是才数据结构里真实意义上的数组,它们在内存里一个接着一个存放,我们通过第一个元素就能访问随 后的元素,我们可以称这样的数组为"真数组"。但是它不安全,因为我们无法从这种真数组的指针上得知数组的维数、元素个数等非常重要的信息,所以也无法控 制对这种数组的访问。我们可以在C里将一个二维数组当做一维数组来处理,我们还可以通过一个超过数组大小的索引去访问数组外的内存,但这些都是极不安全 的,数组边界错误可以说是C里一个非常容易犯却不易发现的错误。 因此就有了COM里的SafeArray安全数组来解决这个问题,在VB里我们传递一个数组时,传递的实际上COM里的SafeAraay结构指构的指针,SafeAraay结构样子如下: Private Type SAFEARRAY cDims As Integer 注释:这个数组有几维? fFeatures As Integer 注释:这个数组有什么特性? cbElements As Long 注释:数组的每个元素有多大? cLocks As Long 注释:这个数组被锁定过几次? pvData As Long 注释:这个数组里的数据放在什么地方? 注释:rgsabound() As SFArrayBOUND End Type 紧接在pvData这后的rgsabound是个真数组,所以不能在上面的结构里用VB数组来声明,记住,在VB里的数组都是SafeArray,在VB里没有声明真数组的方法。 不过这不是问题,因为上面SFArrayBOUND结构的真数组在整个SAFEARRAY结构的位置是不变的,总是在最后,我们可以用指针来访问它。 SFArrayBOUND数组的元素个数有cDims个,每一个元素记录着一个数组维数的信息,下面看看它的样子: Private Type SAFEARRAYBOUND cElements As Long 注释:这一维有多少个元素? lLbound As Long 注释:它的索引从几开始? End Type 还有一个东西没说清,那就是上面SAFEARRAY结构里的fFeatures,它是一组标志位来表示数组有那些待性,这些特性的标志并不需要仔细的了解,本文用不上这些,后面的文章用到它们时我会再来解释。 看了上面的东西,各位一定很头大,好在本文的还用不了这么多东西,看完本文你就知道其实SafeArray也不难理解。先来看看如下的声明: Dim MyArr(1 To 8, 2 To 10) As Long 这个数组做为SafeArray在内存里是什么样子呢?如图一: cDims = 2 fFeatures = FADF_AUTO AND FADF_FIXEDSIZE 位置 0 cbElements = 4 LenB(Long) 4 cLocks = 0 8 pvData(指向真数组) 12 rgsabound(0).cElements = 8 16 rgsabound(0).lLbound = 1 18 rgsabound(1).cElements = 9 22 rgsabound(1).lLbound = 2 26 cDims = 2 fFeatures = FADF_AUTO AND FADF_FIXEDSIZE 位置 0 cbElements = 4 LenB(Long) 4 cLocks = 0 8 pvData(指向真数组) 12 rgsabound(0).cElements = 8 16 rgsabound(0).lLbound = 1 18 rgsabound(1).cElements = 9 22 rgsabound(1).lLbound = 2 26 图一 :SafeArray内存结构 cDims表示它是个2维数组,sFeatures表示它是一个在堆栈里分配的固定大小的数组, cbElements表示它的每个元素大小是Long四个字节,pvData指向真的数组(就是上面说的C里的数组),rgsabound这个真数组表明 数组二个维的大小和每个维的索引开始位置值。 先来看看从这个上面我们能做些什么,比如要得到一个数组的维数,在VB里没有直接提供这样的方法,有一个变通的方法是通过错误捕获如下: On Error Goto BoundsError For I = 1 To 1000 注释:不会有这么多维数的数组 lTemp = LBound(MyArr, I) Next BoundErro: nDims = I - 1 MsgBox "这个数组有" & nDims & "维" 现在我们知道了SafeArray的原理,所以也可以直接得到维数,如下: 注释:先得到一个指向SafeArray结构的指针的指针,原理是什么,我后面说。 ppMyArr = VarPtrArray(MyArr) 注释:从这个指针的指针得到SafeArray结构的指针 CopyMemory pMyArr, ByVal ppMyArr, 4 注释:再从这个指针所指地址的头两个字节取出cDims CopyMemory nDims, ByVal pMyArr, 2 MsgBox "这个数组有" & nDims & "维" 怎么样,是不是也明白了LBound实际上是SafeArray里的rgsabound的lLbound,而UBound实际上等于lLbound +cElements - 1,现在我提个问,下面iUBound应该等于几? Dim aEmptyArray() As Long iUBound = UBound(aEmptyArray) 正确的答案是-1,不奇怪,lLbound -cElements - 1 = 0 - 0 - 1 = -1 所以检查UBound是不是等于-1是一个判断数组是不是空数组的好办法。 还有SafeArray结构里的pvData指向存放实际数据的真数组,它实际就是一个指向真数组第一个元素的指针,也就是说有如下的等式: pvDate = VarPtr(MyArr(0)) 在上一篇文章里,我们传给排序函数的是数组第一个元素的地址VarPtr(xxxx(0)),也就是说我们传的是真数组,我们可以直接在真数组上进行数据 的移动、传递。但是要如何得到一个数组SafeArray结构的指针呢?你应该注意到我上面所用的VarPtrArray,它的声明如下: Declare Function VarPtrArray Lib "msvbvm60.dll" _ Alias "VarPtr" (Var() As Any) As Long 它就是VarPtr,只不过参数声明上用的是VB数组,这时它返回来的就是一个指向数组SafeArray结构的指针的指针。因为VarPtr会将传给它 的参数的地址返回,而用ByRef传给它一个VB数组,如前面所说,实际上传递的是一个SafeArray结构的指针,这时VarPtrArray将返回 这个指针的指针。所以要访问到SafeArray结构需要,如下三步: 用VarPtrArray返回ppSA,再通过ppSA得到它指向的pSA,pSA才是指向SafeArray结构的指针,我们访问SafeArray结构需要用到就是这个pSA指针。 现在你应该已经了解了SafeArray大概的样子,就这么一点知识,我们就能在VB里对数组进行HACK了。 三、HACK数组字串指针 这已经是第三篇讲指针的东西了,我总在说指针能够让我们怎么样怎么样,不过你是不是觉得除了我说过的几个用途外,你也没觉得它有什么用,其实这是因为我和大家一样急于求成。在讲下去之前,我再来理一理VB里指针应该在什么情况下用。 只对指针类型用指针!废话?我的意思是说,象Integer, Long, Double这样的数值类型它们的数据直接存在变量里,VB处理它们并不慢,没有HACK的必要。但是字串,以及包括字串、数组、对象、结构的 Variant,还有包括字串、对象结构的数组它们都是指针,实际数据不放在变量里,变量放的是指针,由于VB不直接支持指针,对他们的操作必须连同数据 拷贝一起进行。有时我们并不想赋值,我们只想交换它们指针,或者想让多个指针指向同一个数据,让多个变量对同一处内存操作,要达到这样的目的,在VB里不 HACK是不行的。 对数组尤其如此,比如我们今天要做的菜:对一个字串数组进行排序。我们知道,对字串数组进行排序很大一部分时间都用来交换字串元素,在VB里对字串赋值时 要先将原字串释放掉,再新建一个字串,再将源字串拷贝过来,非常耗时。用COM里的概念来说,比如字串a、b的操作a=b,要先用 SysFreeString(a)释放掉原来的字串a, 再用a = SysAllocString(b)新建和拷贝字串,明白了这一点就知道,在交换字串时不要用赋值的方式去交换,而应该直接去交换字串指针,我在指针葵花 宝典第一篇里介绍过这种交换字串的方法,这可以大大提高交换字串的速度。但是这种交换至少也要用两次CopyMemory来将指针写回去,对多个字串进行 交换时调用CopyMemory的次数程几何增长,效率有很大的损失。而实际上,指针只是32位整数而已,在C里交换两个指针,只需要进行三次Long型 整数赋值就行了。所以我们要想想我们能不能将字串数组里所有字串指针拿出来放到一个Long型指针数组里,我们只交换这个Long型数组里的元素,也就相 当于交换了字串指针,排好序后,再将这个Long型指针数组重新写回到字串数组的所有字串指针里,而避免了多次使用CopyMemory来一次次两两交换 字串指针。这样我们所有的交换操作都是对一个Long型数组来进行,要知道交换两个Long型整数,在VB里和在C里是一样快的。 现在我们的问题成了如何一次性地将字串数组里的字串指针拿出来,又如何将调整后的字串指针数组写回去。 不用动数组的SafeArray结构,我们用StrPtr也能完成它。我们知道,字串数组元素里放的是实际上是字串指针,也就是BSTR指针,把这些指针放到一个Long型数组里很简单,用下面的方法: Private Sub GetStrPtrs() Dim Hi As Long, Lo As Long Hi = UBound(MyArr) Lo = LBound(MyArr) ReDim lStrPtrs(0 To 1, Lo To Hi) As Long Dim i As Long For i = Lo To Hi lStrPtrs(0, i) = StrPtr(MyArr(i)) 注释:BSTR指针数组 lStrPtrs(1, i) = i 注释:原数组索引 Next End Sub 为什么要用2维数组,这是排序的需要,因为当我们交换lStrPtrs里的Long型指针时,原来的字串数组MyArr里的字串指针并没有同时交换,所以 用lStrPtrs里的Long型指针访问字串时,必须通过原来的索引,因此必须用2维数组同时记录下每个Long型指针所指字串在原字串数组里的索引。 如果只用1维数组,访问字串时就又要用到CopyMemory了,比如访问lStrPtrs第三个元素所指的字串,得用如下方法: CopyMemory ByVal VarPtr(StrTemp), lStrPtrs(3), 4 虽然只要我们保证StrTemp足够大,再加上一些清理善后的工作,这种做法是可以的,但实际上我们也看到这样还是得多次调用CopyMemory,实际上考虑到原来的字串数组MyArr一直就没变,我们能够通过索引来访问字串,上面同样的功能现在就成了: StrTemp = MyArr(lStrPtrs(1,3)) 注释:通过原字串数组索引读出字串。 不过,当我们交换lStrPtrs里的两个Long型指针元素时,还要记得同时交换它们的索引,比如交换第0个和第3个元素,如下: lTemp1 = lStrPtrs(0, 3) : lTemp2 = lStrPtrs(1, 3) lStrPtrs(0, 3) = lStrPtrs(0, 0) : lStrPtrs(1, 3) = lStrPtrs(1, 0) lStrPtrs(0, 0) = lTemp1 : lStrPtrs(1, 0) = lTemp2 当我们排好序后,我们还要将这个lStrPtrs里的指针元素写回去,如下: For i = Lo To Hi CopyMemory(ByVal VarPtr(MyArr(i)), lStrPtrs(0,i), 4) Next 我已经不想再把这个方法讲下去,虽然它肯定可行,并且也肯定比用CopyMemory来移动数据要快,因为我们实际上移动的仅仅是Long型的指针元素。但我心里已经知道下面有更好更直接的方法,这种转弯抹角的曲线救国实在不值得浪费文字。 四、HACK数组的BSTR结构,实时处理指针。 最精采的来了,实时处理指针动态交换数据,好一个响亮的说法。 我们看到,上一节中所述方法的不足在于我们的Long型指针数组里的指针是独立的,它没有和字串数组里的字串指针联系在一起,要是能联系在一起,我们就能在交换Long型指针的同时,实时地交换字串元素。 这可能吗? 当然,否则我花那么笔墨去写SafeArray干什么! 在上一节,我们的目的是要把字串数组里的BSTR指针数组拿出来放到一个Long型数组里,而在这一节我们的目的是要让我们Long型指针数组就是字串数 组里的BSTR指针数组。拿出来再放回去的方法,我们在上一节看到了,现在我们来看看,不拿出来而直接用的方法。 这个方法还是要从字串数组的SafeArray结构来分析,我们已经知道SafeArray结构里的pvData指向的就是一个放实际数据的真数组,而一 个字串数组如MyArr它的pvData指向的是一个包含BSTR指针的真数组。现在让我们想想,如果我们将一个Long型数组lStrPtrs的 pvData弄得和字串数组MyArr的pvData一样时会怎样?BSTR指针数组就可以通过Long型数组来访问了,先看如何用代码来实现这一点: 注释:模块级变量 Private MyArr() As String 注释:要排序的字串数组 Private lStrPtrs() As Long 注释:上面数组的字串指针数组,后面会凭空构造它 Private pSA As Long 注释:保存lStrPtrs数组的SafeArray结构指针 Private pvDataOld As Long 注释:保存lStrPtrs数组的SafeArray结构的原 注释: pvData指针,以便恢复lStrPtrs 注释:功能: 将Long型数组lStrPtrs的pvData设成字串数组MyArr的pvData 注释: 以使Long指针数组的变更能实时反应到字串数组里 Private Sub SetupStrPtrs() Dim pvData As Long 注释: 初始化lStrPtrs,不需要将数组设得和MyArr一样大 注释: 我们会在后面构造它 ReDim lStrPtrs(0) As Long 注释:得到字串数组的pvData pvData = VarPtr(MyArr(0)) 注释:得到lStrPtrs数组的SafeArray结构指针 CopyMemory pSA, ByVal VarPtrArray(lStrPtrs), 4 注释:这个指针偏移12个字节后就是pvData指针,将这个指针保存到pvDataOld 注释: 以便最后还原lStrPtrs,此处也可以用: 注释: pvDataOld = VarPtr(lStrPtrs(0)) CopyMemory pvDataOld, ByVal pSA + 12, 4 注释:将MyArr的pvData写到lStrPtrs的pvData里去 CopyMemory ByVal pSA + 12, pvData, 4 注释:完整构造SafeArray必须要构造它的rgsabound(0).cElements CopyMemory ByVal pSA + 16, UBound(MyArr) - LBound(MyArr) + 1, 4 注释:还有rgsabound(0).lLbound CopyMemory ByVal pSA + 20, LBound(MyArr), 4 End Sub 看不懂,请结合图一再看看,应该可以看出我们是凭空构造了一个lStrPtrs,使它几乎和MyArr一模一样,唯一的不同就是它们的类型不同。 MyArr字串数组里的fFeatures包含FADF_BSTR,而lStrPtrs的fFeatures包含FADF_HAVEVARTYPE,并且 它的VARTYPE是VT_I4。不用关心这儿,我们只要知道lStrPtrs和MyArr它们指向同一个真数组,管他是BSTR还是VT_I4,我们把 真数组里的元素当成指针来使就行了。 注意,由于lStrPtrs是我们经过了我们很大的改造,所以当程序结束前,我们应该将它还原,以便于VB来释放资源。是的,不释放也不一定会引起问题, 因为程序运行结束后,操作系统的确是会回收我们在堆栈里分配了却没有释放的lStrPtrs原来的野指针pvOldData,但当你在IDE中运行时,你 有60%的机会让VB的IDE死掉。我们是想帮VB做点家务事,而不是想给VB添乱子,所以请记住在做完菜后,一定要把厨房打扫干净,东西该还原的一定要 还原。下面看看怎么样来还原: 注释:还原我们做过手脚的lStrPtr Private Sub CleanUpStrPtrs() 注释:lStrPtr的原来声明为:ReDim lStrPtrs(0) As Long 注释: 按声明的要求还原它 CopyMemory pSA, ByVal VarPtrArray(lStrPtrs), 4 CopyMemory ByVal pSA + 12, pvDataOld, 4 CopyMemory ByVal pSA + 16, 1, 4 CopyMemory ByVal pSA + 20, 0, 4 End Sub 好了,精华已经讲完了,如果你还有点想不通,看看下面的实验: 注释:实验 Sub Main() 注释:初始化字串数组 Call InitArray(6) 注释:改造lStrPtrs Call SetupStrPtrs 注释:下面说明两个指针是一样的 Debug.Print lStrPtrs(3) Debug.Print StrPtr(MyArr(3)) Debug.Print 注释:先看看原来的字串 Debug.Print MyArr(0) Debug.Print MyArr(3) Debug.Print 注释:现在来交换第0个和第3个字串 Dim lTmp As Long lTmp = lStrPtrs(3) lStrPtrs(3) = lStrPtrs(0) lStrPtrs(0) = lTmp 注释:再来看看我们的字串,是不是觉得很神奇 Debug.Print MyArr(0) Debug.Print MyArr(3) Debug.Print 注释:还原 Call CleanUpStrPtrs End Sub 在我的机器上,运行结果如下: 1887420 1887420 OPIIU WCYKOTC WCYKOTC OPIIU 怎么样?如愿已偿!字串通过交换Long型数被实时交换了。 通过这种方式来实现字串数组排序就非常快了,其效率上的提高是惊人的,对冒泡排序这样交换字串次数很多的排序方式,其平均性能能提高一倍以上(要看我们字 串平均长度,),对快速排序这样交换次数较少的方法也能有不少性能上的提高,用这种技术实现的快速排序,可以看看本文的配套代码中的 QSortPointers。 本道菜最关键的技术已经讲了,至于怎么做完这道菜,怎么把这道菜做得更好,还需要大家自己来实践。 四、我们学到了什么。 仅就SafeArray来说,你可能已经发现我根本就没有直接去用我定义了的SAFEARRAY结构,我也没有展开讲它,实际上对SafeArray我们 还可以做很多工作,还有很多巧妙的应用。还有需要注意的,VarPtrArray不能用来返回字串数组和Variant数组的SafeArray结构的指 针的指针,为什么会这样和怎样来解决这个问题?这些都需要我们了解BSTR,了解VARIANT,了解VARTYPE,这些也只是COM的冰山一角,要学 好VB,乃至整个软件开发技术,COM还有很多很多东西需要学习,我也还在学,在我开始COM的专题之前,大家也应该自学一下。 COM的东西先放一放,下一篇文章,应朋友的要求,我准备来写写内存共享。 后记: 又花了整整一天的时间,希望写的东西有价值,觉得有用就来叫个好吧! VB真是想不到系列之五:高效字串指针类关键字:VB、HCAK、字串指针、BSTR、效率、内存共享 难度:中级或高级 参考文章: 1、2000年7月VBPJ Black Belt专栏文章《Modify a Varialbe注释:s Pointer》 作者:Bill McCarthy 2、1998年4月VBPJ Black Belt专栏文章《Play VB注释:s Strings》 作者:Francesco Balena 引言: 本想以内存共享做为VB指针专题的最后一篇,写着写着发现字串的问题应该单独谈谈。在内存共享的问题上,我尤其关心的是字串的共享,因为在我一个多月前发 布的源码里用的是《HardCore VB》里Bruce Mckinney提供的CShareStr类,它实现了字串的内存共享。但是Bruce也没有突破局限,对字串的处理依然是CopyMemory的乾坤大 挪移,尤其是还要进行讨厌的ANSI/DBCS和Unicode的转换。我在readme里说过它效率极低,应该采用Variant或Byte数组来实 现,才能避免转换。后来又想到可以用StrPtr来做,并在VC里用DLL共享节实现了可以不进行转换的字串内存共享。不过在VC里我仍然需要用 SysAllocString来建立VB能使用的BSTR。这都不是我想要的,我想要的东西要象VC里的CString的一样,只要字串够大,对其赋值就 不用重新分配内存,还要象VC里CComBSTR类一样可以Attach到一个特定BSTR。 知道该怎么做,是在看了VBPJ上Bill McCarthy和Francesco Balena的两篇文章之后。Bill用修改SafeArray描述结构实现了数组的内存共享,而Francesco则对字串指针进行深入的探讨。但是 Bill和Francesco的东西都没有实现我想要的字串类。 方法知道了,实现并不难,所以我决定自己来包装一个这样的东西。 正文: 使用VB里的字串类型String有两大不足:第一、它的分配是由VB运行时控制,我们不能将其分配在指定内存处;第二,任何一次对字串的赋值操作都要进行内存重新分配。要实现高效、灵活的字串处理,我们必须克服这两大不足。 对于第一个问题,通过修改String变量里放着的BSTR描述符指针可以实现;对于第二个问题,可以用Mid语句(注意是语句而不是函数)来赋值。不详细讲了,直接看下面的这个类: Option Explicit 注释:******************************************************** 注释:clsBSTR.cls 注释:作者: 熊超 ID: AdamBear 2002年3月18日 注释:http://www.csdn.net/Author/AdamBear 注释: 你可以自由使用本类模块,不过请保留本声明 注释:******************************************************** Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As Long) 注释:不要直接对sString赋值(可以用MID语句),将其设为公有仅为提高效率。 Public sString As String 注释:BSTR描述符指针 Private pStr As Long 注释:BSTR地址 Private nMaxLen As Long 注释:BSTR最大字节数 注释:让本字串指向特定地址 Public Sub Attach(Addr As Long, Optional nLen As Long) pStr = Addr 注释:修改BSTR描述符指针,使其指向Addr CopyMemory ByVal VarPtr(sString), Addr, 4 If IsMissing(nLen) Then Exit Sub 注释:设定最大字串字节数 nMaxLen = nLen End Sub 注释:还原本字串原BSTR描述符 Public Sub Detach() CopyMemory ByVal VarPtr(sString), 0&, 4 End Sub 注释:让本字串指向源字串 Public Sub AttachStr(sStr As String) Attach StrPtr(sStr), LenB(sStr) End Sub 注释:data为缺省属性 Public Property Let data(sVal As String) Dim c As Long c = LenB(sVal) 注释:超过最大字串数,抛出错误。 If c > nMaxLen Then Err.Raise vbObjectError + 3000, _ "CString::Let Data", "溢出" 注释:写字串长度 CopyMemory ByVal (pStr - 4), c, 4 注释:写字串 Mid(sString, 1) = sVal End Property 注释:可以通过公有变量sString来读字串,效率更高 Public Property Get data() As String data = sString End Property Private Sub Class_Terminate() Call Detach End Sub 用法如下,假设我们已通过VitualAlloc,HeapAlloc,MapViewOfFile这样的内存管理API得到了一个4k个字节的可读写的内存地址baseAddr: Dim sShare As New clsBSTR 注释:留下前4个字节用于BSTR保存字串字节数 sShare.Attach(baseAddr+4, 4096-4) 注释:下面的字串"Test"会直接写到baseAddr+4字节处 sShare = "Test" Dim y As String 注释:读字串时可以用sString属性或缺省属性 y = sShare.sString 注释:用AttachStr方法Attach到一个字串。 注释:必须要先Detach sShare.Detach sShare.AttachStr(y) sShare = "Hahaha" Debug.Print y 注释:一旦AttachStr到字串y后,对sShare的修改就相当于对y的修改。 注释:并且以后对y的修改也只能用Mid语句 Mid(y, 1) = "xxxxx" 注释:不能直接赋值,这样VB会将原来y所指(也是sShare所指)内存释放, 注释: 重新分配y。这样在访问sShare时会出错。 注释:y = "Test" 我也不在这里讲这个类的详细原理,可以参考我前面说的两篇文章。 使用这个类有几个需要注意的地方。 1、读字串时可以用sString属性来读,更快。 读sShare有两种方法,一种是用缺省属性Data来读,一种是直接用sString属性来读。用sString属性不重新分配内存,要快得多。 2、不要直接给sString赋值,应使用缺省的data属性来赋值。 之所以把sString属性暴露出来,是为了效率和方便。我们可以用Mid语句对其进行修改,但不要直接用"="来赋值。 3、注意Attach的第二个参数,表示字串的最大字节数,不要让它超过已经分配的内存。 4、用AttachStr将本字串对象Attach到某个字串(比如上面的y)上后,不能再对这个字串y重新赋值,也不能将其传递到会对其重新赋值的过程。 哇,这么多需要注意的问题,用起来岂不是更不方便。的确,用它的之前要考虑是不是必须的。因为建立这个类也一样有开销。所以还有一个需要注意的问题: 5、它主要的应用还是在于将字串安放在指定内存处。虽然它也可以让同一个进程内几个的字串达到共享的目的,但是如果只是两三个很小的字串这样时做反而慢了。 后计: 数组指针和字串指针我们已经谈过了,对于普通的数值类型变量的指针没有什么Hack的必要,但是它关系到一个有用的技术,下篇文章再谈。 本文和下篇文章的代码,以及用这个类来实现的共享内存的代码,我会发布到CSDN共享软件上,名字是《内存共享和指针》。 VB真是想不到系列之六:有用的技术和没用的指针关键字:VB、SafeArray、数值类型指针 难度:中级 参考文章: 1、2000年7月VBPJ Black Belt专栏文章《Modify a Varialbe注释:s Pointer》 作者:Bill McCarthy 引言: 这真的是指针专题的最后一篇了(当然,以后肯定还会提到指针)。主要是来谈谈Bill McCarthy的文章《Modify a Varialbe注释:s Pointer》的精华。关于这篇文章的东西,在我的《VB指针葵花宝典之SafeArray》里曾谈到过,但那篇文章实际上没有写出SafeArray 的精华,用SafeArray最妙的地方在于可以将一个变量建在指定的内存处,就象上一篇文章给出的那个字串类一样。 正文: Bill McCarthy在那篇《Modify a Varialbe注释:s Pointer》里用SafeArray实现多进程的数组共享内存,他考虑了数组变量的类型,因此可以兼容大部分数值类型的数组,是一个非常不错的东西。 我这里不讲它实现的具体方法,只是想和大家一起看看SafeArray还能做什么。 修改SafeArray结构的pvData指针却是一个非常有用的技术,通过修改pvData,就能够通过数组直接访问指定的内存。 和上一篇文章包装字串指针类一样,通过修改pvData,我们也可以包装一些普通数值类型变量的指针类。 我在指针的第一篇文章里说过,要想实现C语言里注释:*注释:这个取指针所指变量值功能,必须要用CopyMemory。实际上,我说错了,我们完全可以实现和C里一样的指针,如下: //C语言 Long L; Long* pL = &L; *pL = 12; printf("L = %d *pL = %d", l, *pl); 注释:VB里 Dim pL As New pLong, L As Long pL.Attach L 注释:也可以 pL.Ptr = VarPtr(L) pL = 12 Debug.Print "L ="; L; " *pL ="; pL 结果都能够通过修改pL指针,达到修改变量L的目的。 上面VB代码里的pLong就是一个包装好的Long型变量的指针类,下面看看如何来实现它: Option Explicit 注释:******************************************************** 注释:pLong.cls 注释:包装一个Long型指针的类 注释:作者: 熊超 ID: AdamBear 2002年3月18日 注释:http://www.csdn.net/Author/AdamBear 注释: 你可以自由使用本类模块,不过请保留本声明 注释:******************************************************** Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As Long) Private m_Arr(0) As Long 注释:缺省属性 Public Property Get Data() As Long Data = m_Arr(0) End Property Public Property Let Data(ByVal Value As Long) m_Arr(0) = Value End Property Public Sub Attach(Target As Long) Ptr = VarPtr(Target) End Sub Public Property Let Ptr(ByVal Target As Long) Dim pSA As Long 注释:得到SafeArray结构指针pSA CopyMemory pSA, ByVal VarPtrArray(m_Arr), 4 注释:这个指针偏移12个字节后就是pvData指针 CopyMemory ByVal (pSA + 12), Target, 4 End Property Public Property Get Ptr() As Long Ptr = m_SA.pvData End Property Private Sub Class_Terminate() CopyMemory ByVal VarPtrArray(m_Arr), 0&, 4 End Sub 要将它改成Byte的指针类,只需要将上面的代码中m_Arr数组的类型,Data属性和Attach方法中的参数类型改为Byte型即可。 当我们这样做出pLong、pByte、pInteger后,我们就能够玩点和C里一样的花样了。 Sub Main() Dim pB As New pByte, B As Byte Dim pI As New pInteger, I As Integer Dim pL As New pLong, L As Long 注释:用Attach方法将经过类型检查,直接用Ptr属性则可以绕过类型检查 pB.Attach B pI.Attach I pL.Attach L 注释:试试指针 B = 1 Debug.Print "B ="; B; " *pB ="; pB pB = 1 Debug.Print "B ="; B; " *pB ="; pB I = 1000 Debug.Print "I ="; I; " *pI ="; pI pI = 2000 Debug.Print "I ="; I; " *pI ="; pI L = 40000 Debug.Print "L ="; L; " *pL ="; pL pL = 60000 Debug.Print "L ="; L; " *pL ="; pL 注释:试试C里的类型转换 注释:用Integer指针访问Long型变量 pI.Ptr = VarPtr(L) Debug.Print "*pI ="; pI End Sub 搞出这几种普通数值类型的指针类有什么用?基本上没有什么大用。不过是证明一种方法的可行性,和演示技术。这种技术还有什么用,需要的时候还会再谈。 后记: 本文的东西,可见CSDN共享软件上的《内存共享和指针》, 指针的专题就到这儿了,下一篇准备开始着手写VB和COM的一个系列文章,其间我准备翻译一下《VB Design Patterns》,这是一本不错的书。
阅读更多
换一批

没有更多推荐了,返回首页