澄清VB调用API时字符串参数的困惑

声明:本文部分内容来源于网络!

首先,我认为这样花费精力去研究VB调用API字符串的种种猫腻是很有必要的。本着严谨和发现问题与解决问题的原则,和为了能更好的认识问题的本质,我特写了这篇冗长的讨论。网上有很多关于此的讨论,但比较杂乱,目的不明确,也很难懂。在此也就是做个总结吧!让大家能有一个清楚认识。

我的问题是从调用内存API时参数的ByVal VarPtr+变量和直接写变量的区别开始的。举个例子:ByVal VarPtr x 与x,按照网上的说法这两者是没有任何区别的,这是不正确的!这两个有没有区别要看变量x是什么类型的,如果是字符串型,这两个是完全不一样的含义;如果是除了字符串型以外的任何类型(也不包括变体型),这两个的含义就是一样的!为什么这样说?往下看你就明白了。

先说说VB中字符串的存储结构。VB里面使用的字符串String就是COM规范中的BSTR ,其特点是缓冲区前面有4字节的前缀用于说明缓冲区的长度:

 

在VB中字符串型的变量实际上不是字符串型,而是一个Long型的变量,它里面存放的是指向字符串缓冲区的指针(也就是指向图中蓝色部分的起始地址)。知道了VB字符串的结构,重点就来了。VB中的字符串全是以Unicode编码表示的,而API中的字符串是以ASNI表示的。所以在调用API涉及到字符串参数时VB妈妈就自动帮程序员完成了由Unicode到ASNI的转换。也就是说你写一个API,VB妈妈只要看到这个API参数中有字符串,在传递参数前,就自动帮你在内存中建立一个临时的变量,用来存放转换Unicode到ASNI转换的结果,一切对于字符串的操作都是对这个临时的变量处理的,这时候你原来的变量就一边歇着去了,直到处理完毕,细心的VB妈妈又把临时变量的值给原来的字符串变量。问题就出在这!就是这个转换过程使得ByVal VarPtr x 与x有着天壤之别!(第一遍看不到就多读几遍)

 

看一个例子:

Sub SwapPtr(sA As String, sB As String)

  Dim lTmp As Long

  CopyMemory lTmp, sA, 4

  CopyMemory sA, sB, 4

  CopyMemory sB, lTmp, 4

End Sub

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

 

这是交换两个字符串的函数,第一个是错误的,第二个是正确的,这两个函数的差别就在于有没有ByVal VarPtr,如果按照网上的说法,这两段代码是一样的,但是事实证明是不一样的!

 

下面我们来分析这两段代码。宏观上看这两段代码都是要取出变量中存放的缓冲区的指针,然后交换,所以定义了一个long型的变量lTmp来临时存放这个指针。但是只有第二段代码成功了。第一个为什么不行?按照普通的理解:VB妈妈首先发现sA是个字符串参数,先进行UA转换。转换完成之后按照传地址的方式传给CopyMemory临时变量的地址,然后寻址成功之后获取的是临时变量中的ASNI字符串缓冲区的指针,再把这个32位long型的指针给lTmp。貌似有了这个指针,就不愁找不到真正的字符串。但不要忽略一个问题:这是临时的变量!临时缓冲区!当CopyMemory lTmp, sA, 4这句结束之后,这个缓冲区就不复存在了!对于CopyMemory sA, sB, 4这句话,按照上面的分析,sA,sB都是字符串型,所有都进行转换,都有临时变量,这两个临时变量传入自己的地址,CopyMemory找到地址中的内容,也就是缓冲区指针,sB成功的交赋予给sA自己的缓存区指针,完成之后,VB妈妈根据临时变量的指针,找到对应的字符串转换成Unicode赋给sA。至此完成了把sB赋给sA。但到了 CopyMemory sB, lTmp, 4这句,却无法把sA给sB,因为lTmp对应的缓存区已经释放了!所以得不到任何值。所以此代码只完成了一半任务。如果读到这你能读懂,聪明的你马上会想到如何更改第一段代码,一个小小的改动,就可以使第一段代码正确!就是把lTmp改成Sring型!让lTmp不是去存放一个临时字符串变量的缓冲区地址,而是去存放一个真正的字符串!lTmp字符串型与long型的最大区别就是:如果是long型,只是存放了一个临时的指针,而如果是String型,在CopyMemory执行完毕后,VB妈妈会把这个临时指针对应的字符串给lTmp,放在lTmp的缓冲区。第二段代码由于使用了ByVal VarPtr,所以VB妈妈不再进行UA转换,就没有临时变量,直接传送真实变量的地址。

下面是一个表,有助于你理解(这个表来源于网络,变量名与本文不符,但含义一致!)

 

 

如果你真的看懂了我所说的一切。如果你的头脑够灵活。你会继续思考,也会有第七感指引你:是不是还要明确一些东西?

我也不知道是怎么想到的,反正我就是意识到了:实际上第一段代码交换的是缓冲区的的字符串(变量sA、sB中存放的缓冲区的指针没有改变),而第二段代码交换的是变量sA、sB中存放的缓冲区的指针。两个都达到了交换的目的,但结果完全不同!

要验证这个还需要说明一个问题:就是VB中一个字符串变量定义完成时,在赋值之前,也就是分配内存空间之前,它是没有缓冲区的!也就是字符串变量在确定大小之前里边没有存放缓冲区指针!

Sub SwapPtr(sA As String, sB As String)

  Dim lTmp As String

  Dim lngBefore As Long

  Dim lngAfter As Long

  lngBefore = StrPtr(lTmp)

  CopyMemory lTmp, sA, 4

  CopyMemory sA, sB, 4

  CopyMemory sB, lTmp, 4

  lngAfter = StrPtr(lTmp)

  MsgBox lngBefore & "|" & lngAfter

End Sub

调用一下这段代码,会发现lngBefore的值是0.

Sub SwapPtr(sA As String, sB As String)

  Dim lTmp As String

  Dim lngBefore As Long

  Dim lngAfter As Long

  CopyMemory lTmp, sA, 4

  lngBefore = StrPtr(lTmp)

  CopyMemory sA, sB, 4

  CopyMemory sB, lTmp, 4

  lngAfter = StrPtr(lTmp)

  MsgBox lngBefore & "|" & lngAfter

End Sub

调用一个这个,就是把lngBefore = StrPtr(lTmp)换了一下位置。这回lngBefore不是0了。这两个例子说明了上面的结果,同时也说明另一个问题:调用API时如果有字符串参数,如果不初始化, VB会自动初始化!但是API无法得知它的大小,初始化也不是API完成的,所以就有内存访问保护的风险!(长度不足!),所以尽量先初始化字符串参数再调用!

         另外,很明显可以看出,在一个字符串变量从初始化到释放,它的缓冲区是不变的!lngBefore 永远等于lngAfter。

         有了以上的基础,我们就可以用两段代码验证:“实际上第一段代码交换的是缓冲区的的字符串(变量sA、sB中存放的缓冲区的指针没有改变),而第二段代码交换的是变量sA、sB中存放的缓冲区的指针”

 

第一段验证代码:

 

Option Explicit

Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As Long)

 

Private Sub Command1_Click()

'用指针的做法SwapPtr

Dim a As String

Dim b As String

Dim lngBeforeA As Long

Dim lngBeforeB As Long

Dim lngAfterA As Long

Dim lngAfterB As Long

 

a = "a"

b = "b"

 

lngBeforeA = StrPtr(a)

lngBeforeB = StrPtr(b)

 

SwapPtr a, b

 

lngAfterA = StrPtr(a)

lngAfterB = StrPtr(b)

 

MsgBox "a的值:" & a & "。b的值:" & b & Chr(10) & _

       "a之前存放的指针" & lngBeforeA & "a之后存放的指针" & lngAfterA & Chr(10) & _

       "b之前存放的指针" & lngBeforeB & "b之后存放的指针" & lngAfterB

End Sub

Sub SwapPtr(sA As String, sB As String)

  Dim lTmp As String

  CopyMemory lTmp, sA, 4

  CopyMemory sA, sB, 4

  CopyMemory sB, lTmp, 4

End Sub

 

结果如图:

 

 

分析:a,b的值已经交换,但a,b的缓冲区指针并没有改变,说明a,b是交换的缓冲区内容。

 

第二段验证代码:

Option Explicit

Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As Long)

 

Private Sub Command1_Click()

'用指针的做法SwapPtr

Dim a As String

Dim b As String

Dim lngBeforeA As Long

Dim lngBeforeB As Long

Dim lngAfterA As Long

Dim lngAfterB As Long

 

a = "a"

b = "b"

 

lngBeforeA = StrPtr(a)

lngBeforeB = StrPtr(b)

 

SwapPtr a, b

 

lngAfterA = StrPtr(a)

lngAfterB = StrPtr(b)

 

MsgBox "a的值:" & a & "。b的值:" & b & Chr(10) & _

       "a之前存放的指针" & lngBeforeA & "a之后存放的指针" & lngAfterA & Chr(10) & _

       "b之前存放的指针" & lngBeforeB & "b之后存放的指针" & lngAfterB

End Sub

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

 

结果如图:

 

 

分析:a,b的结果交换,字符串变量a,b所存放的缓冲区指针也交换,说明这段代码交换的是a,b变量中存放的缓冲区指针,达到交换内容的效果。

 

好了,写了一上午。。。就写到这了,我感觉自己说的也不是太明白。但是说的都是关键。还需要自己努力,多看几遍,多思考。有不懂的可以留言!有指正批评的欢迎留言!

 

参考文章:http://www.m5home.com/bbs/thread-2362-1-1.html

                  http://blog.csdn.net/slowgrace/archive/2009/09/14/4549926.aspx

                  http://www.cnblogs.com/xw885/archive/2005/11/22/105264.html

 

posted @ 2011-08-05 14:50  杨元  阅读(523)  评论(0编辑  收藏  举报