Excel精英培训网

 找回密码
 注册
数据透视表40+个常用小技巧,让你一次学会!
查看: 6637|回复: 6

[分享] 在VB6中用CopyMemory拷贝字符串的种种猫腻

[复制链接]
发表于 2012-2-21 23:12 | 显示全部楼层 |阅读模式
本帖最后由 爱疯 于 2013-3-17 12:28 编辑

版权声明:可以任意转载,转载时请务必以超链接形式标明如下文章原始出处和作者信息及本声明
作者:xixi
出处: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转换)。这样说可能有点抽象,我们来看下面的例子。 [vb] view plaincopyprint?


  • '正确的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转换,也就是相当于做了如下事情: [vb] view plaincopyprint?


  • 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其实是像下面这样定义的: [vb] view plaincopyprint?


  • Type String
  • dwSize as long '后面实际数据的长度'
  • pData() as Integer '实际数据,每一个word就是一个字符,16位'
  • wEnd as Integer '字符串结束点/0/0,一个Unicode字符占双字节,不计入长度
  • end type

所以,VarPtr取到的地址是字符串变量的地址,也就是字符串变量指针,也就是存放"指向pData这个地址的指针"的变量的地址;而StrPtr取到的值就是指向pData地址的指针,也就是字符串缓冲区指针。所以,有时候人们会说,同一个字符串有两个指针,一个是字符串变量指针、另一个是字符串缓冲区指针。看下面的示例,可以更好的理解以上的说法: [vb] view plaincopyprint?


  • 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类型的。下面这个丑陋但清晰的图说明了一切。

我们还可以用以下的代码来验证上面的说法: [vb] view plaincopyprint?


  • 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函数
下面我们来熟悉一下本文重点讨论的这个函数。 [vb] view plaincopyprint?


  • 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字符串的内容啊。

继续学习后续内容前,不妨做以下练习,以确认你已经掌握本节内容。 [vb] view plaincopyprint?


  • 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), 4
  • Debug.Print pString1, StrPtr(string1), VarPtr(string1)
  • '结果:StrPtr(_tmp1)
  • CopyMemory pString1, string1, 4
  • Debug.Print pString1, StrPtr(string1), VarPtr(string1)
  • '结果:"ewoP"的ANSI编码
  • '从内部临时ANSI变量的字符串缓冲区取4个字节出来
  • '"Powe"是50-6F-77-65,取到的pString1里是(65776F50),正好倒过来
  • '因为Long型在书写时是大端在前的
  • CopyMemory pString1, ByVal string1, 4
  • Debug.Print pString1, StrPtr(string1), VarPtr(string1)
  • Debug.Print Hex(pString1)
  • End Sub

2.3 大端序和小端序
Test2_Ptr里的结果你都猜的正确么?我猜除了最后一个,应该都正确,呵呵。学习完以上的基础知识,下面这个语句的基本意思不难推测出来: [vb] view plaincopyprint?


  • '从内部临时ANSI变量的字符串缓冲区取头4个字节出来
  • CopyMemory pString1, ByVal String1, 4

但是有趣的是,头4个字节"Powe"对应的编码是50-6F-77-65,可是取到的pString1里是(65776F50),正好倒过来。这是为什么呢?看下面的解释:

(1)字符串的数据相当于Byte数组,它的字符是放在一个连续的内存块里的。第一个字符地址最低,最后一个字符最高。
(2)当用Long变量去拷贝字符串的部分内容的时候,Long的高字节对应它取到的最后一个字符,低字节则对应第一个字符。而在数字世界里,我们是把高字节写在左边、低字节写在右边的。所以我们从Long里去观察取到的字符,看起来是最后一个字符在左边、第一个字符在右边,好像倒了。


下面的例子可以帮助你更好的理解这一点: [vb] view plaincopyprint?


  • '测试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
[vb] view plaincopyprint?


  • '测试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, ByVal String1, 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 所在的位置,不当操作也会导致无法预期的结果甚至程序崩溃(见后续讨论)。

excel精英培训的微信平台,每天都会发送excel学习教程和资料。扫一扫明天就可以收到新教程
 楼主| 发表于 2012-2-21 23:12 | 显示全部楼层

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

本帖最后由 爱疯 于 2013-3-17 12:30 编辑

版权声明:可以任意转载,转载时请务必以超链接形式标明如下文章原始出处和作者信息及本声明
作者:xixi
出处:http://blog.csdn.net/slowgrace/archive/2009/09/14/4550116.aspx
本文来自此帖的冗长讨论,感谢Tiger_Zhao的全程指点和陈辉阿勇马云剑等很多朋友的热心参与。本文其他部分在:()、()、()。
第三节 经典错误代码集锦
好的,现在我们可以来看看VB妈妈好心没做好事的几个例子了。先说明一下,以下所有这些例子都来自这个帖子热心朋友的回复,它们都共享第一节里声明的模块级变量和常数。
3.1 我在0楼的代码——结果为何变短
[vb] view plaincopyprint?


  • Sub test5()
  • String1 = STR_E
  • String2 = String$(7, 0)
  • CopyMemory pString1, ByVal VarPtr(String1), 4
  • CopyMemory ByVal String2, ByVal pString1, 14
  • Debug.Print String2 '得到的不是PowerVB,而是“P o w e”?
  • End Sub

这个例子的运行过程如下:
(1)第1个CopyMemory得到的是String1的地址,并把这个地址作为源地址传给第2个CopyMemory
(2)第2个CopyMemory从String1的地址拷贝14个字节。由于VB中字符串的内部表示是Unicode,所以这时得到的14个字节的内容是“P-/0-o-/0-w-/0-e-/0-r-/0-V-/0-B-/0-”(注意,其中的“-”是我加入用来分割字符的,并不真的包括在字符串内存中)。
(3)由于CopyMemory的第一个参数是ByVal String2,是一个字符串,而VB会自动对API函数中的字符串参数做UA转换。所以,系统会把14个字节的Unicode空字符串String2转为7个字节的ANSI空字符串,并存在一个临时变量中,假设叫_tmp。
(4)然后系统把拷来的14字节数据“P-/0-o-/0-w-/0-e-/0-r-/0-V-/0-B-/0-”向_tmp拷。注意_tmp只有7字节,所以这里有溢出的危险。
(5)由于_tmp只有7字节,所以_tmp实际只得到头7个字节的数据,就是“P-/0-o-/0-w-/0-e-”
(6)最后VB要把ANSI字串再转回Unicode字串,并把转回的结果赋给String2。AU转换就是是将英文的 1 个字节扩张为 2 个字节,这样String2最终的内容是“P-/0-/0-/0-o-/0-/0-/0-w-/0-/0-/0-e-/0-”,用Debug打印出来,可不就是“P o w e”么?


下面这个表总结了上述过程:


另外,我还做了下图来说明上面的过程:

哎,这个图画得太杰出了。一目了然啊!你看,第②步的Unicode字符串硬是被当做ANSI字串在第③步被强行AU了,所以结果“浮肿”了很多(被插入好些空格)。

这个例子如何改对呢?中间两句不改,前后各改/增加一句,如下:

String2 = String$(LenB(String1), 0) '先确保_tmp2长度足够
CopyMemory pString1, ByVal VarPtr(String1), 4
CopyMemory ByVal String2, ByVal pString1, LenB(String1)
String2 = StrConv(String2, vbFromUnicode) '再做UA转换以抵消VB多做的一次AU转换


3.2 阿勇在11楼的代码——结果为何变胖
[vb] view plaincopyprint?


  • '阿勇11楼
  • Sub test8_Yong()
  • Dim pString1 As Long
  • String1 = STR_E
  • String2 = String$(14, 0)
  • CopyMemory pString1, VarPtr(String1), 4
  • CopyMemory String2, ByVal pString1, 4
  • Debug.Print String2, StrConv(String2, vbFromUnicode)
  • End Sub

运行上面的程序,你会发现string2的结果也是浮肿的。
(0)第1个CopyMemory获得String1变量指针,并存在pString1里;
(1)第2个CopyMemory首先从pString1里把String1缓冲区地址拷出来;然后把这个地址拷到临时字符串变量_tmp2里,也就是让_tmp2的字符串缓冲区指针指向String1的字符串缓冲区地址;
(2)之后VB把_tmp2的内容做AU转换,并把AU转换的内容拷到一个新分配的字符串缓冲区中,并把String2的字符串缓冲区指针指向这个新分配的地址。

看下面的图,第②步之后_tmp2字符串缓冲区里是来自String1的Unicode字符串,可是它还是在第③步被VB妈妈当初ANSI字符串强行AU,然后再倒手给String2,String2里得到的字符串可不就是浮肿的么:)

这个例子如何改对呢?最后加个String2 = StrConv(String2, vbFromUnicode)把VB多做的那一次AU转换抵消就可以了。

3.2.1 插播:字符串内存的初始化
Q:String2 = String$(14, 0)这一句可以不要么?
A:这一句是必要的,相当于给它初始申请内存。千万别用没分配内存的指针,否则很容易崩溃的。另外,String$(7, 0)相当于加入7个Chr(0)字符(vbNullChar), Space$(7)则相当于加入7个Chr(20)字符(空格)。由于VB字符串允许含Null字符,所以这两种初始化方法都可以的。

3.3 Modest在16楼的方法——错误的代码正确的结果
这个方法Modest自己起初的评语是“通俗易懂、还不出错”,打眼一望,我也很赞同这个评语。之后有些细节感觉想不通,请求继续解释。赵老虎分析完之后说“根本就是瞎猫碰上死老鼠”,“实际上是在胡乱操作内存”。刚看到这个评语,我觉得Tiger_Zhao这厮也太那个了,好歹老魏也是VB版一大虾啊。可是看完他的解释之后,我却发现这个评语还真中肯。这里要赞一下Modest,当真好气度,换了别人这么说我,甭管对错,我得先一蹦老高。闲话少说,咱们来看老魏的代码吧。核心的代码如下:
CopyMemory pString1, String1, 4
CopyMemory pString2, String2, 4
CopyMemory pString2, pString1, LenB (String1)

初看起来,这个代码貌似是分别得到两个字符串缓冲区的指针,然后把String1的字符串缓冲区拷给String2,结果也是正确的。当真“通俗易懂、还不出错”。但是细想想,你会发现这一切都不大靠谱:
(1)首先对于头2个CopyMemory而言,既然VB妈妈会做UA转换,那么pString1和pString2得到的应该分别是_tmp1和_tmp2的地址,而这两个临时变量在CopyMemory调用之后会被释放掉,也就是说pString1和pString2得到的其实是无效的地址(见下图)。

所以这两个语句应该改成这样;
CopyMemory pString1, VarPtr(String1), 4
CopyMemory pString2, VarPtr(String2), 4

(2)其次,就算我们把头两个语句改成上面这样,第3个CopyMemory真的在拷贝字符串缓冲区么?看出来了么?要拷贝字符串缓冲区,第3个语句应该加上ByVal,像这样:
CopyMemory ByVal pString2, ByVal pString1, LenB (String1)
(3)可是,诡异的是,就这么一段漏洞百出的代码,它的运行结果明明是正确的啊?这是为什么呢?看下面Tiger_Zhao的解释。
[vb] view plaincopyprint?


  • Sub test9_Modest()
  • Dim String1 As String
  • Dim String2 As String
  • Dim pString1 As Long
  • Dim pString2 As Long
  • '这 4 个变量每个长 4 字节,在栈上按地址从低到高为:
  • '| pString2 | pString1 | String2 | String1 |
  • String1 = "PowerVB"
  • String2 = Space$(Len(String1))
  • 'String1、String2 分别指向两个字符串
  • CopyMemory pString1, String1, 4
  • CopyMemory pString2, String2, 4
  • 'pString1 、pString2 值为已被释放的临时变量的地址,无所谓
  • CopyMemory pString2, pString1, LenB(String1)
  • '由于不加 ByVal,其实是从变量 pString1 的地址向变量 pString2 的地址复制 14 个字节
  • '等于直接操作栈内存,让变量向左复制(看后面插播的“覆盖模式”),于是
  • '| pString2 : 原 pString1 的值
  • '| pString1 : 原 String2 的字符串指针
  • '| String2 : 原 String1 的字符串指针
  • '| String1 : 不确定 |
  • Debug.Print String2
  • End Sub

3.3.1 插播1:关于栈
(1)变量都是存放在中的。这个内存由编译器自动分配释放。
(2)对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。所以先声明的变量的内存地址会比后声明的高。比如上面的示例里那样。

3.3.2 插播2:CopyMemory自动处理覆盖
A: CopyMemory是Copy还是Cut?原来地址中内存的内容还在么?
Q: 复制。源和目标内存不交叉,源不变;如果交叉,还会自动处理覆盖情况。
A: “自动处理覆盖情况”是什么意思?是说:万一交叉了,还会把被覆盖的复原,而且把目的地址挪开点么?
Q: 看下面的例子:
[vb] view plaincopyprint?


  • '如果有一个数组 a() : 00-01-02
  • 'a)
  • CopyMemory a(1), a(0), 2
  • '结果是 00-00-01,会避免:先用 a(0) 覆盖 a(1),然后再用 a(1) 覆盖 a(2),最终变成 00-00-00
  • 'b)
  • CopyMemory a(0), a(1), 2
  • '结果是 01-02-02,会避免:先用 a(2) 覆盖 a(1),然后再用 a(1) 覆盖 a(0),最终变成 02-02-

3.3.3 插播3:VB挂掉的话会有什么后果?
Modest这段代码如果把变量的次序换一下,VB就可能会挂掉。大家可以试一试:P
Q: 这种崩溃有可能波及到整个操作系统么?还是甭管我怎么瞎折腾,把VB关掉就万事大吉?
A: 不可能波及整个系统。因为VB之所以崩溃,就是因为系统搞的鬼。操作系统为了保护自己,会强行关闭它自认为潜在的“威胁”,所以会把VB搞掉。而一旦保护不了,而且出错,就会出现所谓的“蓝屏”。常规情况的话,重开VB就可以了。
Q:在调试状态下用错指针,会导致VB崩溃。那如果是编译成可执行程序,里面有错误的指针操作的话,可能会蓝屏么?
A:通常有进程保护,不会蓝屏。除非你的程序操作了系统级资源。

回复

使用道具 举报

 楼主| 发表于 2012-2-21 23:13 | 显示全部楼层

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

本帖最后由 爱疯 于 2013-3-17 12:23 编辑

版权声明:可以任意转载,转载时请务必以超链接形式标明如下文章原始出处和作者信息及本声明
作者:xixi
出处:http://blog.csdn.net/slowgrace/archive/2009/09/14/4550293.aspx
本文来自此帖的冗长讨论,感谢Tiger_Zhao的全程指点和陈辉阿勇马云剑等很多朋友的热心参与。本文其他部分在:()、()、()。
3.4 King06在10楼的代码——错误的代码正确的结果
再来看这段代码:
CopyMemory pString1, ByVal VarPtr(String1), 4CopyMemory String2, pString1, 14
可以说和老魏的代码有异曲同工之妙。看第2个CopyMemory,又是从一个Long型变量地址拷14字节,我打眼一望,判断结果是乱码,可是没成想结果居然是浮肿型的非乱码。不废话,比葫芦画瓢,看下面的代码注释吧:
[vb] view plaincopyprint?


  • Sub test3_King06()
  • Dim pString1 As Long
  • Dim String2 As String
  • Dim String1 As String
  • '这 3 个变量每个长 4 字节,在栈上按地址从低到高为:
  • '| pString1 | String2 | String1 |
  • String1 = STR_E
  • String2 = String$(7, 0)
  • 'Try three: Get the string's pointer from VarPtr
  • CopyMemory pString1, ByVal VarPtr(String1), 4 '得到String1字符串缓冲区指针
  • CopyMemory String2, pString1, 14
  • '由于UA转换,生成了新的临时变量_tmp2,所以现在栈上的内容如下
  • '| _tmp2 | pString1 | String2 | String1 |
  • '由于不加 ByVal,其实是从变量 pString1 的地址向变量 _tmp2 的地址复制 14 个字节
  • '等于直接操作栈内存,让变量向左复制,于是
  • '| _tmp2 : 原 pString1 的值,也就是String1字符串缓冲区指针,汗
  • '| pString1 : 原 String2 的字符串指针
  • '| String2 : 原 String1 的字符串指针
  • '| String1 : 不确定 |
  • '现在_tmp2里居然得着String1的字符串缓冲区指针了
  • '那么调用之后,强行AU转换,String2得到浮肿的结果,也完全可以理解了
  • Debug.Print String2 & "*" '得到的是 P o w e r V B *
  • End Sub

所以,10楼的代码也是属于瞎猫碰上死老鼠,“实际上是在胡乱操作内存”。不过这个代码没有老魏的代码狠。老魏的代码如果把变量次序换换就不能得到正确结果,甚至会VB挂掉。
而这个不会。这个变量次序无论怎么折腾,从pString1拷到的14字节中的头4字节总是String1的字符串缓冲区指针,也就是说_tmp2总是能得到这个指针,虽然后面的10字节就不定是啥了。这个代码貌似在拷字符串缓冲区,可实质上却是通过拷贝字符串缓冲区指针得到了基本正确的结果。这个乱啊……

3.5 我在34楼的代码——中英文混杂的字符串如何算字节数
看下面这段代码。
[vb] view plaincopyprint?


  • Dim String1C As String
  • Dim String2_7 As String
  • String1C = "我有点Slow"
  • String2_7 = String$(7, 0)
  • CopyMemory ByVal String2_7, ByVal String1C, 14
  • Debug.Print String2_7 & "*", Len(String2_7), LenB(String2_7)

这段代码的输出结果是我有点S*,没能完全地把“我有点Slow”这个字符串拷出来。这是为什么呢?其实用上面各例的原理完全可以理解这个结果。只要记住无论是Unicode还是ANSI编码,中文字符始终是维持双字节一个字符。而英文字符则是在UA转换时,将2个字节缩减为1个字节;AU转换时,将1个字节扩张为2个字节。所以,上面的CopyMemory的实际执行过程是:
(1)首先String1C“我有点Slow”被转换为ANSI字符串_tmp1共10字节,所以拷14字节的话,最后4字节是不确定的,不定是啥。
(2)其次String2_7被转换为ANSI字符串_tmp2共7字节。拷14字节到_tmp2,后7字节会溢出(这7字节中的最后3字节内容还不确定)。因此_tmp里只有前7字节,就是"我有点S"。
(3)最后VB把_tmp2转到String2,这是AU转换,只最后填个0,其他长度不变化。因此最后,LenB(String2)=8


3.5.1 暴强练习
这一小节我们来给出一堆上节代码的变体,并逐行给出解释(代码是我写的,赵老虎给出了超级清晰的逐行解释),来看看你是否全能理解。首先我们有如下声明和初始化:
[vb] view plaincopyprint?


  • Dim String1C As String, String1E As String
  • Dim String2_7 As String, String2_14 As String
  • String1C = "我有点Slow"
  • 'Unicode : 11-62-09-67-B9-70-53-00-6C-00-6F-00-77-00
  • 'Ansi : CE-D2-D3-D0-B5-E3-53-6C-6F-77
  • String1E = "WYDSlow"
  • 'Unicode : 57-00-59-00-44-00-53-00-6C-00-6F-00-77-00
  • 'Ansi : 57-59-44-53-6C-6F-77
  • String2_7 = String$(7, 0)
  • 'Unicode : 00-00-00-00-00-00-00-00-00-00-00-00-00-00
  • 'Ansi : 00-00-00-00-00-00-00
  • String2_14 = String$(14, 0)
  • 'Unicode : 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00
  • 'Ansi : 00-00-00-00-00-00-00-00-00-00-00-00-00-00

其次对以下每一对语句用如下语句在立即窗口观察结果:
Debug.Print String2_7 & "*", Len(String2_7), LenB(String2_7)Debug.Print String2_14 & "*", Len(String2_14), LenB(String2_14)
来看如下语句和注释你是否都能看懂吧:
[vb] view plaincopyprint?


  • '说明
  • 'XX - 源值确定 And 目标溢出
  • '?? - 源值不确定
  • '?X - 源值不确定 And 目标溢出
  • CopyMemory ByVal String2_7, ByVal String1C, 7 '例1 我有点S* 4 8
  • 'Ansi : CE-D2-D3-D0-B5-E3-53 ;复制 7 字节
  • 'Unicode : 11-62-09-67-B9-70-53-00
  • CopyMemory ByVal String2_14, ByVal String1C, 7 '例2 我有点S * 11 22
  • 'Ansi : CE-D2-D3-D0-B5-E3-53-00-00-00-00-00-00-00 ;复制 7 字节,后 7 字节不变
  • 'Unicode : 11-62-09-67-B9-70-53-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00
  • CopyMemory ByVal String2_7, ByVal String1E, 7 '例3 WYDSlow* 7 14
  • 'Ansi : 57-59-44-53-6C-6F-77 ;复制 7 字节
  • 'Unicode : 57-00-59-00-44-00-53-00-6C-00-6F-00-77-00
  • CopyMemory ByVal String2_14, ByVal String1E, 7 '例4 WYDSlow * 14 28
  • 'Ansi : 57-59-44-53-6C-6F-77-00-00-00-00-00-00-00 ;复制 7 字节,后 7 字节不变
  • 'Unicode : 57-00-59-00-44-00-53-00-6C-00-6F-00-77-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00
  • CopyMemory ByVal String2_7, ByVal String1C, 14 '例5 我有点S* 4 8
  • 'Ansi : CE-D2-D3-D0-B5-E3-53-XX-XX-XX-?X-?X-?X-?X ;复制 14 字节,后 7 字节溢出(丢弃)
  • 'Unicode : 11-62-09-67-B9-70-53-00
  • CopyMemory ByVal String2_14, ByVal String1C, 14 '例6 我有点Slow * 11 22
  • 'Ansi : CE-D2-D3-D0-B5-E3-53-6C-6F-77-??-??-??-?? ;复制 14 字节,后 4 字节不确定
  • 'Unicode : 11-62-09-67-B9-70-53-00-6C-00-6F-00-77-00-??... ;长度也不确定
  • CopyMemory ByVal String2_7, ByVal String1E, 14 '例7 WYDSlow* 7 14
  • 'Ansi : 57-59-44-53-6C-6F-77-?X-?X-?X-?X-?X-?X-?X ;复制 14 字节,后 7 字节溢出(丢弃)
  • 'Unicode : 57-00-59-00-44-00-53-00-6C-00-6F-00-77-00
  • CopyMemory ByVal String2_14, ByVal String1E, 14 '例8 WYDSlow * 14 28
  • 'Ansi : 57-59-44-53-6C-6F-77-??-??-??-??-??-??-?? ;复制 14 字节,后 7 字节不确定
  • 'Unicode : 57-00-59-00-44-00-53-00-6C-00-6F-00-77-00-??... ;长度也不确定
  • CopyMemory ByVal String2_7, ByVal String1C, 28 '例9 我有点S* 4 8
  • 'Ansi : CE-D2-D3-D0-B5-E3-53-XX-XX-XX-?X-?X-?X... ;复制 28 字节,后 21 字节溢出(丢弃)
  • 'Unicode : 11-62-09-67-B9-70-53-00
  • CopyMemory ByVal String2_14, ByVal String1C, 28 '例10 我有点Slow * 11 22
  • 'Ansi : CE-D2-D3-D0-B5-E3-53-6C-6F-77-??-??-??-??-?X... ;复制 28 字节,4 字节不确定,后 14 字节溢出(丢弃)
  • 'Unicode : 11-62-09-67-B9-70-53-00-6C-00-6F-00-77-00-??... ;长度也不确定
  • CopyMemory ByVal String2_7, ByVal String1E, 28 '例11 WYDSlow* 7 14
  • 'Ansi : 57-59-44-53-6C-6F-77-?X-?X-?X-?X-?X-?X-?X... ;复制 28 字节,后 21 字节溢出(丢弃)
  • 'Unicode : 57-00-59-00-44-00-53-00-6C-00-6F-00-77-00
  • CopyMemory ByVal String2_14, ByVal String1E, 28 '例12 WYDSlow * 14 28
  • 'Ansi : 57-59-44-53-6C-6F-77-??-??-??-??-??-??-??-?X... ;复制 28 字节,7 字节不确定,后 14 字节溢出(丢弃)
  • 'Unicode : 57-00-59-00-44-00-53-00-6C-00-6F-00-77-00-??... ;长度也不确定

注:输出结果放在每个语句后的注释中。我在每个字符串的末尾加了个“*”输出结果,以便于数字符串末尾的空格。另外,上面赵老虎的解释中给出的'ANSI编码,实际上指的是_tmp2接受拷贝后得到的值;'Unicode编码则是指VB在结束API调用后String2最后得到的值。

3.5.2 长度不确定?
看上面第2小节赵老虎的详细注释,会发现有好几行的注释说String2的长度不确定。比如例5。这很有点奇怪,String2_14我们已经初始化过长度了啊?而且输出的结果是11和22,也是对的(因为14个字节里有3个中文字符,所以要减3)。那是不是赵老虎弄错了呢?来看赵老虎的解释吧。

Unicode-Ansi 转换时,确定 _Tmp 长度为 14 字节。Ansi-Unicode 转换时,只对这 14 字节进行转换,而 String2_14 根据转换的长度,会重新分配一个字符串,并不是说将结果直接写回旧的字符串内存中。这可以通过调用前后 StrPtr(String2_14) 的变化来确认。所以 String2_14 的长度受到最后 4 个不确定字节的影响。看起来长度确定只不过你的代码中最后 4 字节正好全 0 而已。

下面的例子说明 4 个不确定字节的影响。为了不溢出,只复制 14 字节。为了指定后面的字节,String1C 的 Unicode-Ansi 转换就自己模拟了。
[vb] view plaincopyprint?


  • Dim ansiBytes() As Byte
  • ansiBytes = StrConv(String1C & String(4, 0), vbFromUnicode)
  • CopyMemory ByVal String2_14, ansiBytes(0), 14 '例13 我有点Slow * 11 22
  • 'Ansi : CE-D2-D3-D0-B5-E3-53-6C-6F-77-00-00-00-00 ;最后 4 字节全 0,相当于例6和例10
  • 'Unicode : 11-62-09-67-B9-70-53-00-6C-00-6F-00-77-00-00-00-00-00-00-00-00-00 ;转成4个字符
  • ansiBytes = StrConv(String1C & "中文", vbFromUnicode)
  • CopyMemory ByVal String2_14, ansiBytes(0), 14 '例14 我有点Slow中文* 9 18
  • 'Ansi : CE-D2-D3-D0-B5-E3-53-6C-6F-77-D6-D0-CE-C4 ;最后 4 字节为全中文
  • 'Unicode : 11-62-09-67-B9-70-53-00-6C-00-6F-00-77-00-2D-4E-87-65 ;转成2个字符
  • ansiBytes = StrConv(String1C & "a中b", vbFromUnicode)
  • CopyMemory ByVal String2_14, ansiBytes(0), 14 '例15 我有点Slowa中b* 10 20
  • 'Ansi : CE-D2-D3-D0-B5-E3-53-6C-6F-77-61-D6-D0-62 ;最后 4 字节为中英文混合
  • 'Unicode : 11-62-09-67-B9-70-53-00-6C-00-6F-00-77-00-61-00-2D-4E-62-00 ;转成3个字符

补充提问
Q:_tmp在AU转换后的长度是谁确定的?
A:那当然是VB确定的。CopyMemory根本不知道转换这回事。

Q:_tmp在AU转换后的长度是如何确定的?是根据chr(0)字符么?
A:BSTR标准中每个字符串的字符串缓冲区之前的4个字节记录了它的长度啊。

3.5.3 如何查看字符串的编码
细心的朋友也许看到上节的注释会问,这些ANSI编码和Unicode编码是如何得到的啊?这个简单,有两种方法。一种是用ASC函数(这篇博文的最后有详细介绍),另一种是把字符串赋值给Byte数组,然后逐个Hex查看。看以下的代码:
[vb] view plaincopyprint?


  • Public Sub GetUACode(str1 As String)
  • Dim aa() As Byte
  • Dim bb() As Byte
  • Dim i As Long
  • Dim strUMem As String, strAMem As String
  • Dim strU As String, strA As String
  • aa = str1
  • bb = StrConv(str1, vbFromUnicode)
  • '我有点Slow的编码
  • 'Unicode : 11-62-09-67-B9-70-53-00-6C-00-6F-00-77-00
  • 'Ansi : CE-D2-D3-D0-B5-E3-53-6C-6F-77
  • For i = 0 To UBound(aa)
  • strUMem = strUMem & Right$("0" & Hex$(aa(i)), 2) & "-"
  • Next i
  • strUMem = Left(strUMem, Len(strUMem) - 1)
  • Debug.Print strUMem
  • For i = 1 To Len(str1)
  • strU = strU & Hex(AscW(Mid(str1, i))) & "-"
  • Next i
  • strU = Left(strU, Len(strU) - 1)
  • Debug.Print strU
  • For i = 0 To UBound(bb)
  • strAMem = strAMem & Right$("0" & Hex$(bb(i)), 2) & "-"
  • Next i
  • strAMem = Left(strAMem, Len(strAMem) - 1)
  • Debug.Print strAMem
  • For i = 1 To Len(str1)
  • strA = strA & Hex(Asc(Mid(str1, i))) & "-"
  • Next i
  • strA = Left(strA, Len(strA) - 1)
  • Debug.Print strA
  • Debug.Print ChrW(&H6211) & ChrW(&H6709) & ChrW(&H70B9) & ChrW(&H53) & ChrW(&H6C) & ChrW(&H6F) & ChrW(&H77)
  • Debug.Print Chr(&HCED2) & Chr(&HD3D0) & Chr(&HB5E3) & ChrW(&H53) & ChrW(&H6C) & ChrW(&H6F) & ChrW(&H77)
  • End Sub

在立即窗口中键入GetUACode("我有点Slow"),可得到如下结果:
输出11-62-09-67-B9-70-53-00-6C-00-6F-00-77-00
6211-6709-70B9-53-6C-6F-77
CE-D2-D3-D0-B5-E3-53-6C-6F-77
CED2-D3D0-B5E3-53-6C-6F-77
我有点Slow
我有点Slow

第一行和第二行分别是用Byte数组和Asc函数得到的Unicode编码;第一行和第二行分别是用Byte数组和Asc函数得到的ANSI编码;第五行和第六行分别是用我们得到的Unicode编码和ANSI编码复原得到字符串。

从第二行的输出可以看出,汉字“我”的Unicode编码是6211,这从第五行的输出也可以验证。而从第一行的输出可以看出这个编码在内存里的存储方式是1162。想想我们前面提到的小端序,低位在前,这个结果就可以理解了。

可是的话,看ANSI编码又会费解了。比如观察第3行、第4行、第6行的输出,可以发现汉字“我”的ANSI编码CED2在内存里的存储方式是CED2,高位在前,怎么回事?不是说Intel架构的都是小端序么?

呵呵,这其实不是真正的大端序,而是因为多字符集编码的特殊规定导致的。因为对于汉字的ANSI编码而言,无所谓MSB, LSB。它就是把第一个字节理解为Leading Byte,第二个字节理解为另外的编码,所以它们在内存里的存放次序不能倒过来,否则就编码就不能得到正确解释了。(这个帖子里有相关讨论,感谢AisaC)

3.5.4 插播:Debug.Print String1是不是蕴含了UA转换?
Q:Debug.Print String1是不是蕴含了UA转换?你看,String1在内存里显示的英文都是2字节的,每个英文之后都有个空格,可是打印出来却没有了。
A:没有。显示和编码两回事,String1就是Unicode编码的,显示的时候会按Unicode编码解释并显示它,1个两字节的英文字符仍然会按1个字符显示。

3.6 替换指针法——最有效率的方法?!
直接把String2的字符串缓冲区指针指向String2的,这样只需要拷4个字节,不是很妙么?
(1)最直接的想法是像下面这样,不过它涉及隐含的UA/AU转换
CopyMemory String2, String1, 4 'UA/AU转换'
(2)如果不想要这样隐含的UA/AU转换,那就不要传字符串参数,像下面这样:
CopyMemory ByVal VarPtr(String2), ByVal VarPtr(String1), 4 '无需UA/AU转换'
也可以像下面这样
CopyMemory ByVal VarPtr(String2), StrPtr(String1), 4 '无需UA/AU转换'
但是不能像下面这样,自己想为什么吧,呵呵。
CopyMemory StrPtr(String2), StrPtr(String1), 4 '目标地址不对
附上完整的代码:
[vb] view plaincopyprint?


  • '直接拷贝字符串缓冲区指针-危险!
  • Sub test4_CpyBufPtr()
  • String1 = STR_C
  • String2 = String$(7, "0")
  • '以下3种都可以
  • CopyMemory String2, String1, 4 'UA/AU转换'
  • CopyMemory ByVal VarPtr(String2), ByVal VarPtr(String1), 4 '无需UA/AU转换'
  • CopyMemory ByVal VarPtr(String2), StrPtr(String1), 4 '无需UA/AU转换'
  • ' '这一种不可以
  • ' CopyMemory StrPtr(String2), StrPtr(String1), 4 '目标地址不对
  • Debug.Print String2 & "*"
  • End Sub

这看起来是最有效率的字符串复制的方法,只需要拷贝4个字节。但是,这样做是不对的,不仅是不对的,而且是危险的。因为,在VB中字符串和字符串缓冲区是一对一的,如果把两个字符串变量的缓冲区指向同一个地方,就会导致重复释放同一块内存,而这会引起不可预期的错误。而同时,由于String2原先的缓冲区不再被VB记住,这又会导致内存泄漏。不过,据赵老虎说,CopyMemory String2, String1, 4这种写法可以在参数声明为String时正常使用,待考证。

补充提问
Q:那如果我们把String2 = String$(7, "0")这一句拿掉,不给它显式初始化,还会导致重复释放内存和内存泄露么?
A:内存泄露是不会了。但是重复释放内存还是会的。因为在程序接受后,VB并不需要检查字符串变量是否被初始化,只要看有指针值,就要释放字符串缓冲区的空间。
回复

使用道具 举报

 楼主| 发表于 2012-2-21 23:13 | 显示全部楼层

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

本帖最后由 爱疯 于 2013-3-17 12:31 编辑

版权声明:可以任意转载,转载时请务必以超链接形式标明如下文章原始出处和作者信息及本声明
作者:xixi
出处:http://blog.csdn.net/slowgrace/archive/2009/09/14/4550530.aspx
本文来自此帖的冗长讨论,感谢Tiger_Zhao的全程指点和陈辉阿勇马云剑等很多朋友的热心参与。本文其他部分在:()、()、()。
第四节 如何用CopyMemory正确的拷贝字符串
分析了这么多有问题的代码,我们来看看如何用CopyMemory正确的拷贝字符串。假设String1 = “我有点Slow”
(1) 最简单的不会惹麻烦的方法是直接传地址。像下面这样

String2 = String$(Len(String1), 0) '14 bytes,和String1一样大小就OK了
CopyMemory ByVal StrPtr(String2), ByVal StrPtr(String1), LenB(String1)

这种方法由于不涉及UA/AU转换,要拷贝的就是Unicode字符串本身,所以字节数就取LenB(String1)就可以了,而String2的初始长度只要够接受String1的所有字符就行,所以就取Len(String1)。注意:1个是LenB,1个是Len,别弄混了。

(2) 或者你不嫌VB妈妈烦,像下面这样做

lngALen = LenB(StrConv(String1, vbFromUnicode))
String2 = String$(lngALen, 0)
CopyMemory ByVal String2, ByVal String1, lngALen

这里要拷贝的字节数和String2的初始化字符数都是用的lngALen,也即String1对应的ANSI字符串的字节数。这是因为传给CopyMemory的实际上是ANSI字符串临时变量_tmp1,所以要拷贝的字节数当然要按这个算;另一方面,String2初始成lngALen个0后(每个0字符占两个字节),它对应的ANSI字符串的字节数(每个0字符缩减为1个字节)就是lngALen,这样可以确保转换后的ANSI字符串_tmp2可以接收完整的_tmp1的字符串。
另外,要注意,在这种用法里,String2的初始长度不必等于它的最终长度。因为String2的字符串缓冲区在API函数调用完毕后要重新分配的,所以string2的初始长度只由_tmp1的LenB长度决定。附上完整的代码:
[vb] view plaincopyprint?


  • '拷贝字符串缓冲区的的方法:正确
  • Sub test7_CpyBuf()
  • Dim lngALen As Long
  • String1 = STR_C '14 bytes
  • lngALen = LenB(StrConv(String1, vbFromUnicode))
  • '以下两种方法都可以
  • '20 bytes,确保转换后的ANSI字符串_tmp2可以接收完整的_tmp1
  • String2 = String$(lngALen, 0)
  • CopyMemory ByVal String2, ByVal String1, lngALen
  • '没有UA/AU转换,一切轻松的多
  • String2 = String$(Len(String1), 0) '14 bytes,和String1一样大小就OK了'
  • CopyMemory ByVal StrPtr(String2), ByVal StrPtr(String1), LenB(String1)
  • Debug.Print String2 & "*"
  • End Sub

可以看到,推荐的方法有个共同的特征,就是目标地址和源地址的对称性。如果传Long就两者都传Long,如果传字符串就两者都传字符串,这样才能保证得到正确的结果;如果一个地址传Long,一个地址传字符串,那就会出问题,原因嘛,其实也就是因为VB妈妈对一个做了UA/AU转换,对另一个没做,导致互相对不上号,就要多加额外的语句来进行修正。
另外要注意正确处理好如下两个数字:
(1)CopyMemory的第3个参数byteLen。根据源地址参数是否涉及UA/AU转换而定,涉及的话就取对应的ANSI字符串的字节数,不涉及的话就直接LenB(String1)
(2)String2的初始字符数(注意是字符数不是字节数)。根据目标地址参数是否涉及UA/AU转换而定,涉及的话就保证对应的ANSI字符串字节数等于byteLen,不涉及的话就保证Unicode字符串字节数等于byteLen。

第五节 练习:这些回复你都能读懂么?
除了()、()详细讨论过的,还有许多朋友在这个帖子里给出了自己的修正代码,不妨看一遍当做练习巩固一下你对本文的学习成果。综述如下:
8楼,正确。
这个实际是对0楼代码的直接修正。先扩充String2的长度以确保对应的_tmp2长度足够;最后再做UA转换以抵消VB多做的一次AU转换。

9楼,正确。
不经过UA/AU转换,直接拷贝字符串缓冲区。

12楼,正确。
不经过UA/AU转换,直接拷贝字符串缓冲区。

15楼(1),正确。
目的和源同时做UA/AU转换,直接拷贝字符串缓冲区。

15楼(2),正确。
不经过UA/AU转换,直接拷贝字符串缓冲区。

22楼,错误。
同10楼。见248楼解释。

23楼,错误。
同10楼。见248楼解释。

38楼,正确。
直接传递字符串参数要得到正确结果,很重要的一点是要算对字节数。坏上帝专门写了个函数来算ASCI字符串所占的字节数,所以能够得到正确的结果。不过的话,我觉得这个GetStringLength函数完全可以简化为这一条语句:LenB(StrConv("我有点slow", vbFromUnicode))

46楼,正确。
同15楼第一种方法。

50楼(1),正确。
同8楼。

50楼(2),正确。
同15楼第一种方法。(但解释不完全准确:)

57楼,正确。
这个其实同15楼第一种方法,但是更通用。在直接传字符串参数的时候,要用ANSI字符串来计算字节。

58楼,不完全正确。
String2的初始化长度取为Len(String1)就可以了。

62楼,正确。
用ByVal VarPtr(ByVal String1),呵呵,真够绕的。

129楼,错误。
拷贝字符串缓冲区指针会导致重复释放内存。见134,141楼解释。

165楼,正确。同9楼。
但使用了Len和LenB函数,使得代码更通用了。

231楼,错误。
拷贝字符串缓冲区指针会导致重复释放内存和内存泄露。见134,141楼解释。

237楼,错误。
拷贝字符串缓冲区指针会导致重复释放内存和内存泄露。见134,141楼解释。

第六节 补充提问
6.1 如何查看内存
其实要熟悉CopyMemory函数的话,猜来猜去不如直接看内存。怎样看内存呢?可以把目标地址的内存CopyMemory 到数组中,然后逐个字节 Hex。分析内存经常需要这样做。
另外还可以用一些辅助调试工具。比如OllyDbg:RING3下的调试工具,可用于实时浏览各变量值或者寄存器的值。再比如:Ida和冰刃等静态反汇编的工具可以帮助你了解高级语言的实际执行动作。

6.2 在VB6中调用API使用字符串参数时有可能避开UA/AU转换么
VB调用API,总是要经过一个中介,msvbvm60.dll中的DllFunctionCall,这是UA/AU转换的始作俑者。如果利用自己定义的TLB绕过DllFunctionCall,字符串就不会进行UA/AU转换了。

6.3 所有的API函数都把字符串参数当做ANSI字符串处理么
答案是否。有些API函数是,有些不是。对于期待Unicode字符串的API函数(成为W版API),我们直接通过VB调用传给它字符串参数就会出问题。因为VB不会管API函数期待的是啥,都会一股脑地给转成ANSI字符串才传。所以,对于不同类型的API我们需要区别对待,详见这篇博文

第七节 全文摘要
在VB6中要使用CopyMemory函数正确的拷贝字符串有点啰嗦。本文通过分析若干段经典代码,介绍了这其中涉及的各方面的知识,包括:
(1)VB6对字符串参数的自动UA/AU转换机制
(2)关于VB中的字符串:BSTR结构、StrPtr和VtrPtr函数的意义、字符串内存的初始化、查看字符串编码的方法、VB6释放字符串变量的方法等。
(3)关于CopyMemory函数;它的各种不同形式参数的含义、它如何自动处理覆盖、VB6调用CopyMemory后如何确定String2的长度等。
(4)其他内容:大端序、小端序、ANSI编码的内存结构、栈、关于VB崩溃的处理、查看内存的方法、用Tlb避开UA/AU转换的方法等。


附:经典代码段索引

第一节:详细图示、“表”示and“文字”示VB6对字符串参数的自动UA/AU转换机制。
第三节:
例1:非对称的参数形式导致非对称的UA/AU转换,之后初始化字节数不够导致结果被截短;
例2:非对称的参数形式导致非对称的UA/AU转换,得到“浮肿”的结果;
例3:直接用String1传参数带来的危险后果。
例4,本该传值却传了地址之后的后果;
例5,中英文混杂的字符串如何算字节数;
例6,危险的替换指针法。

第四节:正确方法剖析。
第五节:超多其他代码的简析。

回复

使用道具 举报

发表于 2012-2-25 18:44 | 显示全部楼层
很好的,值得收藏,不断学习,不断进一步提高水平
回复

使用道具 举报

发表于 2012-3-12 16:13 | 显示全部楼层
謝謝提供學習下載看看
回复

使用道具 举报

发表于 2012-5-25 05:36 | 显示全部楼层
很好的,值得收藏,不断学习
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

小黑屋|手机版|Archiver|Excel精英培训 ( 豫ICP备11015029号 )

GMT+8, 2024-5-29 09:53 , Processed in 0.255378 second(s), 9 queries , Gzip On, Yac On.

Powered by Discuz! X3.4

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表