眾所周知ByRef傳遞的是變量的指針(或者說是“引用”,但底層其實(shí)就是往棧上壓入了一個(gè)指針)而ByVal傳遞的是實(shí)際的值(對(duì)于Long、Single、Integer等變量而言是這樣)。對(duì)于字符串的情況,ByVal并不會(huì)真正把整個(gè)字符串入棧。那么它實(shí)際是如何做的呢?這里我們繼續(xù)寫個(gè)代碼來驗(yàn)證一下它是怎么做的。但首先我們需要通過一個(gè)特殊手段來取得它底層傳遞的參數(shù),這里我借助了VB6自身運(yùn)行庫 msvbvm60.dll的導(dǎo)出函數(shù)VarPtr,我在聲明API的時(shí)候給它起個(gè)別名,然后強(qiáng)行把它的參數(shù)改成ByRef Val As String,看它返回啥。
Declare Function GetVal Lib "msvbvm60.dll" Alias "VarPtr" (ByRef Val As String) As Long

一個(gè)18F6BC和另一個(gè)18F670,兩個(gè)都是棧里面的變量,18F6BC是StrVal這個(gè)變量的地址,18F670是啥?
在說這個(gè)之前,我注意到VB6聲明API的時(shí)候,都是聲明“A”結(jié)尾的API,也就是ANSI編碼字符串的版本的API,而VB6的String是用Unicode編碼字符串的。它是不是做了一層字符串的轉(zhuǎn)換呢?
Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As Long)
Declare Function GetVal Lib "msvbvm60.dll" Alias "VarPtr" (ByRef Val As String) As Long
Sub Test2(ByVal Ptr As Long)
Debug.Print Hex$(Ptr) '字符串變量的值
Dim NextPtr As Long '字符串的存儲(chǔ)地址
CopyMemory NextPtr, ByVal Ptr, 4
Debug.Print Hex$(NextPtr)
Dim StrBytes() As Byte '字符串的內(nèi)容
ReDim StrBytes(13) '"這是測試字符串"的長度
CopyMemory StrBytes(0), ByVal NextPtr, 14
Debug.Print StrBytes '這里顯示亂碼,因?yàn)樗皇荱nicode
Debug.Print StrConv(StrBytes, vbUnicode, 2052) '轉(zhuǎn)換為Unicode,中文代碼頁
End Sub
Sub TestParam()
Dim StrVal As String '字符串
StrVal = "這是測試字符串"
Debug.Print Hex$(StrPtr(StrVal)), Hex$(VarPtr(StrVal)), Hex$(GetVal(StrVal))
Test2 GetVal(StrVal)
End Sub
如圖所示,為了適應(yīng)API的調(diào)用VB6確實(shí)將字符串轉(zhuǎn)換為多字節(jié)編碼,臨時(shí)生成了一個(gè)String變量(用于存儲(chǔ)多字節(jié)編碼的字符串),然后傳遞這個(gè)臨時(shí)的字符串變量的指針。 圖中的運(yùn)行情況是,18F698是這個(gè)臨時(shí)的字符串變量的地址,6E5FEA4是這個(gè)字符串變量的值,也就是字符串的實(shí)際存儲(chǔ)地址,然后在這個(gè)地址存儲(chǔ)了字符串的多字節(jié)編碼。將其轉(zhuǎn)換為Unicode以后,它就能被VB6的Debug.Print正常顯示出來了。
于是,我們可以把String理解為字符串的指針,指向一個(gè)Unicode編碼的字符串,也就是“LPWSTR”。這個(gè)指針指向的內(nèi)存前面4個(gè)字節(jié)存儲(chǔ)的是字符串的長度(以字節(jié)數(shù)來算的,而非字符數(shù))。調(diào)用API的時(shí)候,通過ByRef傳遞字符串變量,它在底層實(shí)際傳遞的是字符串變量的地址(或者叫“引用”),而這個(gè)字符串變量其實(shí)是臨時(shí)生成的轉(zhuǎn)換為多字節(jié)編碼的字符串,而不是原先的字符串。
接下來看看用ByVal傳遞字符串變量是什么效果。
Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As Long)
Declare Function GetVal Lib "msvbvm60.dll" Alias "VarPtr" (ByVal Val As String) As Long
Sub Test2(ByVal Ptr As Long)
Debug.Print Hex$(Ptr)
Dim MB() As Byte
ReDim MB(13)
CopyMemory MB(0), ByVal Ptr, 14
Debug.Print StrConv(MB, vbUnicode, 2052)
End Sub
Sub TestParam()
Dim StrVal As String '字符串
StrVal = "這是測試字符串"
Debug.Print Hex$(StrPtr(StrVal)), Hex$(VarPtr(StrVal)), Hex$(GetVal(StrVal))
Test2 GetVal(StrVal)
End Sub

果然ByVal傳遞的是字符串的指針值,雖然這個(gè)字符串同樣也是臨時(shí)生成的多字節(jié)編碼字符串。這樣的話,ByVal Val As String用C艸表示相當(dāng)于LPWSTR Val,而ByRef Val As String則是LPWSTR &Val(或者LPWSTR *Val,底層反正都是一樣的) 那這樣的話,如果我們的API是返回字符串指針的,VB6又是怎么處理這樣的返回值的呢?
我假設(shè)它是這種情況:VB6接收API返回的字符串指針,然后將其轉(zhuǎn)換為Unicode編碼。
如果我這么聲明API的話:
Declare Function GetVal Lib "msvbvm60.dll" Alias "VarPtr" (ByVal Ptr As Long) As String
那么我給它的Ptr參數(shù)傳遞一個(gè)多字節(jié)編碼的字符串,它應(yīng)該能給我返回一個(gè)Unicode編碼的字符串。
Declare Function GetVal Lib "msvbvm60.dll" Alias "VarPtr" (ByVal Ptr As Long) As String
Sub TestParam()
Dim StrVal As String '字符串
StrVal = "這是測試字符串"
Debug.Print GetVal(StrPtr(StrConv(StrVal, vbFromUnicode, 2052)))
End Sub

我用StrConv將字符串轉(zhuǎn)換為多字節(jié)編碼,然后用StrPtr傳遞了多字節(jié)編碼的字符串指針(LPSTR),GetVal將其原樣返回,但因?yàn)槲衣暶魃蠈懙氖欠祷豐tring類型,因此VB6認(rèn)定這個(gè)API返回一個(gè)多字節(jié)編碼的字符串,將其轉(zhuǎn)換為Unicode,然后Debug.Print正常打印了"這是測試字符串"。
對(duì)于VB6使用API如果要避免這樣的編碼轉(zhuǎn)換,一個(gè)解決的辦法是盡量使用W結(jié)尾的API,然后字符串參數(shù)聲明為ByVal StringPtr As Long,傳參的時(shí)候用StrPtr。║nicode編碼的)字符串的內(nèi)容的指針。這樣VB6就不會(huì)在背后幫你把字符串轉(zhuǎn)換來轉(zhuǎn)換去了。但這樣寫起來挺麻煩的。