注冊(cè) 登錄
Office中國論壇/Access中國論壇 返回首頁

ganlinlao的個(gè)人空間 http://m.mzhfr.cn/?230471 [收藏] [復(fù)制] [分享] [RSS]

日志

[轉(zhuǎn)]VBA傳遞字符串參數(shù),底層到底是怎么實(shí)現(xiàn)的?

已有 2954 次閱讀2017-1-5 09:58 |個(gè)人分類:vb入門

我們?cè)诼暶鰽PI的時(shí)候,ByRef Val As String 和 ByVal Val As String 之間的區(qū)別到底是什么?它們底層都是怎么工作的?

VB6的String(字符串)是一個(gè)指針,指向一個(gè)BSTR結(jié)構(gòu)的字符串。而BSTR的存儲(chǔ)的結(jié)構(gòu),是用4個(gè)字節(jié)來存儲(chǔ)字符串的長度(所有字符的總字節(jié)數(shù),不包括結(jié)尾的'\0'),然后緊隨其后存儲(chǔ)的是用Unicode編碼的字符串的內(nèi)容(Mac上則是多字節(jié)編碼),然后在最后存儲(chǔ)兩個(gè)用于表示結(jié)尾的'\0'字符。
用StrPtr取字符串地址,實(shí)際取到的是String的字符串內(nèi)容的地址,而這個(gè)地址前面的4個(gè)字節(jié)存儲(chǔ)了字符串的長度。
我們用一小段代碼來演示一下。
Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As Long)

Sub TestParam()
Dim StrVal As String        '字符串
Dim StrLen As Long
Dim WCharArr() As Integer

StrVal = "這是測試字符串"

'取出它存儲(chǔ)的長度
CopyMemory StrLen, ByVal StrPtr(StrVal) - 4, 4
Debug.Print StrLen

'取出內(nèi)容
ReDim WCharArr(StrLen \ 2 - 1)
CopyMemory WCharArr(0), ByVal StrPtr(StrVal), StrLen
'倒著輸出前五個(gè)字符
Debug.Print ChrW$(WCharArr(4)); ChrW$(WCharArr(3)); ChrW$(WCharArr(2)); ChrW$(WCharArr(1)); ChrW$(WCharArr(0))
End Sub
在立即窗口運(yùn)行TestParam,顯示以下的結(jié)果:


接下來步入正題,我們來驗(yàn)證一下ByRef Val As String 和 ByVal Val As String 之間的區(qū)別是什么。
眾所周知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

Sub TestParam()
Dim StrVal As String        '字符串

StrVal = "這是測試字符串"

Debug.Print Hex$(StrPtr(StrVal)), Hex$(VarPtr(StrVal)), Hex$(GetVal(StrVal))
End Sub


一個(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ì)于返回String變量的API,VB6確實(shí)是將其轉(zhuǎn)換為Unicode后存儲(chǔ)在了String類型的變量里。

對(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)換去了。但這樣寫起來挺麻煩的。

注:本文出處 https://www.0xaa55.com/forum.php

評(píng)論 (0 個(gè)評(píng)論)

facelist doodle 涂鴉板

您需要登錄后才可以評(píng)論 登錄 | 注冊(cè)

QQ|站長郵箱|小黑屋|手機(jī)版|Office中國/Access中國 ( 粵ICP備10043721號(hào)-1 )  

GMT+8, 2025-7-13 03:07 , Processed in 0.063553 second(s), 17 queries .

Powered by Discuz! X3.3

© 2001-2017 Comsenz Inc.

返回頂部