在VB5中使用指针
有的人看到这题目,会大嚷起来:“NO,NO,NO,VB中哪里来的指针?书上说VB不支持指针!”
别着急,听我慢慢道来。
在一般的VB教科书中,你不会发现有关“指针”这个概念,你甚至可能不会发现“指针”这个词。或者,你的唯一发现是:“VB不支持指针”。
难道我们真的无法在VB中使用指针吗?抑或VB中真的没有指针吗?
在VB5出现以前,使用指针是非常困难的。——但我没说不可以!——而现在,我们可以在VB中使用指针,而且VB中确实存在指针。本文只针对VB5、6。
例如,当你使用 Dim sTmp As String时,有没有想过这意味着什么?在遥远的VB3时代,我们无须知道也不必考虑,现在,我可以告诉你的是,你已经使用了指针!sTmp现在是指向一个内存地址的指针——不管你愿不愿意知道。不过,跟C中不同的是,这个地址包括字符串的长度。
数组又是什么?在内存中是什么形态?数组其实是对指向内存中数组项的地址的指针的引用。让我们来证明这一点。
有一个用来画多边形的API——Polygon,它在Win32API.txt中的定义是:
Declare Function Polygon Lib "gdi32" Alias "Polygon" _
(ByVal hdc As Long, lpPoint As POINTAPI, _
ByVal nCount As Long) As Long
有关lpPoint项,《Win32 程序员参考》中的解释是:
“Points to an array of POINT structures that specify the vertices of the polygon”,意即“指向一个指出多边形各顶点的POINT结构数组”,这是虾米意思?不知你有没有注意到,lpPoint的前面不象其他两项有一个“ByVal”!这是关键。这说明,对于这个参数,要“按引用传送”,而不是“按值传送”,其实质,就是传送指针。如何用VB语言调用呢?试试下面的方法如何:
…
Me.AutoRedraw = True
Dim p(5) As POINTAPI '即POINT结构的VB描述
Dim i%
For i = 0 To 5
p(i).x = Rnd * 100
p(i).y = Rnd * 100
Next
Polygon hDC, p(), 6
Me.Refresh
…
VB运行到黑体字的一行就停下,轻声告诉你说:“ByRef 参数类型不符”。
试使用下面的语句来代替:
Polygon hDC, p(0), 6
成功了。
p(0)所引用的就是数组的地址,数组中所有的其它数据都在它后面排成一排。
VB的作者通过隐藏和包装指针、内存地址和许多晦涩难懂的概念,使VB成为一种高级而相对简单,易用而功能强大的编程工具,这是我们VB爱好者的幸运;但同时,我们要想提高基于VB的应用程序的性能,或实现更高级甚至系统级的操作,必须掌握教科书所没有告诉你的,因为要这样就必须掌握API,而许多API中充满了指针的概念,那些原来我们不需要考虑的问题,现在来到我们面前。
我们来看一个例子:
Dim a() As Byte
a = "指针"
Dim b(0 To 3) As Byte
b = a
这段代码用来给一个字节数组“a”赋值,并试图给另一个字节数组“b”赋值。
当然运行时VB会毫不犹豫的告诉你,“不能给数组赋值”。
这时怎么办?容易!循环历遍a、b数组,把a的每个元素赋值给b的每个元素。对!这是我所喜欢的纯VB方法,安全而可靠。但问题是如果这个数组很大,例如,200000个,这个方法就非常耗时。为什么不能简单的使用b=a的格式呢?答案很简单,VB不支持数组的直接赋值。要提高速度和简化操作,只有选择使用API。下面我们来讨论实现简单复制的方法。
聪明的你一定想到,把数组a指向的数据复制到数组b指向的位置,不就可以了吗?确实是这样。但查遍《Win32 程序员参考》(C和DEPHI所附的Win32.hlp),似乎找不到直接实现这种功能的API。别急,这个API隐藏kernel32.dll里,原名是RtlMoveMemory。
为了便于记忆,可以这样声明:
Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" _
(pDest As Any, pSource As Any, ByVal ByteLen As Long)
pDest ' 要复制到的地址指针
pSource ' 要复制的数据源地址指针
ByteLen ' 要复制的字节数
看,前两个参数都要求传递一个指针。下面是具体实现的例子:
…
Dim a() As Byte
a = "指针"
Dim b(0 To 3) As Byte
CopyMemory b(0), a(0), 4 ' b=a
…
怎么样,简单和直观多了吧?而且,你获得的速度提升是非常可观的,200000记录情况下,大约是用循环遍历法所消耗时间的1/100。
另外,C中可以直接对数组赋值,你也可以用Array来对VB数组赋值,但CopyMemory更简单、直接和强大——特别是对于需要从另外一个数组的某项开始而非第一项的赋值。例如:
Dim a() As Byte
a = "指针"
Dim b(0 To 1) As Byte
CopyMemory b(0), a(2), 2
经过这段代码,在数组b中包含的是数组a的后两个元素。
等你明白VB用户自定义类型——Type也是指针时,下面的代码就显而易见了:
Dim p1 As POINTAPI
p1.x = 10
p1.y = 1000
Dim p2 As POINTAPI
CopyMemory p2, p1, LenB(p1)
而以前,复制类型结构只能采用LSet。
那么如何使用CopyMemory 在字符串和字节数组间复制呢?问得好:
Dim a(3) As Byte
Dim s As String
s = "指针"
CopyMemory a(0), ByVal s, LenB(s)
注意s前面的ByVal!String类型本身就是地址指针,不能按引用,而必须按值传递。当然,这里的“按值传递”并不是说真要把s复制到堆栈上,而是按s所指向的地址的值。
另外,由于CopyMemory传递字节数,必须保证第三个参数合法且准确,否则,可能会发生崩溃。另外,复制汉字字符串时应注意Unicode格式转换。
看,CopyMemory带来许多方便吧?
但我们还没有看到它更擅长的呢。
稍微有胆量试图超越教科书的VB爱好者可能会弄明白,事件驱动的VB如何使瞬息万变的Windows 消息世界变得安静异常,一个VB程序员可能完全不用理会什么是消息。然而,存在并不以人的意志为转移。
篇幅所限,无法在此详述消息的捕获和处理,而只讲一下如何在消息处理中应用CopyMemory。这里假定大家对消息有一定了解,否则,请先了解一下,再继续读下去。
比如在自画菜单(Ownerdrawn Menus)技术中,对WM_DRAWITEM消息的截获至关重要,当我们应用AddressOf拦截此消息并希望分析其内容时,CopyMemory决不是可有可无的。
大家知道,一个消息拦截处理函数需要处理下面4个参数:
hWnd 发送消息的窗口句柄;
Msg 所截获的消息;
LParam 第一消息参数;
WParam 第二消息参数;
对于WM_DRAWITEM消息来讲,LParam消息为标识,WParam为DRAWITEMSTRUCT结构(即用户定义类型)的地址。用通常的VB方法,无法从一个地址得到一个用户定义类型。但是DRAWITEMSTRUCT中的信息我们如此迫切的需要,该怎么办呢?现在你无须忍受我曾经历的这种痛苦了。请看CopyMemory的杰作:
…
Dim di As DRAWITEMSTRUCT
CopyMemory di, ByVal lParam, LenB(di)
…
就这么简单,现在di已经充满我们所需要的信息了。
如果必要,使用VB的隐藏函数VarPtr、StrPtr等可以获得变量或字符串的地址。但这并不能保证在以后的版本中继续得到支持。
综上所述,可见VB中同样可以使用指针,而且用的好的话可以极大提高程序性能、突破VB限制和简化代码。
注意:如同使用任何API一样,不正确使用CopyMemory可能导致数据错误或系统崩溃,请记得先保存您的代码,并认真检查所有参数是否合法。
下面几种情况下可能需要指针和CopyMemory:
1、快速复制一个结构(VB用户类型——Type)的全部或部分,几乎没有更好的复制部分结构的办法;
2、快速复制一个数组,可以从数组的任意位置开始复制;
3、快速转换字节数组和字符串;
4、快速处理大文件,可以将文件存入字节数组,并用CopyMemory得到任意数据;
5、从内存地址得到数据,可以复制到任何VB变量;
至此,大家已经发现了一个强大的武器,用好它,无疑会给您的编程带来突飞猛进的新进展。
另外,AddressOf语句本身就是对指针的使用,这可不是一篇小短文所能描述的。我可以为此写一本这么厚的书了。
作者水平所限,也由于本文观点仅代表个人观点而非MS官方描述,如有不同意的地方欢迎来信指正和商榷。
请到碧海港湾(http://thriller.533.net)或来信(mailto:thriller@163.net)说明您的观点。