'正确的ByVal String的用法
OptionExplicit
ConstSTR_E ="PowerVB"
PrivateString1AsString
PrivateString2AsString
PrivatepString1AsLong
Subtest7()
DimString1AsString
DimString2AsString
' Dim _tmp1 As String, _tmp2 As String
String1 = "PowerVB"'14 bytes
String2 = String$(7, 0)'14 bytes
CopyMemory ByValString2,ByValString1, 7
' _tmp1 = StrConv(String1, vbFromUnicode) '7 bytes
' _tmp2 = StrConv(String2, vbFromUnicode) '7 bytes
' CopyMemory ByVal _tmp2, ByVal _tmp1, 7
Debug.Print String2
EndSub
如上图所示,当我们在VB中调用CopyMemory ByVal String2, ByVal String1, 7的时候发生了如下事情:
①首先VB妈妈帮我们对String1和String2自动做了UA转换,也就是相当于做了如下事情:
Dim_tmp1AsString, _tmp2AsString
_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其实是像下面这样定义的:
TypeString
dwSize as long '后面实际数据的长度'
pData() as Integer'实际数据,每一个word就是一个字符,16位'
wEnd as Integer'字符串结束点\0\0,一个Unicode字符占双字节,不计入长度
end type
所以,VarPtr取到的地址是字符串变量的地址,也就是字符串变量指针,也就是存放"指向pData这个地址的
指针"的变量的地址;而StrPtr取到的值就是指向pData地址的指针,也就是字符串缓冲区指针。所以,有时候人们会说,同一个字符串有两个指针,一
个是字符串变量指针、另一个是字符串缓冲区指针。看下面的示例,可以更好的理解以上的说法:
OptionExplicit
'From Myjian
'http://topic.csdn.net/u/20090901/09/dddf35aa-7838-4415-85b2-222358422d81_2.html 187楼
PrivateDeclareSubCopyMemoryLib"kernel32.dll"Alias"RtlMoveMemory"( _
ByValDestinationAsLong, _
ByValSourceAsLong, _
ByValLengthAsLong)
SubTestBstr()
Dimstr1AsString, JAsLong, KAsLong
str1 = "IamSlow慢"
Debug.Print VarPtr(str1) '得到变量本身的地址
CallCopyMemory(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))
EndSub
PrivateFunctionGetBSTRFromPtr(ByVallpStrAsLong)AsString
'从指针得到BSTR字符串
DimInStrLenAsLong, OutStrArr()AsByte
InStrLen = LenPtr(lpStr) '得到输入字符串的长度
ReDimOutStrArr(InStrLen - 1)
CallCopyMemory(VarPtr(OutStrArr(0)), lpStr, InStrLen)
GetBSTRFromPtr = OutStrArr
EndFunction
PrivateFunctionLenPtr(ByVallpStrAsLong)AsLong
'根据指针取BSTR长度
DimInStrLenAsLong
IflpStr = 0ThenExitFunction
CopyMemory VarPtr(InStrLen), lpStr - 4, 4 '得到输入字符串的长度
LenPtr = InStrLen
EndFunction
注意,上面的LenPtr函数,是直接通过从字符串缓冲区的长度前缀中拷贝内存得到的。这其实是BSTR指针的特点,你只要保证传入的指针是BSTR指针就可以这样得到字符串的长度。
BSTR是COM中的一种字符串标准,与普通字符串的最大不同在于有长度前缀,所以可以包含NULL在内的字符串。而如果没有长度前缀,字符串中有NULL就会被认为是结束了,从而截断。VB中的字符串就是BSTR类型的。下面这个丑陋但清晰的图说明了一切。
我们还可以用以下的代码来验证上面的说法:
SubtestNull()
Dimstr1AsString
str1 = "aa"& Chr(0) &"bb"
Debug.Print str1, Len(str1), LenB(str1)
MsgBox str1
EndSub
可以看出,VB中的字符串中间可以含有NULL字符,但是MsgBox这样的函数由于是封装的API函数MessageBox,所以它会按照C字符串的标准来解释字符串长度,因此会把aa以后的字符截掉。
2.2 CopyMemory函数
下面我们来熟悉一下本文重点讨论的这个函数。
DeclareSubCopyMemoryLib"kernel32"Alias"RtlMoveMemory"_
(pDest AsAny, pSourceAsAny,ByValbyteLenAsLong)
这个函数的功能是从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字符串的内容啊。
继续学习后续内容前,不妨做以下练习,以确认你已经掌握本节内容。SubTest2_Ptr()
string1 = STR_E
'结果:StrPtr((string1)
'把VarPtr(String1)的值作为地址,拷这个地址里的值出来:)
CopyMemory pString1, ByValVarPtr(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, ByValstring1, 4
Debug.Print pString1, StrPtr(string1), VarPtr(string1)
Debug.Print Hex(pString1)
EndSub
2.3 大端序和小端序
Test2_Ptr里的结果你都猜的正确么?我猜除了最后一个,应该都正确,呵呵。学习完以上的基础知识,下面这个语句的基本意思不难推测出来:
'从内部临时ANSI变量的字符串缓冲区取头4个字节出来
CopyMemory pString1, ByValString1, 4
但是有趣的是,头4个字节"Powe"对应的编码是50-6F-77-65,可是取到的pString1里是(65776F50),正好倒过来。这是为什么呢?看下面的解释:
(1)字符串的数据相当于Byte数组,它的字符是放在一个连续的内存块里的。第一个字符地址最低,最后一个字符最高。
(2)
当用Long变量去拷贝字符串的部分内容的时候,Long的高字节对应它取到的最后一个字符,低字节则对应第一个字符。而在数字世界里,我们是把高字节写
在左边、低字节写在右边的。所以我们从Long里去观察取到的字符,看起来是最后一个字符在左边、第一个字符在右边,好像倒了。
下面的例子可以帮助你更好的理解这一点:
'测试Long在内存的存储顺序和拷贝顺序
Subtest11()
DimLong1AsLong
DimLong2AsLong
DimiAsLong
Long1 = &H1020304
Debug.Print Hex(Long1)
Fori = 1To4
CopyMemory Long2, Long1, i
Debug.Print Hex(Long2)
Nexti
EndSub
输出
1020304
4
304
20304
1020304
'测试String在内存的存储顺序和拷贝顺序
Subtest12()
DimString1AsString
DimString2AsString
DimiAsLong
String1 = "1234"
String2 = String$(4, 0)
Debug.Print String1
Fori = 1To4
CopyMemory ByValString2,ByValString1, i
Debug.Print String2
Nexti
EndSub
输出
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 所在的位置,不当操作也会导致无法预期的结果甚至程序崩溃(见后续讨论)。