《VB真是想不到系列》
每次看大师的东西到了精彩之处,我就会拍案叫绝:"哇噻,真是想不到!"。在经过很多次这种感慨之后,我发现只要我们动了脑筋,我们自己也能有让别人想不到的东西。于是想到要把这些想不到的东拿出来和大家一起分享,希望抛砖引玉,能引出更多让人想不到的东西。
VB真是想不到系列之二:VB《葵花宝典》--指针技术
关键字:VB、指针、动态内存分配、效率、安全
难度:中级至高级
要求:熟悉VB,掌握基本的C,了解汇编,了解内存分配原理。
想当年东方不败,黑木崖密室一战,仅凭一根绣花针独战四大高手,神出鬼没,堪称天下武林第一高手。若想成为VB里的东方不败,熟习VB《葵花宝典》,掌握VB指针技术,乃是不二的法门。
欲练神功,引刀……,其实掌握VB指针技术,并不需要那么痛苦。因为说穿了,也就那么几招,再勤加练习,终可至神出鬼没之境。废话少说,让我们先从指针的定义说起。
一、指针是什么?
不需要去找什么标准的定义,它就是一个32位整数,在C语言和在VB里都可以用Long类型来表示。在32位Windows平台下它和普通的32位长整型数没有什么不同,只不过它的值是一个内存地址,正是因为这个整数象针一样指向一个内存地址,所以就有了指针的概念。
有统计表明,很大一部分程序缺陷和内存的错误访问有关。正是因为指针直接和内存打交道,所以指针一直以来被看成一个危险的东西。以至于不少语言,如著名的JAVA,都不提供对指针操作的支持,所有的内存访问方面的处理都由编译器来完成。而象C和C++,指针的使用则是基本功,指针给了程序员极大的自由去随心所欲地处理内存访问,很多非常巧妙的东西都要依靠指针技术来完成。
关于一门高级的程序设计语言是不是应该取消指针操作,关于没有指针操作算不算一门语言的优点,我在这里不讨论,因为互联网上关于这方面的没有结果的讨论,已经造成了占用几个GB的资源。无论最终你是不是要下定决心修习指针技术《葵花宝典》,了解这门功夫总是有益处的。
注意:在VB里,官方是不鼓励使用什么指针的,本文所讲的任何东西你都别指望取得官方的技术支持,一切都要靠我们自己的努力,一切都更刺激!
让我们开始神奇的VB指针探险吧!
顺便提一下,听说VB.NET里没有这几个函数,但只要还能调用API,我们就可以试试上面的几个声明,这样在VB.NET里我们一样可以进行指针操作。
但是请注意,如果通过API调用来使用VarPtr,整个程序二SwapPtr将比原来使用内置VarPtr函数时慢6倍。)
如果你喜欢刨根问底,那么下面就是VarPtr函数在C和汇编语言里的样子:
在C里样子是这样的:
long VarPtr(void* pv){
return (long)pv;
}
所对就的汇编代码就两行:
mov eax,dword ptr [esp+4]
ret 4 '弹出栈里参数的值并返回。
之所以让大家了解VarPtr的具体实现,是想告诉大家它的开销并不大,因为它们不过两条指令,即使加上参数赋值、压栈和调用指令,整个获取指针的过程也就六条指令。当然,同样的功能在C语言里,由于语言的直接支持,仅需要一条指令即可。但在VB里,它已经算是最快的函数了,所以我们完全不用担心使用VarPtr会让我们失去效率!速度是使用指针技术的根本要求。
一句话,VarPtr返回的是变量所在处的内存地址,也可以说返回了指向变量内存位置的指针,它是我们在VB里处理指针最重要的武器之一。
3、ByVal和ByRef
ByVal传递的参数值,而ByRef传递的参数的地址。在这里,我们不用去区别传指针/传地址/传引用的不同,在VB里,它们根本就是一个东西的三种不同说法,即使VB的文档里也有地方在混用这些术语(但在C++里的确要区分指针和引用)
初次接触上面的程序二SwapPtr的朋友,一定要搞清在里面的CopyMemory调用中,在什么地方要加ByVal,什么地方不加(不加ByVal就是使用VB缺省的ByRef)
准确的理解传值和传地址(指针)的区别,是在VB里正确使用指针的基础。
现在一个最简单的实验来看这个问题,如下面的程序三:
【程序三】:'体会ByVal和ByRef
Sub TestCopyMemory()
Dim k As Long
k = 5
Note: CopyMemory ByVal VarPtr(k), 40000, 4
Debug.Print k
End Sub
上面标号Note处的语句的目的,是将k赋值为40000,等同于语句k=40000,你可以在"立即"窗口试验一下,会发现k的值的确成了40000。
实际上上面这个语句,翻译成白话,就是从保存常数40000的临时变量处拷贝4个字节到变量k所在的内存中。
现在我们来改变一个Note处的语句,若改成下面的语句:
Note2: CopyMemory ByVal VarPtr(k), ByVal 40000, 4
这句话的意思就成了,从地址40000拷贝4个字节到变量k所在的内存中。由于地址40000所在的内存我们无权访问,操作系统会给我们一个Access Violation内存越权访问错误,告诉我们"试图读取位置0x00009c40处内存时出错,该内存不能为'Read'"。
我们再改成如下的语句看看。
Note3: CopyMemory VarPtr(k), 40000, 4
这句话的意思就成了,从保存常数40000的临时变量处拷贝4个字节到到保存变量k所在内存地址值的临时变量处。这不会出出内存越权访问错误,但k的值并没有变。
我们可以把程序改改以更清楚的休现这种区别,如下面的程序四:
【程序四】:'看看我们的东西被拷贝到哪儿去了
Sub TestCopyMemory()
Dim i As Long, k As Long
k = 5
i = VarPtr(k)
NOTE4: CopyMemory i, 40000, 4
Debug.Print k
Debug.Print i
i = VarPtr(k)
NOTE5: CopyMemory ByVal i, 40000, 4
Debug.Print k
End Sub
程序输出:
5
40000
40000
由于NOTE4处使用缺省的ByVal,传递的是i的地址(也就是指向i的指针),所以常量40000拷贝到了变量i里,因此i的值成了40000,而k的值却没有变化。但是,在NOTE4前有:i=VarPtr(k),本意是要把i本身做为一个指针来使用。这时,我们必须如NOTE5那样用ByVal来传递指针i,由于i是指向变量k的指针,所以最后常量40000被拷贝了变量k里。
希望你已经理解了这种区别,在后面问题的讨论中,我还会再谈到它。
4、AddressOf
它用来得到一个指向VB函数入口地址的指针,不过这个指针只能传递给API使用,以使得API能回调VB函数。
本文不准备详细讨论函数指针,关于它的使用请参考VB文档。
5、拿来主义。
实际上,有了CopyMemory,VarPtr,AddressOf这三把斧头,我们已经可以将C里基本的指针操作拿过来了。
如下面的C程序包括了大部分基本的指针指针操作:
struct POINT{
int x; int y;
};
int Compare(void* elem1, void* elem2){}
void PtrDemo(){
//指针声明:
char c = 'X'; //声明一个char型变量
char* pc; long* pl; //声明普通指针
POINT* pPt; //声明结构指针
void* pv; //声明无类型指针
int (*pfnCastToInt)(void *, void*);//声明函数指针:
//指针赋值:
pc = &c; //将变量c的地址值赋给指针pc
pfnCompare = Compare; //函数指针赋值。
//指针取值:
c = *pc; //将指针pc所指处的内存值赋给变量c
//用指针赋值:
*pc = 'Y' //将'Y'赋给指针pc所指内存变量里。
//指针移动:
pc++; pl--;
}
这些对指针操作在VB里都有等同的东西,
前面讨论ByVal和ByRef时曾说过传指针和传地址是一回事,实际上当我们在VB里用缺省的ByRef声明函数参数时,我们已经就声明了指针。
如一个C声明的函数:long Func(char* pc)
其对应的VB声明是:Function Func(pc As Byte) As Long
这时参数pc使用缺省的ByRef传地址方式来传递,这和C里用指针来传递参数是一样。
那么怎么才能象C里那样明确地声明一个指针呢?
很简单,如前所说,用一个32位长整数来表达指针就行。在VB里就是用Long型来明确地声明指针,我们不用区分是普通指针、无类型指针还是函数指针,通通都可用Long来声明。而给一个指针赋值,就是赋给它用VarPar得到的另一个变量的地址。具体见程序五。
【程序五】:同C一样,各种指针。
Type POINT
X As Integer
Y As Integer
End Type
Public Function Compare(elem1 As Long, elem2 As Long) As Long
'
End Function
Function FnPtrToLong(ByVal lngFnPtr As Long) As Long
FnPtrToLong = lngFnPtr
End Function
Sub PtrDemo()
Dim l As Long, c As Byte, ca() As Byte, Pt As POINT
Dim pl As Long, pc As Long, pv As Long, pPt As Long, pfnCompare As Long
c = AscB("X")
pl = VarPtr(l) '对应C里的long、int型指针
pc = VarPtr(c) '对应char、short型指针
pPt = VarPtr(Pt) '结构指针
pv = VarPtr(ca(0)) '字节数组指针,可对应任何类型,也就是void*
pfnCompare = FnPtrToLong(AddressOf Compare) '函数指针
CopyMemory c, ByVal pc, LenB(c) '用指针取值
CopyMemory ByVal pc, AscB("Y"), LenB(c) '用指针赋值
pc = pc + LenB(c) : pl = pl - LenB(l) '指针移动
End Sub
我们看到,由于VB不直接支持指针操作,在VB里用指针取值和用指针赋值都必须用CopyMemory这个API,而调用API的代价是比较高的,这就决定了我们在VB里使用指针不能象在C里那样自由和频繁,我们必须要考虑指针操作的代价,在后面的"指针应用"我们会再变谈这个问题。
程序五中关于函数指针的问题请参考VB文档,无类型指针void*会在下面"关于Any的问题"里说。
程序五基本上已经包括了我们能在VB里进行的所有指针操作,仅此而已。
下面有一个小测试题,如果现在你就弄懂了上面程咬金的三板斧,你就应该能做得出来。
上面提到过,VB.NET中没有VarPtr,我们可以用声明API的方式来引入MSVBVM60.DLL中的VarPtr。现在的问题如果不用VB的运行时DLL文件,你能不能自己实现一个ObjPtr。答案在下一节后给出。
四、指针使用中应注意的问题
1、关于ANY的问题
如果以一个老师的身份来说话,我会说:最好永远也不要用Any!是的,我没说错,是永远!所以我没有把它放在程咬金的三板斧里。当然,这个问题和是不是应该使用指针这个问题一样会引发一场没有结果的讨论,我告诉你的只是一个观点,因为有时我们会为了效率上的一点点提高或想偷一点点懒而去用Any,但这样做需要要承担风险。
Any不是一个真正的类型,它只是告诉VB编译器放弃对参数类型的检查,这样,理论上,我们可以将任何类型传递给API。
Any在什么地方用呢?让我们来看看,在VB文档里的是怎么说的,现在就请打开MSDN(Visual Studio 6自带的版本),翻到"Visual Basic文档"->"使用Visual Basic"->"部件工具指南"->"访问DLL和Windows API"部分,再看看"将 C 语言声明转换为 Visual Basic 声明"这一节。文档里告诉我们,只有C的声明为LPVOID和NULL时,我们才用Any。实际上如果你愿意承担风险,所有的类型你都可以用Any。当然,也可以如我所说,永远不要用Any。
为什么要这样?那为什么VB官方还要提供Any?是信我的,还是信VB官方的?有什么道理不用Any?
如前面所说,VB官方不鼓励我们使用指针。因为VB所标榜的优点之一,就是没有危险的指针操作,所以的内存访问都是受VB运行时库控制的。在这一点上,JAVA语言也有着同样的标榜。但是,同JAVA一样,VB要避免使用指针而得到更高的安全性,就必须要克服没有指针而带来的问题。VB已经尽最大的努力来使我们远离指针的同时拥有强类型检查带来的安全性。但是操作系统是C写的,里面到处都需要指针,有些指针是没有类型的,就是C程序员常说的可怕的void*无类型指针。它没有类型,因此它可以表示所有类型。如CopyMemory所对应的是C语言的memcpy,它的声明如下:
void *memcpy( void *dest, const void *src, size_t count );
因memcpy前两个参数用的是void*,因此任何类型的参数都可以传递给他。
一个用C的程序员,应该知道在C函数库里这样的void*并不少见,也应该知道它有多危险。无论传递什么类型的变量指针给上面memcpy的void*,C编译器都不会报错或给任何警告。
在VB里大多数时候,我们使用Any就是为了使用void*,和在C里一样,VB也不对Any进行类型检查,我们也可以传递任何类型给Any,VB编译器也都不会报错或给任何警告。
但程序运行时会不会出错,就要看使用它时是不是小心了。正因为在C里很多错误是和void*相关的,所以,C++鼓励我们使用satic_cast<void*>来明确指出这种不安全的类型的转换,已利于发现错误。
说了这么多C/C++,其实我是想告诉所有VB的程序员,在使用Any时,我们必须和C/C++程序员使用void*一样要高度小心。
VB里没有satic_cast这种东西,但我们可以在传递指针时明确的使用long类型,并且用VarPtr来取得参数的指针,这样至少已经明确地指出我们在使用危险的指针。如程序二经过这样的处理就成了下面的程序:
【程序五】:'使用更安全的CopyMemory,明确的使用指针!
Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (ByVal Destination As Long, ByVal Source As Long, ByVal Length As Long)
Sub SwapStrPtr2(sA As String, sB As String)
Dim lTmp As Long
Dim pTmp As Long, psA As Long, psB As Long
pTmp = VarPtr(lTmp): psA = VarPtr(sA): psB = VarPtr(sB)
CopyMemory pTmp, psA, 4
CopyMemory psA, psB, 4
CopyMemory psB, pTmp, 4
End Sub