-
- 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
-
-
- String1 = "PowerVB"
- String2 = String$(7, 0)
-
- CopyMemory ByVal String2, ByVal String1, 7
-
-
-
-
-
- Debug.Print String2
- End Sub

如上圖所示,當(dāng)我們在VB中調(diào)用CopyMemory
ByVal String2,
ByVal String1, 7的時候發(fā)生了如下事情:
①首先VB媽媽幫我們對String1和String2自動做了
UA轉(zhuǎn)換,也就是相當(dāng)于做了如下事情:
- Dim _tmp1 As String, _tmp2 As String
-
- _tmp1 = StrConv(String1, vbFromUnicode)
- _tmp2 = StrConv(String2, vbFromUnicode)
也就是說,兩個14字節(jié)的Unicode字符串現(xiàn)在被存在兩個7字節(jié)的ANSI字符串里了。
②然后CopyMemory函數(shù)就做實際的拷貝動作。注意,這時CopyMemory得到的參數(shù)不是String1, String2了,而是VB媽媽傳給它的_tmp1, _tmp2了。所以,實際上,CopyMemory同學(xué)是在這么干活:CopyMemory ByVal _tmp2, ByVal _tmp1, 7。也就是,從_tmp1的緩沖區(qū)拷貝7個字節(jié)到_tmp2的緩沖區(qū)。
③CopyMemory同學(xué)干完活,VB媽媽又細(xì)心地做善后工作。它把_tmp2的內(nèi)容再轉(zhuǎn)成14字節(jié)的Unicode字符串,并把它給String2。
PS:
(1) 文字中帶圈標(biāo)號1與圖上的1是一一對應(yīng)的。
(2) 注意①和③是VB自動進(jìn)行的,和CopyMemory函數(shù)無關(guān)。也就是VB只要看到API函數(shù)調(diào)用中涉及到字符串參數(shù),就會自動做這種轉(zhuǎn)換!
看完上面的例子,也許你就會對VB媽媽這種細(xì)致體貼的勁頭有點體會了。但是正如現(xiàn)實生活中媽媽的過多干涉會給我們帶來困擾一樣,VB媽媽的這種體貼有時也會帶來讓人哭笑不得的效果。
第二節(jié) 基礎(chǔ)知識
在展示VB媽媽的各種“杰作”之前,我們先來準(zhǔn)備一些基礎(chǔ)知識。
2.1 VB中字符串的存儲結(jié)構(gòu)
當(dāng)你在VB里聲明了一個String型的變
量,比如:Dim str1 As
String。這個Str1本身其實是一個4字節(jié)的Long型,里面存的是一個指針,指向的是實際字符串的緩沖區(qū)開始地址,這個開始地址前面4字節(jié)里存放
的是這個緩沖區(qū)的長度,單位為字節(jié)。也就是,VB里的String其實是像下面這樣定義的:
- Type String
- dwSize as long
- pData() as Integer
- wEnd as Integer
- end type
所以,VarPtr取到的地址是字符串變量的地址,也就是字符串變量指針,也就是存放"指向pData這個地址的
指針"的變量的地址;而StrPtr取到的值就是指向pData地址的指針,也就是字符串緩沖區(qū)指針。所以,有時候人們會說,同一個字符串有兩個指針,一
個是字符串變量指針、另一個是字符串緩沖區(qū)指針。看下面的示例,可以更好的理解以上的說法:
- Option Explicit
-
-
-
-
- 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)
- 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
-
- 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
-
- Dim InStrLen As Long
-
- If lpStr = 0 Then Exit Function
- CopyMemory VarPtr(InStrLen), lpStr - 4, 4
- LenPtr = InStrLen
- End Function
注意,上面的LenPtr函數(shù),是直接通過從字符串緩沖區(qū)的長度前綴中拷貝內(nèi)存得到的。這其實是BSTR指針的特點,你只要保證傳入的指針是BSTR指針就可以這樣得到字符串的長度。
BSTR是COM中的一種字符串標(biāo)準(zhǔn),與普通字符串的最大不同在于有長度前綴,所以可以包含NULL在內(nèi)的字符串。而如果沒有長度前綴,字符串中有NULL就會被認(rèn)為是結(jié)束了,從而截斷。VB中的字符串就是BSTR類型的。下面這個丑陋但清晰的圖說明了一切。
我們還可以用以下的代碼來驗證上面的說法:
- 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這樣的函數(shù)由于是封裝的API函數(shù)MessageBox,所以它會按照C字符串的標(biāo)準(zhǔn)來解釋字符串長度,因此會把a(bǔ)a以后的字符截掉。
2.2 CopyMemory函數(shù)
下面我們來熟悉一下本文重點討論的這個函數(shù)。
- Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" _
- (pDest As Any, pSource As Any, ByVal byteLen As Long)
這個函數(shù)的功能是從pSource拷貝byteLen個字節(jié)的數(shù)據(jù)到pDest,其中源地址和目標(biāo)地址都是聲明為Any類型。下面是CopyMemory對不同形式參數(shù)的理解:
(1) 傳一個變量給pSource,那么源地址就是變量所在的地址
(2) 以ByVal形式傳一個變量給pSource,那么源地址就是變量的值
(3) 字符串變量的值是個指針,指向字符串緩沖區(qū)的地址,也就是StrPtr(String1)。因此,以ByVal形式傳一個字符串變量給pSource,那么源地址就是字符串變量的值,也就是字符串緩沖區(qū)的地址。
下表總結(jié)了幾種常見的傳參數(shù)給CopyMemory的形式:
注:
(1)取到的內(nèi)容根據(jù)byteLen實際規(guī)定的字節(jié)數(shù)的多少,可能有所不同,這里只是個大概。
(2)帶高亮的兩行,VB對字符串參數(shù)做了自動的UA轉(zhuǎn)換,所以實際的CopyMemory動作針對的是由String1轉(zhuǎn)換得到的ANSI字符串_tmp1而進(jìn)行的。
(3)字節(jié)數(shù)那
一列給出了要取到有效的數(shù)據(jù)byteLen參數(shù)可以使用的數(shù)字范圍。簡單的說,如果pSource的參數(shù)是字符串類型的話,那么byteLen的字節(jié)數(shù)要
取為String1對應(yīng)的ANSI字符串的長度。要理解這個也容易,你只要記住CopyMemory這時候?qū)嶋H上是對ANSI字符串做操作就可以了。而如
果不發(fā)生字符串轉(zhuǎn)換的話,像表里第4行,那么你就要拷貝String1的LebB長度。這也好理解,不發(fā)生轉(zhuǎn)換的話,CopyMemory實際上是在直接
拷貝Unicode字符串的內(nèi)容啊。
繼續(xù)學(xué)習(xí)后續(xù)內(nèi)容前,不妨做以下練習(xí),以確認(rèn)你已經(jīng)掌握本節(jié)內(nèi)容。
- Sub Test2_Ptr()
- string1 = STR_E
-
-
-
- CopyMemory pString1, ByVal VarPtr(string1), 4
- Debug.Print pString1, StrPtr(string1), VarPtr(string1)
-
-
-
- CopyMemory pString1, VarPtr(string1), 4
- Debug.Print pString1, StrPtr(string1), VarPtr(string1)
-
-
- CopyMemory pString1, string1, 4
- Debug.Print pString1, StrPtr(string1), VarPtr(string1)
-
-
-
-
-
- CopyMemory pString1, ByVal string1, 4
- Debug.Print pString1, StrPtr(string1), VarPtr(string1)
- Debug.Print Hex(pString1)
- End Sub
2.3 大端序和小端序
Test2_Ptr里的結(jié)果你都猜的正確么?我猜除了最后一個,應(yīng)該都正確,呵呵。學(xué)習(xí)完以上的基礎(chǔ)知識,下面這個語句的基本意思不難推測出來:
-
- CopyMemory pString1, ByVal String1, 4
但是有趣的是,頭4個字節(jié)"Powe"對應(yīng)的編碼是50-6F-77-65,可是取到的pString1里是(65776F50),正好倒過來。這是為什么呢?看下面的解釋:
(1)字符串的數(shù)據(jù)相當(dāng)于Byte數(shù)組,它的字符是放在一個連續(xù)的內(nèi)存塊里的。第一個字符地址最低,最后一個字符最高。
(2)
當(dāng)用Long變量去拷貝字符串的部分內(nèi)容的時候,Long的高字節(jié)對應(yīng)它取到的最后一個字符,低字節(jié)則對應(yīng)第一個字符。而在數(shù)字世界里,我們是把高字節(jié)寫
在左邊、低字節(jié)寫在右邊的。所以我們從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
-
- 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
這里要補(bǔ)充一些關(guān)于字節(jié)序的知識。Big Endian和Little Endian是CPU處理多字節(jié)數(shù)的不同方式。例如“漢”字的Unicode編碼是6C49。那么寫到文件里時,究竟是將6C寫在前面,還是將49寫在前面?如果將6C寫在前面,就是big endian,譯作大端序。還是將49寫在前面,就是little endian,譯作小端序。
“endian”這個詞出自《格列佛游記》。小人國的內(nèi)戰(zhàn)就源于吃雞蛋時是究竟從大頭(Big-Endian)敲開還是從小頭(Little-Endian)敲開,由此曾發(fā)生過六次叛亂,其中一個皇帝送了命,另一個丟了王位。
大端序指的是:從最大的一端開始存儲(從低地址存起),MSB的地址最低。
小端序指的是:從最小的一端開始存儲(從低地址存起),MSB的地址最高。
像我們上面測試的Long,它的最高位是1,最低位是4,從拷貝出來的結(jié)果可以看出來4在最低位,也就是從小端開始存儲,所以我們說它是小端序的。實際上Intel處理器都是小端序的。
而在Big-endian處理器(如蘋果
Macintosh電腦)上建立的Unicode文件中的文字位元組(存放單位)排列順序,與在Intel處理器上建立的文件的文字位元組排列順序相反。
最重要的位元組(MSB)擁有最低的地址,且會先儲存文字中較大的一端。為使這類電腦的用戶能夠存取你的文件,可選擇Unicode
big-endian格式。
2.4 如何傳參數(shù)會被VB6當(dāng)做字符串?
Q:VB6根據(jù)什么判斷要傳給CopyMemory的參數(shù)是字符串,因而會觸發(fā)自動的UA /AU轉(zhuǎn)換?以下這些傳法,哪種會轉(zhuǎn),哪種不會轉(zhuǎn)?
(1)ByVal String2
(2)ByVal StrPtr(String1)
(3)ByRef String1
(4)ByVal VarPtr(String1)
A:
(1)ByVal String2:字符串參數(shù),自動轉(zhuǎn)換。
(2)ByVal StrPtr(String1):指針,不轉(zhuǎn)換。
(3)ByRef String1:編譯錯誤,去掉 ByRef 可以通過編譯,也會引起UA/AU轉(zhuǎn)換。但其實 Any 類型的參數(shù)不支持這種用法,會導(dǎo)致無法預(yù)期的結(jié)果甚至程序崩潰(見后續(xù)討論)。
(4)ByVal VarPtr(String1):指針的指針,不轉(zhuǎn)換。但是這其實是變量 String1 所在的位置,不當(dāng)操作也會導(dǎo)致無法預(yù)期的結(jié)果甚至程序崩潰(見后續(xù)討論)。