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