众所周知ByRef传递的是变量的指针(或者说是“引用”,但底层其实就是往栈上压入了一个指针)而ByVal传递的是实际的值(对于Long、Single、Integer等变量而言是这样)。对于字符串的情况,ByVal并不会真正把整个字符串入栈。那么它实际是如何做的呢?这里我们继续写个代码来验证一下它是怎么做的。但首先我们需要通过一个特殊手段来取得它底层传递的参数,这里我借助了VB6自身运行库 msvbvm60.dll的导出函数VarPtr,我在声明API的时候给它起个别名,然后强行把它的参数改成ByRef Val As String,看它返回啥。
Declare Function GetVal Lib "msvbvm60.dll" Alias "VarPtr" (ByRef Val As String) As Long
一个18F6BC和另一个18F670,两个都是栈里面的变量,18F6BC是StrVal这个变量的地址,18F670是啥?
在说这个之前,我注意到VB6声明API的时候,都是声明“A”结尾的API,也就是ANSI编码字符串的版本的API,而VB6的String是用Unicode编码字符串的。它是不是做了一层字符串的转换呢?
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 '字符串的存储地址
CopyMemory NextPtr, ByVal Ptr, 4
Debug.Print Hex$(NextPtr)
Dim StrBytes() As Byte '字符串的内容
ReDim StrBytes(13) '"这是测试字符串"的长度
CopyMemory StrBytes(0), ByVal NextPtr, 14
Debug.Print StrBytes '这里显示乱码,因为它不是Unicode
Debug.Print StrConv(StrBytes, vbUnicode, 2052) '转换为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
如图所示,为了适应API的调用VB6确实将字符串转换为多字节编码,临时生成了一个String变量(用于存储多字节编码的字符串),然后传递这个临时的字符串变量的指针。 图中的运行情况是,18F698是这个临时的字符串变量的地址,6E5FEA4是这个字符串变量的值,也就是字符串的实际存储地址,然后在这个地址存储了字符串的多字节编码。将其转换为Unicode以后,它就能被VB6的Debug.Print正常显示出来了。
于是,我们可以把String理解为字符串的指针,指向一个Unicode编码的字符串,也就是“LPWSTR”。这个指针指向的内存前面4个字节存储的是字符串的长度(以字节数来算的,而非字符数)。调用API的时候,通过ByRef传递字符串变量,它在底层实际传递的是字符串变量的地址(或者叫“引用”),而这个字符串变量其实是临时生成的转换为多字节编码的字符串,而不是原先的字符串。
接下来看看用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传递的是字符串的指针值,虽然这个字符串同样也是临时生成的多字节编码字符串。这样的话,ByVal Val As String用C艹表示相当于LPWSTR Val,而ByRef Val As String则是LPWSTR &Val(或者LPWSTR *Val,底层反正都是一样的) 那这样的话,如果我们的API是返回字符串指针的,VB6又是怎么处理这样的返回值的呢?
我假设它是这种情况:VB6接收API返回的字符串指针,然后将其转换为Unicode编码。
如果我这么声明API的话:
Declare Function GetVal Lib "msvbvm60.dll" Alias "VarPtr" (ByVal Ptr As Long) As String
那么我给它的Ptr参数传递一个多字节编码的字符串,它应该能给我返回一个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将字符串转换为多字节编码,然后用StrPtr传递了多字节编码的字符串指针(LPSTR),GetVal将其原样返回,但因为我声明上写的是返回String类型,因此VB6认定这个API返回一个多字节编码的字符串,将其转换为Unicode,然后Debug.Print正常打印了"这是测试字符串"。
对于VB6使用API如果要避免这样的编码转换,一个解决的办法是尽量使用W结尾的API,然后字符串参数声明为ByVal StringPtr As Long,传参的时候用StrPtr取(Unicode编码的)字符串的内容的指针。这样VB6就不会在背后帮你把字符串转换来转换去了。但这样写起来挺麻烦的。