计算机工作组:在VB6中用CopyMemory拷贝字符串的种种猫腻(一)

来源:百度文库 编辑:九乡新闻网 时间:2024/05/07 06:42:20

在VB6中用CopyMemory拷贝字符串的种种猫腻(一)收藏


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

本文来自此帖的冗长讨论,感谢Tiger_Zhao的全程指点和陈辉、阿勇、马云剑等很多朋友的热心参与。本文其他部分在:(二)、(三)、(四)。

话说VB6是个很认真细致的妈妈,它会悄没声地帮你做很多事。今天我们来说的是VB6在API调用时自动对字符串参数进行转换的“好人好事”。 第一节 体贴的VB妈妈我们知道,在VB6中字符串都是以Unicode编码存在的,而Windows API函数中很多时候用的是ANSI字符串。VB妈妈害怕程序员们累着,所以在VB程序员调用API时,会自动的对其中的字符串参数做Unicode到ANSI的转换(以下简称UA转换),在API调用结束后会再把字符串参数做ANSI到Unicode的转换(以下简称AU转换)。这样说可能有点抽象,我们来看下面的例子。view plaincopy to clipboardprint?
  1. '正确的ByVal String的用法  
  2. Option Explicit  
  3.   
  4. Const STR_E = "PowerVB"  
  5. Private String1 As String  
  6. Private String2 As String  
  7. Private pString1 As Long  
  8.   
  9. Sub test7()  
  10.     Dim String1 As String  
  11.     Dim String2 As String  
  12. '    Dim _tmp1 As String, _tmp2 As String  
  13.       
  14.     String1 = "PowerVB" '14 bytes  
  15.     String2 = String$(7, 0) '14 bytes  
  16.       
  17.     CopyMemory ByVal String2, ByVal String1, 7  
  18.       
  19. '    _tmp1 = StrConv(String1, vbFromUnicode) '7 bytes  
  20. '    _tmp2 = StrConv(String2, vbFromUnicode) '7 bytes  
  21. '    CopyMemory ByVal _tmp2, ByVal _tmp1, 7  
  22.       
  23.     Debug.Print String2  
  24. End Sub  
如上图所示,当我们在VB中调用CopyMemory ByVal String2, ByVal String1, 7的时候发生了如下事情:①首先VB妈妈帮我们对String1和String2自动做了UA转换,也就是相当于做了如下事情:
  1. Dim _tmp1 As String, _tmp2 As String  
  2.   
  3. _tmp1 = StrConv(String1, vbFromUnicode) '7 bytes  
  4. _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 AsString。这个Str1本身其实是一个4字节的Long型,里面存的是一个指针,指向的是实际字符串的缓冲区开始地址,这个开始地址前面4字节里存放的是这个缓冲区的长度,单位为字节。也就是,VB里的String其实是像下面这样定义的:
  1. Type String   
  2.     dwSize as long  '后面实际数据的长度'   
  3.     pData() as Integer '实际数据,每一个word就是一个字符,16位'   
  4.     wEnd as Integer '字符串结束点\0\0,一个Unicode字符占双字节,不计入长度   
  5. end type  
所以,VarPtr取到的地址是字符串变量的地址,也就是字符串变量指针,也就是存放"指向pData这个地址的指针"的变量的地址;而StrPtr取到的值就是指向pData地址的指针,也就是字符串缓冲区指针。所以,有时候人们会说,同一个字符串有两个指针,一个是字符串变量指针、另一个是字符串缓冲区指针。看下面的示例,可以更好的理解以上的说法:view plaincopy to clipboardprint?
  1. Option Explicit  
  2.   
  3. 'From Myjian  
  4. 'http://topic.csdn.net/u/20090901/09/dddf35aa-7838-4415-85b2-222358422d81_2.html 187楼  
  5.   
  6. Private Declare Sub CopyMemory Lib "kernel32.dll" Alias "RtlMoveMemory" ( _  
  7.      ByVal Destination As Long, _  
  8.      ByVal Source As Long, _  
  9.      ByVal Length As Long)  
  10.   
  11. Sub TestBstr()  
  12.     Dim str1 As String, J As Long, K As Long  
  13.       
  14.     str1 = "IamSlow慢"  
  15.     Debug.Print VarPtr(str1)                            '得到变量本身的地址  
  16.       
  17.     Call CopyMemory(VarPtr(J), VarPtr(str1), 4)         '取得str1里面保存的指针,与StrPtr一样  
  18.     Debug.Print J, StrPtr(str1)  
  19.       
  20.     K = LenPtr(J)                                       '得到字符串的长度,实际字节值  
  21.     Debug.Print K, Len(str1), LenB(str1)  
  22.       
  23.     Debug.Print GetBSTRFromPtr(J)                       '根据这个指针得到字符串  
  24.     Debug.Print GetBSTRFromPtr(StrPtr(str1))  
  25. End Sub  
  26.   
  27. Private Function GetBSTRFromPtr(ByVal lpStr As Long) As String  
  28.     '从指针得到BSTR字符串  
  29.     Dim InStrLen As Long, OutStrArr() As Byte  
  30.       
  31.     InStrLen = LenPtr(lpStr)        '得到输入字符串的长度  
  32.     ReDim OutStrArr(InStrLen - 1)  
  33.     Call CopyMemory(VarPtr(OutStrArr(0)), lpStr, InStrLen)  
  34.       
  35.     GetBSTRFromPtr = OutStrArr  
  36. End Function  
  37.   
  38. Private Function LenPtr(ByVal lpStr As Long) As Long  
  39.     '根据指针取BSTR长度  
  40.     Dim InStrLen As Long  
  41.       
  42.     If lpStr = 0 Then Exit Function  
  43.     CopyMemory VarPtr(InStrLen), lpStr - 4, 4 '得到输入字符串的长度  
  44.     LenPtr = InStrLen  
  45. End Function  
注意,上面的LenPtr函数,是直接通过从字符串缓冲区的长度前缀中拷贝内存得到的。这其实是BSTR指针的特点,你只要保证传入的指针是BSTR指针就可以这样得到字符串的长度。 BSTR是COM中的一种字符串标准,与普通字符串的最大不同在于有长度前缀,所以可以包含NULL在内的字符串。而如果没有长度前缀,字符串中有NULL就会被认为是结束了,从而截断。VB中的字符串就是BSTR类型的。下面这个丑陋但清晰的图说明了一切。我们还可以用以下的代码来验证上面的说法:view plaincopy to clipboardprint?
  1. Sub testNull()  
  2.     Dim str1 As String  
  3.     str1 = "aa" & Chr(0) & "bb"  
  4.     Debug.Print str1, Len(str1), LenB(str1)  
  5.     MsgBox str1  
  6. End Sub  
可以看出,VB中的字符串中间可以含有NULL字符,但是MsgBox这样的函数由于是封装的API函数MessageBox,所以它会按照C字符串的标准来解释字符串长度,因此会把aa以后的字符截掉。 2.2 CopyMemory函数下面我们来熟悉一下本文重点讨论的这个函数。view plaincopy to clipboardprint?
  1. Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" _  
  2.     (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字符串的内容啊。 继续学习后续内容前,不妨做以下练习,以确认你已经掌握本节内容。view plaincopy to clipboardprint?
  1. Sub Test2_Ptr()  
  2.     string1 = STR_E  
  3.       
  4.     '结果:StrPtr((string1)  
  5.     '把VarPtr(String1)的值作为地址,拷这个地址里的值出来:)  
  6.     CopyMemory pString1, ByVal VarPtr(string1), 4  
  7.     Debug.Print pString1, StrPtr(string1), VarPtr(string1)  
  8.       
  9.     '结果:VarPtr(String1)  
  10.     '把VarPtr(String1)这个变量的值拷出来  
  11.     CopyMemory pString1, VarPtr(string1), 4  
  12.     Debug.Print pString1, StrPtr(string1), VarPtr(string1)  
  13.       
  14.     '结果:StrPtr(_tmp1)  
  15.     CopyMemory pString1, string1, 4  
  16.     Debug.Print pString1, StrPtr(string1), VarPtr(string1)  
  17.       
  18.     '结果:"ewoP"的ANSI编码  
  19.     '从内部临时ANSI变量的字符串缓冲区取4个字节出来  
  20.     '"Powe"是50-6F-77-65,取到的pString1里是(65776F50),正好倒过来  
  21.     '因为Long型在书写时是大端在前的  
  22.     CopyMemory pString1, ByVal string1, 4  
  23.     Debug.Print pString1, StrPtr(string1), VarPtr(string1)  
  24.     Debug.Print Hex(pString1)  
  25. End Sub  
2.3 大端序和小端序Test2_Ptr里的结果你都猜的正确么?我猜除了最后一个,应该都正确,呵呵。学习完以上的基础知识,下面这个语句的基本意思不难推测出来:
  1. '从内部临时ANSI变量的字符串缓冲区取头4个字节出来  
  2. CopyMemory pString1, ByVal String1, 4  
但是有趣的是,头4个字节"Powe"对应的编码是50-6F-77-65,可是取到的pString1里是(65776F50),正好倒过来。这是为什么呢?看下面的解释:(1)字符串的数据相当于Byte数组,它的字符是放在一个连续的内存块里的。第一个字符地址最低,最后一个字符最高。
(2)当用Long变量去拷贝字符串的部分内容的时候,Long的高字节对应它取到的最后一个字符,低字节则对应第一个字符。而在数字世界里,我们是把高字节写在左边、低字节写在右边的。所以我们从Long里去观察取到的字符,看起来是最后一个字符在左边、第一个字符在右边,好像倒了。 下面的例子可以帮助你更好的理解这一点:view plaincopy to clipboardprint?
  1. '测试Long在内存的存储顺序和拷贝顺序  
  2. Sub test11()  
  3.     Dim Long1 As Long  
  4.     Dim Long2 As Long  
  5.     Dim i As Long  
  6.       
  7.     Long1 = &H1020304  
  8.     Debug.Print Hex(Long1)  
  9.     For i = 1 To 4  
  10.         CopyMemory Long2, Long1, i  
  11.         Debug.Print Hex(Long2)  
  12.     Next i  
  13. End Sub  
输出10203044304203041020304

 

view plaincopy to clipboardprint?
  1. '测试String在内存的存储顺序和拷贝顺序  
  2. Sub test12()  
  3.     Dim String1 As String  
  4.     Dim String2 As String  
  5.     Dim i As Long  
  6.       
  7.     String1 = "1234"  
  8.     String2 = String$(4, 0)  
  9.     Debug.Print String1  
  10.     For i = 1 To 4  
  11.         CopyMemory ByVal String2, ByVal String1, i  
  12.         Debug.Print String2  
  13.     Next i  
  14. End Sub  

 

输出1234112123 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)拥有最低的地址,且会先储存文字中较大的一端。为使这类电脑的用户能够存取你的文件,可选择Unicodebig-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 所在的位置,不当操作也会导致无法预期的结果甚至程序崩溃(见后续讨论)。