一 引言
笔者在编程实践中发现,VB对位操作的支持仅限于AND、OR、XOR几种位运算,远远不如其他的开发工具那样全面(如Visual C++、C++Builder、Delphi等开发工具都提供了整形变量的移位、拆分、合并的运算),因此在使用VB编写诸如加密之类的通用数据处理程序时往往困难重重。为了使以后的开发工作不再陷入僵局,我开始寻求增强VB位操作功能的通用方法,以达到一劳永逸的效果。
VB的数据类型不够丰富,整形数只包括Byte、Integer、Long三种类型,分别对应C++中的 unsigned char、short 和 long 类型,而我们常用的二字节无符号整形unsigned short(也叫“字”、Word)、四字节无符号整形unsigned long(也叫“双字”、DWord)在VB中却没有被支持。 但好在无符号数和有符号数在二进制的层次上没有任何差别,不同之处仅在于编译器对变量的理解。在进行位操作时我们只关心变量的二进制位,因此VB中的Integer类型可以当作Word类型使用,Long类型则对应DWord。(此后文中提及的Integer类型均指VB Integer类型,Long类型均指VB Long类型,Word 、DWord类型则是不依赖于特定编译器的对二字节、四字节整形值的通用称呼)再来看位运算方面,可以看出VB不支持整型变量的左移、右移、拆分、合并等操作。
经过上述的分析之后,已经确定了工作的可行性和工作目标,于是笔者决定开发一个通用模块来增强VB的位操作功能,这个模块是可重用的,只要把这个模块加入工程中,就可以象使用VB的内置函数一样透明的使用模块中的函数,非常方便。如果使用大量的可重用模块来开发程序,则开发周期短,代码可读性好,易于维护,不容易出错。
二设计思路
1. 实现整形变量的拆分、合并
整型变量的拆分、合并是经常要用到的操作,比如IP地址就是一个四字节的双字,有时候为了以点分十进制的方式显示IP地址,就需要单独取出每个字节的值,而有时候为了把点分十进制的IP地址转换为计算机内部的双字,又需要把四个字节组合成一个双字。VB没有提供这样的功能,所以整型变量的拆分、合并也是我们这次要实现的功能。另外整型变量的拆分、合并也是实现Integer、Long类型变量移位的前提条件(后面“分而治之策略”将会提到),只要实现了整型变量的拆分合并,移位问题就完全解决了。
方法1:利用API函数Copymemory实现
在这里笔者利用Win32 API 函数CopyMemory实现了整形变量的拆分、合并操作。在VB中使用API函数必须要声明,CopyMemory函数的声明代码如下:
Declare Sub CopyMemory Lib 'kernel32' Alias 'RtlMoveMemory' _
(Destination As Any, Source As Any, ByVal Length As Long)
其中的参数Destination是目标内存的第一个字节地址,参数Source是被复制内存的第一个字节地址,参数Length是需要复制的字节数。
实现原理很简单:要实现拆分,就用CopyMemory函数把一个整形变量的一部分拷贝到另一个更小的整形变量的地址空间中;而实现合并,则利用CopyMemory函数把两个待合并的小变量拷贝到另一个大整形变量的地址空间中。见示例代码:
Public Function Hi(ByVal Word As Integer) As Byte
'取一个字(Word)的高字节(Byte)
'INPUT-------------------------------------------
'Word 字(Word)
'OUTPUT------------------------------------------
'返回值 Word参数的高字节
'Last updated by Liu Qi 2004-3-20。
Dim bytRet As Byte
CopyMemory bytRet, ByVal VarPtr(Word) + 1, 1’把Word的高字节的内容烤入bytRet的地址处
Hi = bytRet’返回结果
End Function 根据数据类型的不同需要,笔者共设计了6个函数,HI()函数用来获得一个单字的高字节,LO()函数获得单字的低字节,HIWORD()函数获得双字的高位字,LOWORD()函数双字的低位字。CON()函数把两个字节组合成字,CONWORD()函数把两个字组合成双字。只要把这6个函数组合应用就可以随意的拆分、组合各种整型变量。例如前面提到的IP地址,IP地址是用一个DWORD类型变量存储的,在VB中则对应Long类型变量,假设一个IP地址存储在长整型变量中,就可以这样提取一个IP地址的最高字节:HI(HIWORD(lngIP))。
方法2:利用安全数组借用内存的方法实现
方法1虽然用起来简单方便,但是要执行API函数调用,函数调用时要保存现场、恢复现场,时间开销很大,效率太低,因此不适合大数据量密集运算的场合。笔者在开发加密软件时曾使用方法1来处理文件数据,效果很不理想,速度奇慢。其实有一种方法可以巧妙的骗过VB,让一个数组直接访问其他变量的内存空间,从而达到拆分、合并整形变量的目的。由于这种方法省去了API函数调用,因此效率非常高。下面就让我们认识一下VB中的安全数组。VB中的安全数组与C语言中的数组有很大的差别,虽然在VB和C语言中数组变量都是指针,但C语言中的数组变量直接指向数组元素,而 VB中的数组变量却是指向一个SafeArray结构,这个SafeArray结构中的pvData域才指向数组元素。
那么这个SafeArray结构是做什么用的呢?它存储着数组的上界、下界、维数、元素大小等一系列的信息,正是SafeArray结构的存在,使得VB程序能够对数组的访问做越界检查,这就是为什么VB中的数组叫做安全数组的原因,而C语言中的数组显然不具备越界检查的能力。当然安全数组的缺点就是没有C语言的数组灵活,但尽管如此,我们还是有办法操纵它,通过对安全数组的操纵,可以让它访问任意的内存位置,甚至包括其他变量的内存空间。对于一维数组来说,它的SafeArray结构如下: Type SafeArray1d '1维数组的 SafeArray 定义
cDims As Integer '维数
fFeatures As Integer '标志
cbElements As Long '单个元素的字节数
clocks As Long '锁定计数
pvData As Long '指向数组元素的指针
cElements As Long '维定义,该维的元素个数
Lbound As Long '该维的下界
End Type 如果显式的给一个数组变量赋值,让它指向我们自己创建的SafeArray结构,就可以通过设置SafeArray结构的pvData域来访问任意内存位置。请看示例代码: Public Declare Function VarPtrArray Lib 'msvbvm60.dll' _Alias 'VarPtr' (ptr() As Any) As Long
Private Declare Sub CopyMemory Lib 'KERNEL32' Alias 'RtlMoveMemory' (Destination As Any, Source As Any, ByVal Length As Long)
Private Sub Command2_Click()
Dim pBytesInLong() As Byte
Dim SA1D As SafeArray1d
Dim i As Long
With SA1D
.cDims = 1
.fFeatures = 17
.cbElements = 1
.clocks = 0
.pvData = VarPtr(i) '使数组的数据指针指向长整形变量 i
.cElements = 4
.Lbound = 0
End With
'使数组变量(其实就是个指针)指向我们自己创建的 SafeArray1d 结构
CopyMemory ByVal VarPtrArray(pBytesInLong), VarPtr(SA1D), 4
i = &HFFFFFFFF
MsgBox pBytesInLong(1) '访问长整形变量的第2个字节(从低处开始数)
pBytesInLong(3) = 0 '把全部数组元素设为0
pBytesInLong(2) = 0
pBytesInLong(1) = 0
pBytesInLong(0) = 0
MsgBox i '你会发现 i 也变成了 0
'把数组变量(其实就是个指针)指向 0,既 C 语言中的 NULL
CopyMemory ByVal VarPtrArray(pBytesInLong), 0&, 4
End Sub 从代码中可以看到我们用一个字节数组借用了长整形变量i的地址空间,从而可以通过数组元素访问变量i的各个字节。这样也实现了拆分、组合整形变量的目的,和方法1殊途同归,但很显然方法2不需要函数调用,不需要数据复制,因此效率非常高。用这种方法,我专门构筑了一个模块:FastBitEx模块,实现了方法1中提及的6个函数的Fast版本,代码很长,不在这里给出,请读者参阅代码。
2.移位运算的设计实现 在很多VB的资料和代码中都用乘以2的方法实现左移,除以2的方法实现右移。这是可行的,也是有理论依据的。下图是一个BYTE类型的权值表:
位序号 76543210
权值
2 7
2 6
2 5
2 4
2 3
2 2
2 1
2 0
可以看出每一位的权值都是比它低一位的那一位的权值的2倍,对一个BYTE变量左移一位相当于每一个二进制位都向高位移动,则每一位的权值变为原来的两倍(最高位除外),由于BYTE变量的十进制值等于它的每个二进制位的值和该位权值的乘积的总和,所以把一个BYTE变量左移和把它的十进制值乘以2是等效的,唯一的区别就是如果BYTE变量的最高位为 1,乘以2会溢出,我们要使用一个小技巧防止溢出:先把最高位屏蔽为0,再乘以2就不会溢出了。据此我们可以写出把BYTE类型变量左移1位的函数: Private Function ShLB_By1Bit(ByVal Byt As Byte) As Byte
‘把BYTE类型变量左移1位的函数,参数Byt是待移位的字节,函数返回移位结果
‘(Byt And &H7F)的作用是屏蔽最高位。 *2:左移一位
ShLB_By1Bit = (Byt And &H7F) * 2
End Function 类似的把BYTE类型变量右移1位时采用除以2的方法 ,这时要注意舍去小数位,以免VB按照四舍五入的方法处理小数位而引起结果不正确。据此我们可以写出把BYTE类型变量右移1位的函数: Private Function ShRB_By1Bit(ByVal Byt As Byte) As Byte
‘把BYTE类型变量右移1位的函数,参数Byt是待移位的字节,函数返回移位结果
‘/2:右移一位
ShRB_By1Bit = Fix(Byt / 2)
End Function
有了移一位的函数,那么移任意位数的函数就不难写出了:只要反复的调用ShLB_By1Bit()或ShRB_By1Bit()就可以了,参见代码中的函数ShLB() 和 ShRB()。
至此字节变量的移位问题已经得到解决,现在再来看单字和双字的移位,它们分别对应VB中的Integer和Long类型。用乘以2和除以2的方法还行吗?用几个数试验一下就会发现,这个方法失灵了。请看各种运算结果的对比:
A=1001’0111’1110’1100
右移一位: 0100’1011’1111’0110
(A/2):1100’1011’1111’0110
问题好象变的有点复杂了,其实导致这个方法失灵的最根本的原因是VB把Integer和Long类型当做有符号数理解,把一个有符号数乘以2或除以2,最高位(即符号位)根本就没有参与运算,这一点从上面的运算结果对比就可以看出来:把A除以2 以后最高位还是1,根本就没有变,而右移一位后最高位补入的是0,两种运算的结果自然是相去甚远。不只是符号位的问题,如果选用其它的数据来对比还会发现更多的问题,这里就不再赘述了。难道就真的没有办法了吗?办法当然是有的,既然已经实现了字节的移位操作,那么可以 用“分而治之”的策略,把Integer变量一分为二,拆成两个字节,把这两个字节交给ShLB()或ShRB(),把它俩各移一位,最后把移位后的两字节重新组合成一个Integer变量就是移位后的结果了,这不就实现了Integer类型变量的移位了吗。这种方法完全绕过了有符号数的符号位给我们带来的众多麻烦,顺利的实现了目的。用这种方法需要注意一点:如果是左移,要保证把低字节的最高位移入高字节的最低位,反之如果是右移,要把高字节的最低位移入低字节的最高位。从下面的代码中可以看到实现的过程: Private Function ShLW_By1Bit(ByVal Word As Integer) As Integer
'把一个字左移一位的函数, 参数Word是待移位的字,函数返回移位结果
'INPUT-------------------------------
'Word 源操作数
'OUTPUT------------------------------
'返回值 移位结果
'last updated by Liu Qi 2004-3-24
Dim HiByte As Byte, LoByte As Byte
'把字拆分为字节
HiByte = Hi(Word): LoByte = Lo(Word)
'把高字节左移一位,保证把低字节的最高位移入高字节的最低位
HiByte = ShLB_By1Bit(HiByte) Or IIf((LoByte And &H80) = &H80, &H1, &H0)
LoByte = ShLB_By1Bit(LoByte) '低字节左移一位
'把移位后的字节再重新组合成字
ShLW_By1Bit = Con(HiByte, LoByte)
End Function
至于Long类型,和Integer类型一样,属于有符号数,也不能用乘以2和除以2的方法实现移位。我们只好和处理Integer类型一样如法炮制,用分而治之的方法实现移位。具体过程不再赘述,请参看代码。
3.移位运算的性能优化
本文中的移位实现方法偏重于代码的可读性,没有优化代码的性能,因此不适用于对性能要求苛刻的场合。为了优化性能,可以用查表法来优化执行速度,这是一种拿空间换时间的方案,移位结果可以事先都计算出来,保存在移位表中,用的时候查表,比用*2,/2快多了。比如,字节类型的移位表数组定义如下:
dim aSHLB(0 to 255,1 to 7) as byte'字节左移表
dim aSHRB(0 to 255,1 to 7) as byte'字节右移表
使用方法也很简单,比如想要求字节变量x左移一位的结果,只需使用aSHLB(x,1)就可得到,和函数调用很相似。当然,与函数调用不同的是,使用移位表之前一定要初始化移位表的所有元素,否则会得到错误的结果。
Integer类型的移位也可以用查表法,移位表占用 65535 * 15 * 2 * 2 个字节的内存空间。
移位表数组定义如下:
aSHLW(0 to &Hffff&,1 to 15) as integer'单字的左移表
aSHRW(0 to &Hffff&,1 to 15) as integer'单字的右移表
注意 :Integer是有符号类型,造表的时候要用它的无符号值来造表,同样查表的时候也要用它的无符号值来查表。(因为数组下标是不允许负数的。)
求Integer类型无符号值的方法是:(Int and &hFFFF&),注意,不等同于CLng(Int)
遗憾的是,Long类型无法造表,因为Long类型的值的范围是 4 GB,如果对它造表,那么表的大小就会超出总的内娴刂房占洹?
查表移位的代码请参见本文附带的代码,这里就不给出了。
三 结语
要想实现本文所述的那些位操作函数其实有很多方法,本文所用的方式未必是最好的,主要是为了提供一种解决问题的思路:在编程过程中遇到难以解决的问题时,想一想能不能把大问题分解成能解决或已解决的小问题,这就是“分而治之”的策略。因笔者水平有限,本文难免会有疏漏和不足之处,欢迎批评指正,有意见或建议给我发电子邮件liuqi5521@sina.com。
本程序在 Win2000+VB6.0下调试通过。