版权声明:可以任意转载,转载时请务必以超链接形式标明如下文章原始出处和作者信息及本声明
作者:xixi
出处:http://blog.csdn.net/slowgrace/archive/2009/09/14/4550293.aspx
本文来自此帖的冗长讨论,感谢Tiger_Zhao的全程指点和陈辉、阿勇、马云剑等很多朋友的热心参与。本文其他部分在:(一)、(二)、(四)。
3.4 King06在10楼的代码——错误的代码正确的结果
再来看这段代码:
CopyMemory pString1, ByVal VarPtr(String1), 4
CopyMemory String2, pString1, 14
可以说和老魏的代码有异曲同工之妙。看第2个CopyMemory,又是从一个Long型变量地址拷14字节,我打眼一望,判断结果是乱码,可是没成想结果居然是浮肿型的非乱码。不废话,比葫芦画瓢,看下面的代码注释吧:
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楼的代码——中英文混杂的字符串如何算字节数
看下面这段代码。
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 暴强练习
这一小节我们来给出一堆上节代码的变体,并逐行给出解释(代码是我写的,赵老虎给出了超级清晰的逐行解释),来看看你是否全能理解。首先我们有如下声明和初始化:
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)
来看如下语句和注释你是否都能看懂吧:
'说明 '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 转换就自己模拟了。
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查看。看以下的代码:
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 '目标地址不对
附上完整的代码:
'直接拷贝字符串缓冲区指针-危险! 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并不需要检查字符串变量是否被初始化,只要看有指针值,就要释放字符串缓冲区的空间。
WEI WAN DAI XU