本文来自此帖的冗长讨论,感谢Tiger_Zhao的全程指点和陈辉、阿勇、马云剑等很多朋友的热心参与。本文其他部分在:(二)、(三)、(四)。
话说VB6是个很认真细致的妈妈,它会悄没声地帮你做很多事。今天我们来说的是VB6在API调用时自动对字符串参数进行转换的“好人好事”。
第一节 体贴的VB妈妈
我们知道,在VB6中字符串都是以Unicode编码存在的,而Windows API函数中很多时候用的是ANSI字符串。VB妈妈害怕程序员们累着,所以在VB程序员调用API时,会自动的对其中的字符串参数做Unicode到ANSI的转换(以下简称
UA
转换),在API调用结束后会再把字符串参数做ANSI到Unicode的转换(以下简称AU转换)。这样说可能有点抽象,我们来看下面的例子。
'正确的ByVal String的用法
Option Explicit
Const STR_E = "PowerVB"
Private String1 As String
Private String2 As String
Private pString1 As Long
Sub test7()
Dim String1 As String
Dim String2 As String
' Dim _tmp1 As String,_tmp2 As String
String1 = "PowerVB" '14 bytes
String2 = String$(7,0) '14 bytes
CopyMemory ByVal String2,ByVal String1,7
' _tmp1 = StrConv(String1,vbFromUnicode) '7 bytes
' _tmp2 = StrConv(String2,vbFromUnicode) '7 bytes
' CopyMemory ByVal _tmp2,ByVal _tmp1,7
Debug.Print String2
End Sub
如上图所示,当我们在VB中调用CopyMemory
ByVal
String2,ByVal
String1,7的时候发生了如下事情:
①首先VB妈妈帮我们对String1和String2自动做了
UA
转换,也就是相当于做了如下事情:
Dim _tmp1 As String,_tmp2 As String
_tmp1 = StrConv(String1,vbFromUnicode) '7 bytes
_tmp2 = StrConv(String2,vbFromUnicode) '7 bytes也就是说,两个14字节的Unicode字符串现在被存在两个7字节的ANSI字符串里了。
②然后CopyMemory函数就做实际的拷贝动作。注意,这时CopyMemory得到的参数不是String1,String2了,而是VB妈妈传给它的_tmp1,_tmp2了。所以,实际上,CopyMemory同学是在这么干活:CopyMemory
ByVal _tmp2,ByVal _tmp1,7。也就是,从_tmp1的缓冲区拷贝7个字节到_tmp2的缓冲区。
③CopyMemory同学干完活,VB妈妈又细心地做善后工作。它把_tmp2的内容再转成14字节的Unicode字符串,并把它给String2。
PS:
(1) 文字中带圈标号1与图上的1是一一对应的。
(2) 注意①和③
是
VB
自动进行的,和CopyMemory函数无关。也就是VB只要看到API函数调用中涉及到字符串参数,就会自动做这种转换!
看完上面的例子,也许你就会对VB妈妈这种细致体贴的劲头有点体会了。但是正如现实生活中妈妈的过多干涉会给我们带来困扰一样,VB妈妈的这种体贴有时也会带来让人哭笑不得的效果。
第二节 基础知识
在展示VB妈妈的各种“杰作”之前,我们先来准备一些基础知识。
2.1 VB中字符串的存储结构
当你在VB里声明了一个String型的变量,比如:Dim str1 As String。这个Str1本身其实是一个4字节的Long型,里面存的是一个指针,指向的是实际字符串的缓冲区开始地址,这个开始地址前面4字节里存放的是这个缓冲区的长度,单位为字节。也就是,VB里的String其实是像下面这样定义的:
Type String
dwSize as long '后面实际数据的长度'
pData() as Integer '实际数据,每一个word就是一个字符,16位'
wEnd as Integer '字符串结束点/0/0,一个Unicode字符占双字节,不计入长度
end type所以,VarPtr取到的地址是字符串变量的地址,也就是字符串变量指针,也就是存放"指向pData这个地址的指针"的变量的地址;而StrPtr取到的值就是指向pData地址的指针,也就是字符串缓冲区指针。所以,有时候人们会说,同一个字符串有两个指针,一个是字符串变量指针、另一个是字符串缓冲区指针。看下面的示例,可以更好的理解以上的说法:
Option Explicit
'From Myjian
'http://topic.csdn.net/u/20090901/09/dddf35aa-7838-4415-85b2-222358422d81_2.html 187楼
Private Declare Sub CopyMemory Lib "kernel32.dll" Alias "RtlMoveMemory" ( _
ByVal Destination As Long,_
ByVal Source As Long,_
ByVal Length As Long)
Sub TestBstr()
Dim str1 As String,J As Long,K As Long
str1 = "IamSlow慢"
Debug.Print VarPtr(str1) '得到变量本身的地址
Call CopyMemory(VarPtr(J),VarPtr(str1),4) '取得str1里面保存的指针,与StrPtr一样
Debug.Print J,StrPtr(str1)
K = LenPtr(J) '得到字符串的长度,实际字节值
Debug.Print K,Len(str1),LenB(str1)
Debug.Print GetBSTRFromPtr(J) '根据这个指针得到字符串
Debug.Print GetBSTRFromPtr(StrPtr(str1))
End Sub
Private Function GetBSTRFromPtr(ByVal lpStr As Long) As String
'从指针得到BSTR字符串
Dim InStrLen As Long,OutStrArr() As Byte
InStrLen = LenPtr(lpStr) '得到输入字符串的长度
ReDim OutStrArr(InStrLen - 1)
Call CopyMemory(VarPtr(OutStrArr(0)),lpStr,InStrLen)
GetBSTRFromPtr = OutStrArr
End Function
Private Function LenPtr(ByVal lpStr As Long) As Long
'根据指针取BSTR长度
Dim InStrLen As Long
If lpStr = 0 Then Exit Function
CopyMemory VarPtr(InStrLen),lpStr - 4,4 '得到输入字符串的长度
LenPtr = InStrLen
End Function注意,上面的LenPtr函数,是直接通过从字符串缓冲区的长度前缀中拷贝内存得到的。这其实是BSTR指针的特点,你只要保证传入的指针是BSTR指针就可以这样得到字符串的长度。
BSTR是COM中的一种字符串标准,与普通字符串的最大不同在于有长度前缀,所以可以包含NULL在内的字符串。而如果没有长度前缀,字符串中有NULL就会被认为是结束了,从而截断。VB中的字符串就是BSTR类型的。下面这个丑陋但清晰的图说明了一切。
我们还可以用以下的代码来验证上面的说法:
Sub testNull()
Dim str1 As String
str1 = "aa" & Chr(0) & "bb"
Debug.Print str1,LenB(str1)
MsgBox str1
End Sub可以看出,VB中的字符串中间可以含有NULL字符,但是MsgBox这样的函数由于是封装的API函数MessageBox,所以它会按照C字符串的标准来解释字符串长度,因此会把aa以后的字符截掉。
2.2 CopyMemory函数
下面我们来熟悉一下本文重点讨论的这个函数。
Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" _
(pDest As Any,pSource As Any,ByVal byteLen As Long)这个函数的功能是从pSource拷贝byteLen个字节的数据到pDest,其中源地址和目标地址都是声明为Any类型。下面是CopyMemory对不同形式参数的理解:
(1) 传一个变量给pSource,那么源地址就是变量所在的地址
(2) 以ByVal形式传一个变量给pSource,那么源地址就是变量的值
(3) 字符串变量的值是个指针,指向字符串缓冲区的地址,也就是StrPtr(String1)。因此,以ByVal形式传一个字符串变量给pSource,那么源地址就是字符串变量的值,也就是字符串缓冲区的地址。
下表总结了几种常见的传参数给CopyMemory的形式:
注:
(1)
取到的内容根据byteLen实际规定的字节数的多少,可能有所不同,这里只是个大概。
(2)带高亮的两行,VB对字符串参数做了自动的UA转换,所以实际的CopyMemory动作针对的是由String1转换得到的ANSI字符串_tmp1而进行的。
(3)
字节数那一列给出了要取到有效的数据byteLen参数可以使用的数字范围。简单的说,如果pSource的参数是字符串类型的话,那么byteLen的字节数要取为String1对应的ANSI字符串的长度。要理解这个也容易,你只要记住CopyMemory这时候实际上是对ANSI字符串做操作就可以了。而如果不发生字符串转换的话,像表里第4行,那么你就要拷贝String1的LebB长度。这也好理解,不发生转换的话,CopyMemory实际上是在直接拷贝Unicode字符串的内容啊。
继续学习后续内容前,不妨做以下练习,以确认你已经掌握本节内容。
Sub Test2_Ptr()
string1 = STR_E
'结果:StrPtr((string1)
'把VarPtr(String1)的值作为地址,拷这个地址里的值出来:)
CopyMemory pString1,ByVal VarPtr(string1),4
Debug.Print pString1,StrPtr(string1),VarPtr(string1)
'结果:VarPtr(String1)
'把VarPtr(String1)这个变量的值拷出来
CopyMemory pString1,VarPtr(string1),VarPtr(string1)
'结果:StrPtr(_tmp1)
CopyMemory pString1,string1,VarPtr(string1)
'结果:"ewoP"的ANSI编码
'从内部临时ANSI变量的字符串缓冲区取4个字节出来
'"Powe"是50-6F-77-65,取到的pString1里是(65776F50),正好倒过来
'因为Long型在书写时是大端在前的
CopyMemory pString1,ByVal string1,VarPtr(string1)
Debug.Print Hex(pString1)
End Sub
2.3 大端序和小端序
Test2_Ptr里的结果你都猜的正确么?我猜除了最后一个,应该都正确,呵呵。学习完以上的基础知识,下面这个语句的基本意思不难推测出来:
'从内部临时ANSI变量的字符串缓冲区取头4个字节出来
CopyMemory pString1,4但是有趣的是,头4个字节"Powe"对应的编码是50-6F-77-65,可是取到的pString1里是(65776F50),正好倒过来。这是为什么呢?看下面的解释:
(1)字符串的数据相当于Byte数组,它的字符是放在一个连续的内存块里的。第一个字符地址最低,最后一个字符最高。
(2)当用Long变量去拷贝字符串的部分内容的时候,Long的高字节对应它取到的最后一个字符,低字节则对应第一个字符。而在数字世界里,我们是把高字节写在左边、低字节写在右边的。所以我们从Long里去观察取到的字符,看起来是最后一个字符在左边、第一个字符在右边,好像倒了。
下面的例子可以帮助你更好的理解这一点:
'测试Long在内存的存储顺序和拷贝顺序
Sub test11()
Dim Long1 As Long
Dim Long2 As Long
Dim i As Long
Long1 = &H1020304
Debug.Print Hex(Long1)
For i = 1 To 4
CopyMemory Long2,Long1,i
Debug.Print Hex(Long2)
Next i
End Sub
1020304
4
304
20304
1020304
'测试String在内存的存储顺序和拷贝顺序
Sub test12()
Dim String1 As String
Dim String2 As String
Dim i As Long
String1 = "1234"
String2 = String$(4,0)
Debug.Print String1
For i = 1 To 4
CopyMemory ByVal String2,i
Debug.Print String2
Next i
End Sub
1234
1
12
123
1234
这里要补充一些关于
字节序的知识。Big Endian和Little Endian是cpu处理多字节数的不同方式。例如“汉”字的Unicode编码是6C49。那么写到文件里时,究竟是将6C写在前面,还是将49写在前面?如果将6C写在前面,就是big endian,译作
大端序。还是将49写在前面,就是little endian,译作
小端序。
“endian”这个词出自《格列佛游记》。小人国的内战就源于吃鸡蛋时是究竟从大头(Big-Endian)敲开还是从小头(Little-Endian)敲开,由此曾发生过六次叛乱,其中一个皇帝送了命,另一个丢了王位。
大端序指的是:
从最大的一端开始存储(从低地址存起),MSB的地址最低。
小端序指的是:
从最小的一端开始存储(从低地址存起),MSB的地址最高。
像我们上面测试的Long,它的最高位是1,最低位是4,从拷贝出来的结果可以看出来4在最低位,也就是从小端开始存储,所以我们说它是小端序的。实际上Intel处理器都是小端序的。
而在Big-endian处理器(如苹果Macintosh电脑)上建立的Unicode文件中的文字位元组(存放单位)排列顺序,与在Intel处理器上建立的文件的文字位元组排列顺序相反。最重要的位元组(MSB)拥有最低的地址,且会先储存文字中较大的一端。为使这类电脑的用户能够存取你的文件,可选择Unicode big-endian格式。
2.4 如何传参数会被VB6当做字符串?
Q:VB6根据什么判断要传给CopyMemory的参数是字符串,因而会触发自动的UA /AU转换?以下这些传法,哪种会转,哪种不会转?
(1)ByVal String2
(2)ByVal StrPtr(String1)
(3)ByRef String1
(4)ByVal VarPtr(String1)
A:
(1)ByVal String2:
字符串参数,自动转换。
(2)ByVal StrPtr(String1):指针,不转换。
(3)ByRef
String1:编译错误,去掉 ByRef 可以通过编译,也会引起UA/AU转换。但其实 Any 类型的参数不支持这种用法,会导致无法预期的结果甚至程序崩溃(见后续讨论)。
(4)ByVal VarPtr(String1):指针的指针,不转换。但是这其实是变量 String1 所在的位置,不当操作也会导致无法预期的结果甚至程序崩溃(见后续讨论)。
WEI WAN DAI XU
总结
如果觉得编程之家网站内容还不错,欢迎将编程之家网站推荐给程序员好友。
本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
小编个人微信号 jb51ccc
喜欢与人分享编程技术与工作经验,欢迎加入编程之家官方交流群!