-
- 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
-
-
- String1 = "PowerVB"
- String2 = String$(7, 0)
-
- CopyMemory ByVal String2, ByVal String1, 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)
- _tmp2 = StrConv(String2, vbFromUnicode)
也就是说,两个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
- wEnd as Integer
- end type
所以,VarPtr取到的地址是字符串变量的地址,也就是字符串变量指针,也就是存放"指向pData这个地址的
指针"的变量的地址;而StrPtr取到的值就是指向pData地址的指针,也就是字符串缓冲区指针。所以,有时候人们会说,同一个字符串有两个指针,一
个是字符串变量指针、另一个是字符串缓冲区指针。看下面的示例,可以更好的理解以上的说法:
- Option Explicit
-
-
-
-
- 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)
- 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
-
- 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
-
- 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, Len(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
-
-
-
- CopyMemory pString1, ByVal VarPtr(string1), 4
- Debug.Print pString1, StrPtr(string1), VarPtr(string1)
-
-
-
- CopyMemory pString1, VarPtr(string1), 4
- Debug.Print pString1, StrPtr(string1), VarPtr(string1)
-
-
- CopyMemory pString1, string1, 4
- Debug.Print pString1, StrPtr(string1), VarPtr(string1)
-
-
-
-
-
- CopyMemory pString1, ByVal string1, 4
- Debug.Print pString1, StrPtr(string1), VarPtr(string1)
- Debug.Print Hex(pString1)
- End Sub
2.3 大端序和小端序
Test2_Ptr里的结果你都猜的正确么?我猜除了最后一个,应该都正确,呵呵。学习完以上的基础知识,下面这个语句的基本意思不难推测出来:
-
- CopyMemory pString1, ByVal String1, 4
但是有趣的是,头4个字节"Powe"对应的编码是50-6F-77-65,可是取到的pString1里是(65776F50),正好倒过来。这是为什么呢?看下面的解释:
(1)字符串的数据相当于Byte数组,它的字符是放在一个连续的内存块里的。第一个字符地址最低,最后一个字符最高。
(2)
当用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
-
- 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, ByVal String1, i
- Debug.Print String2
- Next i
- End Sub
这里要补充一些关于字节序的知识。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 所在的位置,不当操作也会导致无法预期的结果甚至程序崩溃(见后续讨论)。