字符串存储格式影响着编程使用,C格式字符串是以 \0 作为结束符,其长度可以由此计算,而VB格式的字符串其长度存于字符串前缀之中。不仅如此,字符串编码方式 ansi, unicode, widestring, dbcs, sbcs 这些概念也影响着长度计算。下面用VB6程序对它们进行剖析验证。
解释:首先定义局部字符串并赋值,在 Text1中显示字符串的VarPtr,这是字符变量的在STACK中的地址,然后将字符串作为参数调用下面的过程,过程中用了 ByRef A as string, 在过程中 Text2显示变量A的地址VarPtr。
Private Sub Command1_Click()
Dim Astring As String
Astring = "Hello World!"
Text1.Text = CVar(VarPtr(Astring))
Call VarTest1(Astring)
Text6.Text = Astring
End Sub
Sub VarTest1(ByRef A As String)
Text2.Text = CVar(VarPtr(A))
A = "It's Changed!"
End Sub
运行上述程序,结果如下。它验证了Byref传递的是字符串变量的地址,相当于C中的别名,变量A是变量Astring的别名,它们有相同的地址。即然地址相同,那对A赋值,Astring会发生同样的变化,因为它们是同一地址。
Private Sub Command2_Click()
Dim Astring As String
Dim vaddr As Long
Dim caddr As Long
Dim I As Integer
Astring = "Hello World!"
vaddr = VarPtr(Astring): Text3.Text = CVar(vaddr)
Text1.Text = Right$(("0" + Hex$(Peek(vaddr + 3))), 2) _
+ Right$(("0" + Hex$(Peek(vaddr + 2))), 2) _
+ Right$(("0" + Hex$(Peek(vaddr + 1))), 2) _
+ Right$(("0" + Hex$(Peek(vaddr + 0))), 2)
Text2.Text = Val("&H" + Text1.Text)
caddr = StrPtr(Astring): Text4.Text = CVar(caddr)
Text5.Text = ""
For I = 1 To LenB(Astring)
Text5.Text = Text5.Text + Chr$(Peek(caddr + I - 1))
Next I
End Sub
运行结果:通过peek得到varptr处存的值是 16进制0716A1CC, 即10进制118923724,与strptr的值是完全一样的,从这个地址直接 peek 出来即是字符串的实际字符。
下面Command3先在Text1中显示 VarPtr值,然后把这个值以 byval 的方式传送给下面的sub过程,sub过程得到是 varptr 的值,然后用peek计算出varptr下面的内存值,再然后直接将另一字符串"It's changed!"移换掉原来的 "Hello World!"
Private Sub Command3_Click()
Dim Astring As String
Dim hAstr As Long
Astring = "Hello World!"
hAstr = VarPtr(Astring)
Text1.Text = CVar(hAstr)
Call VarTest2(hAstr)
Text6.Text = Astring
End Sub
Sub VarTest2(ByVal B As Long)
Dim Bstring As String
Dim DestAddr As Long
Bstring = "It's Changed!"
Text2.Text = CVar(B)
DestAddr = Val("&H" + (Right$(("0" + Hex$(Peek(B + 3))), 2) _
+ Right$(("0" + Hex$(Peek(B + 2))), 2) _
+ Right$(("0" + Hex$(Peek(B + 1))), 2) _
+ Right$(("0" + Hex$(Peek(B + 0))), 2)))
Call MoveMemory(DestAddr, StrPtr(Bstring), LenB(Bstring))
End Sub
结果是下面的样子。label贴了没改,不影响程序代码。
在过程中显示Byval 后字符串的varptr和strptr。如果调用过程前显示 varptr和strptr的话(可以自己补上两句,程序中没写),对比会发现过程中用的源字符串的副本,内容相同,但varptr和strptr不同。
Private Sub Command4_Click()
Dim Astring As String
Astring = "Hello World!"
Text1.Text = Astring
Call ByvalTest(Astring)
End Sub
Sub ByvalTest(ByVal C As String)
Text2.Text = CVar(VarPtr(C))
Text6.Text = CVar(StrPtr(C))
End Sub
Private Sub Command5_Click()
Dim Bstring As String
Dim caddr As Long
Dim PreString As Long
Bstring = "123456789"
caddr = StrPtr(Bstring)
PreString = Val("&H" + (Right$(("0" + Hex$(Peek(caddr - 1))), 2) _
+ Right$(("0" + Hex$(Peek(caddr - 2))), 2) _
+ Right$(("0" + Hex$(Peek(caddr - 3))), 2) _
+ Right$(("0" + Hex$(Peek(caddr - 4))), 2)))
Text1.Text = CVar(PreString)
End Sub
首先定义一个字符串“123456789”,然后得到它的 strptr 于 caddr变量中,以这个值向前按big endian方式peek读取内存中的四个字节,得到的值显示在 Text1中。值是18,不是9,说明在内存中是按unicode存储的。
mbcs是多字节编码,现在用的是dbcs双字节编码,早期用的是sbcs单字节编码,最早是ansi编码,现在基本上统一到 unicode 的UTF16
是期IBM PC时代是没有汉字的,用的都是ansi码表, 一个字节就代表一个字符。后来使用汉字,就在ansi后面扩展双字节汉字,形成单了节与双字节混合的ansi码表。再后来统一用双字节unicode的UTF16编码。下面引用一段程序验证说明这种情况。
Private Sub Command6_Click()
Dim MyString, MyLen
MyString = "中国c"
' Where "中" and "国" are DBCS and "c" is SBCS.
MyLen = Len(MyString): Debug.Print MyLen
' Returns 3 - 3 characters in the string. 1+1+1
MyLen = LenB(MyString): Debug.Print MyLen
' Returns 6 - 6 bytes used for Unicode. 2+2+2
MyLen = LenMbcs(MyString): Debug.Print MyLen
' Returns 5 - 5 bytes used for ANSI. 2+2+1
End Sub
Function LenMbcs(ByVal str As String)
LenMbcs = LenB(StrConv(str, vbFromUnicode))
End Function
字符串是双字节与单字节混合的 “中国c”,用len得到的结果是 1+1+1=3, 表明是3个字符。用lenb得到是unicde值,每个字符,不分单双字节,全部按双字节计算,因此是2+2+2 = 6。用lenb(strconv(str, vbFromUnicode))变换出的是ansi值,汉字是2个字节,英文字母是1个字节,所以值是2+2+1=5。上面验证过了,从字符串前缀peek得到的值字符串“123456789”是18,它是按unicode存储的。
应付字符串的不同编码,VB这是上述处理的,Delphi设了专门的ansistring, string和widestring类型,c本身是\0结尾的直接就出字符串长度,但它要用BString的话也必须计算转换。面对如此烦锁的转换,不同语言之间传递参数时一般不建议使用字符串,VB可以使用byte数组,delphi可以使用pchar,通过直接传送单字节数组地址比字符串更方便。总之,传送地址,由被call程序根据地址取数据是比较方便的。这些内容好理解不好表达,感觉受水平所限没写好没写透。
差点忘了,poke和peek函数我放到下面。vb要用的话,把它们放在一个 module 里。
Public Declare Sub MoveMemory _
Lib "KERNEL32" _
Alias "RtlMoveMemory" _
(ByVal Dest As Any, _
ByVal Source As Any, _
ByVal Bytes As Long)
Public Declare Sub MoveString _
Lib "KERNEL32" _
Alias "RtlMoveMemory" _
(Dest As Any, _
ByVal Source As String, _
ByVal Bytes As Long)
Public Sub Poke(MemLoc&, B As Byte)
Call MoveMemory(MemLoc, VarPtr(B), 1)
End Sub
Public Function Peek(MemLoc&) As Byte
Dim Q As Byte
Call MoveMemory(VarPtr(Q), MemLoc, 1)
Peek = Q
End Function