linux网络编程 copymemory,在VB6中用CopyMemory拷贝字符串的种种猫腻(一)

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

话说VB6是个很认真细致的妈妈,它会悄没声地帮你做很多事。今天我们来说的是VB6在API调用时自动对字符串参数进行转换的“好人好事”。

第一节 体贴的VB妈妈

我们知道,在VB6中字符串都是以Unicode编码存在的,而Windows API函数中很多时候用的是ANSI字符串。VB妈妈害怕程序员们累着,所以在VB程序员调用API时,会自动的对其中的字符串参数做Unicode到ANSI的转换(以下简称

UA

转换),在API调用结束后会再把字符串参数做ANSI到Unicode的转换(以下简称AU转换)。这样说可能有点抽象,我们来看下面的例子。

'正确的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

转换,也就是相当于做了如下事情:

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其实是像下面这样定义的:

Type String

dwSize as long '后面实际数据的长度'

pData() as Integer '实际数据,每一个word就是一个字符,16位'

wEnd as Integer '字符串结束点/0/0,一个Unicode字符占双字节,不计入长度

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

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

我们还可以用以下的代码来验证上面的说法:

Sub testNull()

Dim str1 As String

str1 = "aa" & Chr(0) & "bb"

Debug.Print 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

'结果: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),VarPtr(string1)

'结果:StrPtr(_tmp1)

CopyMemory pString1,string1,VarPtr(string1)

'结果:"ewoP"的ANSI编码

'从内部临时ANSI变量的字符串缓冲区取4个字节出来

'"Powe"是50-6F-77-65,取到的pString1里是(65776F50),正好倒过来

'因为Long型在书写时是大端在前的

CopyMemory pString1,ByVal string1,VarPtr(string1)

Debug.Print Hex(pString1)

End Sub

2.3 大端序和小端序

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

'从内部临时ANSI变量的字符串缓冲区取头4个字节出来

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

(1)字符串的数据相当于Byte数组,它的字符是放在一个连续的内存块里的。第一个字符地址最低,最后一个字符最高。

(2)当用Long变量去拷贝字符串的部分内容的时候,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

1020304

4

304

20304

1020304

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

WEI WAN DAI XU

总结

如果觉得编程之家网站内容还不错,欢迎将编程之家网站推荐给程序员好友。

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。

小编个人微信号 jb51ccc

喜欢与人分享编程技术与工作经验,欢迎加入编程之家官方交流群!

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值