使用未公开函数在 Visual Basic 中使用指针
VB-> 使用 VarPtr, StrPtr, ObjPtr 和 CopyMemory API 实现指针
引言
也许你们中的一些人会认为我写错了标题,是的……有时我会弄混,但这次不会。你可以在 Visual Basic 中实现指针。有一篇关于“如何在 Visual Basic 中实现指针”的优秀文章,我推荐你在阅读本文之前或之后阅读它,如果你还没有读过的话。
未公开的函数
现在我们来看看这些未公开的函数是什么。打开 **对象浏览器** 窗口,选择 **VBA** 库,右键单击对象浏览器,然后从菜单中选择 **显示隐藏成员**。现在选择 `_HiddenModule` 类,你将看到三个隐藏函数 – `ObjPtr`, `StrPtr` 和 `VarPtr`。这些函数没有被公开,因为微软不保证它们会在 VB 的未来版本中可用。

VarPtr
- 返回变量的内存地址StrPtr
- 返回字符串内容所在内存的地址ObjPtr
- 返回对象(接口)的内存地址
当然,我们可以用这些函数查看变量的内存地址,但还缺少一些东西。我们如何设置内存地址的*内容*呢?为此,我们需要 API 函数 CopyMemory
,它位于 kernel32.dll 中。
Public Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" _
(Destination As Any, Source As Any, ByVal Length As Long)
此函数将一块内存从一个内存地址复制到另一个内存地址。请注意,根据声明,Destination
和 Source
是通过引用传递的(默认是 ByRef
),这意味着 CopyMemory
函数期望 Destination
和 Source
参数是内存地址。尽管它们被声明为 ByRef
,但我们可以通过在调用中包含 ByVal
来覆盖它(覆盖……你不喜欢这个词吗?)。最后看看那个 Any
数据类型。Any
意味着我们可以为 Destination
和 Source
参数传递“任何”数据类型,但最终传递的是一个 4 字节长的内存地址。请记住这一点。这在处理 Win32 API 时非常重要。
使用 VarPtr
指针 是一个包含另一个变量在内存中地址的变量。在 Windows 中,存储一个内存地址需要 4 个字节。因此,如果我们想在 VB 中声明一个指针,我们必须使用 Long
数据类型。
看看这段代码示例
Dim myInt As Integer
Dim ptr As Long ' Long pointer – need 4 bytes to hold a memory address
ptr = VarPtr(myInt) ' ptr now points to myInt
Debug.Print ptr ' this will print the memory address of myInt
CopyMemory ByVal ptr, 123, 2 ' copy 2 bytes (Integer size - 2 bytes)
Debug.Print myInt ' now contains the value 123
如果你运行这段代码,值 123 将被复制到 myInt
的内存地址,所以最终 myInt
将包含复制的值 123。
现在仔细看看这一行
CopyMemory ByVal ptr, 123, 2 ' copy 2 bytes (Integer size - 2 bytes)
变量 ptr
是 myInt
的指针,换句话说 ptr
存储了 myInt
的内存地址。所以如果我们写这行代码时不加 ByVal
关键字,会发生什么?因为 Destination
被声明为 ByRef
(默认),所以传递的将是 ptr
的内存地址而不是 myInt
的内存地址。
毫不费力地,我们可以将它重写成如下形式
CopyMemory myInt, 123, 2
所以 myInt
是以 ByRef
的方式传递的,换句话说,传递的是 myInt
的内存地址,这正是我们想要做的。记住这个小技巧:当你想传递的内存地址在一个变量中时,你必须使用 ByVal
关键字来覆盖 ByRef
。
如果我们想将变量 intA
的值复制到变量 intB
,我们可以通过以下方式实现:
CopyMemory ByVal VarPtr(intB), ByVal VarPtr(intA), 2
或
CopyMemory intB, intA, 2
希望你现在对 CopyMemory
或 VarPtr
没有疑问了。让我们继续 StrPtr
。
使用 StrPtr
你有没有想过 Len(str)
函数是如何工作的。嗯,你可能会猜它从 string
的开头开始计数,直到找到一个 null
字符。再猜猜,VB 处理 string
s 的方式不是这样的。当我们声明一个 String
类型的变量时,我们声明的是一种名为 BSTR
的数据类型的成员,它代表 Basic String。BSTR
是一个指向 Unicode(每个字符 2 字节)字符数组的指针,该数组前面有一个 4 字节的长度字段,并且以一个 2 字节的空字符结尾。看下面的图以获得更好的理解。

所以当我们写代码时
Dim str as string
str = "hello"
变量 str
实际上是 BSTR
类型的一个成员,它存储了实际 Unicode 字符数组开头的内存地址。注意它并不是指向 4 字节的长度字段。这个长度字段存储了不包含终止空字符的字节数。所以 "hello"
在 Unicode 中表示需要 10 个字节。
Dim str As String
Dim length As Long ' variable to hold the length of string
Dim ptrLengthField As Long ' pointer to 4-byte length field
str = "hello"
Debug.Print StrPtr(str) ' address of the character array
ptrLengthField = StrPtr(str) - 4 ' length field is 4 bytes behind
' // CopyMemory ByVal ptrLengthField, 200&, 4
CopyMemory length, ByVal ptrLengthField, 4
' // this is also correct
' // CopyMemory ByVal VarPtr(length), ByVal ptrLengthField, 4
Debug.Print length ' number of bytes (without null terminator)
Debug.Print Len(str) ' number of characters
在你运行这段代码之前,我们先理清一些事情。检查这一行
ptrLengthField = StrPtr(str) - 4 ' length field is 4 bytes behind
StrPtr(str)
返回的是字符数组的地址,所以我们必须减去 4 个字节才能得到长度字段的地址。
看看这一行
CopyMemory length, ByVal ptrLengthField, 4
变量 ptrLengthField
存储了 str
的长度字段的内存地址,所以我们必须使用 ByVal
关键字将其按值传递。
现在运行代码,输出是
Debug.Print length ' number of bytes (without null terminator)
Debug.Print Len(str) ' number of characters
将是
10
5
变量 length
被设置为 10
,因为 Unicode,"hello"
占用 10 个字节,而 Len(str)
返回 5
,这是 "hello"
中的字符数。现在,让我们找出 Len()
函数是如何真正工作的!
取消注释这一行
CopyMemory ByVal ptrLengthField, 200&, 4
这里发生的是值 200
被复制到 str
的长度字段,取代了原始值 10
。200&
中的 &
符号是为了表明将 200
视为 Long
(Long
的类型声明字符是 &
)。
运行代码,输出是
Debug.Print length ' number of bytes (without null terminator)
Debug.Print Len(str) ' number of characters
将是
200
100
当我们 str
包含 "hello"
时,Len(str)
返回 100
,呃?所以 VB 返回了我们长度字段除以 2 的值。我想你现在知道 Len()
对于 string
s 是如何工作的了。
使用 ObjPtr
最后我们将看到 ObjPtr
的实际应用。
Dim obj As New Form1
Debug.Print ObjPtr(obj) ' gives the address to the object (new instance of Form1)
Debug.Print VarPtr(obj) ' gives the address to the variable - obj
这里没有什么需要解释的。这里有另一个简单的代码示例。
Dim objA As New Form1
Dim objB As New Form1
Debug.Print "before"
Debug.Print ObjPtr(objA)
Debug.Print ObjPtr(objB)
Set objA = objB
Debug.Print "after"
Debug.Print ObjPtr(objA)
Debug.Print ObjPtr(objB)
输出将是:
before
1849600
1761448
after
1761448
1761448
正如你所见,在这一行之后
Set objA = objB
objA
现在指向与 objB
相同的内存位置,正如它应该的那样。你也可以用以下方式替换上面那行
CopyMemory ByVal VarPtr(objA), ByVal VarPtr(objB), 4
或者更简单的版本
CopyMemory objA, objB, 4
它能工作,但不要使用这种方式来设置对象,因为有时当对象被销毁时会抛出非法操作。
就是这样。这些函数用于快速 string
处理例程和子类化。我在这里不打算讨论它们,因为……嗯……我对此一无所知。记住……我只能为你指明方向,而你需要自己走过去!
编码愉快!!!
历史
- 2003 年 12 月 18 日:首次发布