|
3#
楼主 |
发表于 2015-6-10 12:32:05
|
只看该作者
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
|
|