注册 登录
Office中国论坛/Access中国论坛 返回首页

ganlinlao的个人空间 http://www.office-cn.net/?230471 [收藏] [复制] [分享] [RSS]

日志

[转]VBA传递字符串参数,底层到底是怎么实现的?

已有 2697 次阅读2017-1-5 09:58 |个人分类:vb入门

我们在声明API的时候,ByRef Val As String 和 ByVal Val As String 之间的区别到底是什么?它们底层都是怎么工作的?

VB6的String(字符串)是一个指针,指向一个BSTR结构的字符串。而BSTR的存储的结构,是用4个字节来存储字符串的长度(所有字符的总字节数,不包括结尾的'\0'),然后紧随其后存储的是用Unicode编码的字符串的内容(Mac上则是多字节编码),然后在最后存储两个用于表示结尾的'\0'字符。
用StrPtr取字符串地址,实际取到的是String的字符串内容的地址,而这个地址前面的4个字节存储了字符串的长度。
我们用一小段代码来演示一下。
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 = "这是测试字符串"

'取出它存储的长度
CopyMemory StrLen, ByVal StrPtr(StrVal) - 4, 4
Debug.Print StrLen

'取出内容
ReDim WCharArr(StrLen \ 2 - 1)
CopyMemory WCharArr(0), ByVal StrPtr(StrVal), StrLen
'倒着输出前五个字符
Debug.Print ChrW$(WCharArr(4)); ChrW$(WCharArr(3)); ChrW$(WCharArr(2)); ChrW$(WCharArr(1)); ChrW$(WCharArr(0))
End Sub
在立即窗口运行TestParam,显示以下的结果:


接下来步入正题,我们来验证一下ByRef Val As String 和 ByVal Val As String 之间的区别是什么。
众所周知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

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

StrVal = "这是测试字符串"

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


一个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正常打印了"这是测试字符串"。
所以对于返回String变量的API,VB6确实是将其转换为Unicode后存储在了String类型的变量里。

对于VB6使用API如果要避免这样的编码转换,一个解决的办法是尽量使用W结尾的API,然后字符串参数声明为ByVal StringPtr As Long,传参的时候用StrPtr取(Unicode编码的)字符串的内容的指针。这样VB6就不会在背后帮你把字符串转换来转换去了。但这样写起来挺麻烦的。

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

评论 (0 个评论)

facelist doodle 涂鸦板

您需要登录后才可以评论 登录 | 注册

QQ|站长邮箱|小黑屋|手机版|Office中国/Access中国 ( 粤ICP备10043721号-1 )  

GMT+8, 2024-11-25 07:25 , Processed in 0.074366 second(s), 17 queries .

Powered by Discuz! X3.3

© 2001-2017 Comsenz Inc.

返回顶部