本系列文章可见: http://www.csdn.net/develop/list_article.asp?author=AdamBear
VB真是想不到系列之四:VB指针葵花宝典之SafeArray 关键字:VB、HCAK、指针、SafeArray、数组指针、效率、数组、排序 难度:中级或高级 要求:熟悉VB,了解基本的排序算法,会用VC更好。
引言: 上回说到,虽然指针的运用让我们的数组排序在性能上有了大大的提高,但是CopyMemory始终是我们心里一个挥之不去的阴影,因为它还是太慢。在C里我们用指针,从来都是来去自如,随心所欲,四两拨千斤;而在VB里,我们用指针却要瞻前顾后,哪怕一个字节都要用到CopyMemory乾坤大挪移,真累。今天我们就来看看,能不能让VB里的指针也能指哪儿打哪儿,学学VB指针的凌波微步。 各位看官,您把茶端好了。 一、帮VB做点COM家务事 本系列开张第一篇里,我就曾说过VB的成功有一半的功劳要记到COM开发小组身上,COM可是M$公司打的一手好牌,从OLE到COM+,COM是近十年来M$最成功技术之一,所以有必要再吹它几句。 COM组件对象模型就是VB的基础,Varinat、String、Current、Date这些数据类型都是COM的,我们用的CStr、CInt、CSng等Cxxx函数根本就是COM开发小组写的,甚至我们在VB里用的数学函数,COM里都有对应的VarxxxDiv、VarxxxAdd,VarxxxAbs。嘿嘿,VB开发小组非常聪明。我们也可以说COM的成功也有VB开发小组和天下无数VB程序员的功劳,Bill大叔英明地将COM和VB捆绑在一起了。 所以说,学VB而不需要了解COM,你是幸福的,你享受着VB带给你的轻松写意,她把那些琐碎的家务事都干了,但同时你又是不幸的,因为你从来都不曾了解你爱的VB,若有一天VB对你发了脾气,你甚至不知该如何去安慰她。所以,本系列文章将拿出几大篇来教大家如何帮VB做点COM方面的家务事,以备不时之需。 想一口气学会所有COM家务事,不容易,今天我们先拿数组来开个头,更多的技术我以后再一一道来。 二、COM自动化里的SafeArray 就象洗衣机、电饭堡、吹尘器,VB洗衣服、做饭、打扫卫生都会用到COM自动化。它包含了一切COM里通用的东西,所有的女人都能用COM自动化来干家务,无论是犀利的VC、温柔的VB、还是小巧的VBScript,她们都能用COM自动化,还能通过COM自动化闲话家常、交流感情。这是因为COM自动化提供了一种通用的数据结构和数据转换传递的方式。而VB的数据结构基本上就是COM自动化的数据结构,比如VB里的数组,在COM里叫做SafeArray。所以在VB里处理数组时我们要清楚的知道我们是在处理SafeArray,COM里的一种安全的数组。 准备下厨,来做一道数组指针排序的菜,在看主料SafeArray的真实结构这前,先让我们来了解一下C里的数组。 在C和C++里一个数组指针和数组第一个元素的指针是一回事,如对下: #include <iostream> using namespace std; int main() { int a[10]; cout << "a = " << a << endl; cout << "&a[0] =" << &a[0] << endl; } ///:~ 可以看到结果a和&a[0]是相同的,这里的数组是才数据结构里真实意义上的数组,它们在内存里一个接着一个存放,我们通过第一个元素就能访问随后的元素,我们可以称这样的数组为"真数组"。但是它不安全,因为我们无法从这种真数组的指针上得知数组的维数、元素个数等非常重要的信息,所以也无法控制对这种数组的访问。我们可以在C里将一个二维数组当做一维数组来处理,我们还可以通过一个超过数组大小的索引去访问数组外的内存,但这些都是极不安全的,数组边界错误可以说是C里一个非常容易犯却不易发现的错误。 因此就有了COM里的SafeArray安全数组来解决这个问题,在VB里我们传递一个数组时,传递的实际上COM里的SafeAraay结构指构的指针,SafeAraay结构样子如下: Private Type SAFEARRAY cDims As Integer ''''这个数组有几维? fFeatures As Integer ''''这个数组有什么特性? cbElements As Long ''''数组的每个元素有多大? cLocks As Long ''''这个数组被锁定过几次? pvData As Long ''''这个数组里的数据放在什么地方? ''''rgsabound() As SFArrayBOUND End Type 紧接在pvData这后的rgsabound是个真数组,所以不能在上面的结构里用VB数组来声明,记住,在VB里的数组都是SafeArray,在VB里没有声明真数组的方法。 不过这不是问题,因为上面SFArrayBOUND结构的真数组在整个SAFEARRAY结构的位置是不变的,总是在最后,我们可以用指针来访问它。SFArrayBOUND数组的元素个数有cDims个,每一个元素记录着一个数组维数的信息,下面看看它的样子: Private Type SAFEARRAYBOUND cElements As Long ''''这一维有多少个元素? lLbound As Long ''''它的索引从几开始? End Type 还有一个东西没说清,那就是上面SAFEARRAY结构里的fFeatures,它是一组标志位来表示数组有那些待性,这些特性的标志并不需要仔细的了解,本文用不上这些,后面的文章用到它们时我会再来解释。 看了上面的东西,各位一定很头大,好在本文的还用不了这么多东西,看完本文你就知道其实SafeArray也不难理解。先来看看如下的声明: Dim MyArr(1 To 8, 2 To 10) As Long 这个数组做为SafeArray在内存里是什么样子呢?如图一:
cDims = 2
fFeatures = FADF_AUTO AND FADF_FIXEDSIZE
位置 0
cbElements = 4 LenB(Long)
4
cLocks = 0
8
pvData(指向真数组)
12
rgsabound(0).cElements = 8
16
rgsabound(0).lLbound = 1
18
rgsabound(1).cElements = 9
22
rgsabound(1).lLbound = 2
26
cDims = 2
fFeatures = FADF_AUTO AND FADF_FIXEDSIZE
位置 0
cbElements = 4 LenB(Long)
4
cLocks = 0
8
pvData(指向真数组)
12
rgsabound(0).cElements = 8
16
rgsabound(0).lLbound = 1
18
rgsabound(1).cElements = 9
22
rgsabound(1).lLbound = 2
26
图一 :SafeArray内存结构
cDims表示它是个2维数组,sFeatures表示它是一个在堆栈里分配的固定大小的数组,cbElements表示它的每个元素大小是Long四个字节,pvData指向真的数组(就是上面说的C里的数组),rgsabound这个真数组表明数组二个维的大小和每个维的索引开始位置值。 先来看看从这个上面我们能做些什么,比如要得到一个数组的维数,在VB里没有直接提供这样的方法,有一个变通的方法是通过错误捕获如下: On Error Goto BoundsError For I = 1 To 1000 ''''不会有这么多维数的数组 lTemp = LBound(MyArr, I) Next BoundErro: nDims = I - 1 MsgBox "这个数组有" & nDims & "维"
现在我们知道了SafeArray的原理,所以也可以直接得到维数,如下: ''''先得到一个指向SafeArray结构的指针的指针,原理是什么,我后面说。 ppMyArr = VarPtrArray(MyArr) ''''从这个指针的指针得到SafeArray结构的指针 CopyMemory pMyArr, ByVal ppMyArr, 4 ''''再从这个指针所指地址的头两个字节取出cDims CopyMemory nDims, ByVal pMyArr, 2 MsgBox "这个数组有" & nDims & "维"
怎么样,是不是也明白了LBound实际上是SafeArray里的rgsabound的lLbound,而UBound实际上等于lLbound +cElements - 1,现在我提个问,下面iUBound应该等于几? Dim aEmptyArray() As Long iUBound = UBound(aEmptyArray) 正确的答案是-1,不奇怪,lLbound -cElements - 1 = 0 - 0 - 1 = -1 所以检查UBound是不是等于-1是一个判断数组是不是空数组的好办法。
还有SafeArray结构里的pvData指向存放实际数据的真数组,它实际就是一个指向真数组第一个元素的指针,也就是说有如下的等式: pvDate = VarPtr(MyArr(0)) 在上一篇文章里,我们传给排序函数的是数组第一个元素的地址VarPtr(xxxx(0)),也就是说我们传的是真数组,我们可以直接在真数组上进行数据的移动、传递。但是要如何得到一个数组SafeArray结构的指针呢?你应该注意到我上面所用的VarPtrArray,它的声明如下: Declare Function VarPtrArray Lib "msvbvm60.dll" _ Alias "VarPtr" (Var() As Any) As Long 它就是VarPtr,只不过参数声明上用的是VB数组,这时它返回来的就是一个指向数组SafeArray结构的指针的指针。因为VarPtr会将传给它的参数的地址返回,而用ByRef传给它一个VB数组,如前面所说,实际上传递的是一个SafeArray结构的指针,这时VarPtrArray将返回这个指针的指针。所以要访问到SafeArray结构需要,如下三步: 用VarPtrArray返回ppSA,再通过ppSA得到它指向的pSA,pSA才是指向SafeArray结构的指针,我们访问SafeArray结构需要用到就是这个pSA指针。 现在你应该已经了解了SafeArray大概的样子,就这么一点知识,我们就能在VB里对数组进行HACK了。
三、HACK数组字串指针 这已经是第三篇讲指针的东西了,我总在说指针能够让我们怎么样怎么样,不过你是不是觉得除了我说过的几个用途外,你也没觉得它有什么用,其实这是因为我和大家一样急于求成。在讲下去之前,我再来理一理VB里指针应该在什么情况下用。 只对指针类型用指针!废话?我的意思是说,象Integer, Long, Double这样的数值类型它们的数据直接存在变量里,VB处理它们并不慢,没有HACK的必要。但是字串,以及包括字串、数组、对象、结构的Variant,还有包括字串、对象结构的数组它们都是指针,实际数据不放在变量里,变量放的是指针,由于VB不直接支持指针,对他们的操作必须连同数据拷贝一起进行。有时我们并不想赋值,我们只想交换它们指针,或者想让多个指针指向同一个数据,让多个变量对同一处内存操作,要达到这样的目的,在VB里不HACK是不行的。 对数组尤其如此,比如我们今天要做的菜:对一个字串数组进行排序。我们知道,对字串数组进行排序很大一部分时间都用来交换字串元素,在VB里对字串赋值时要先将原字串释放掉,再新建一个字串,再将源字串拷贝过来,非常耗时。用COM里的概念来说,比如字串a、b的操作a=b,要先用SysFreeString(a)释放掉原来的字串a, 再用a = SysAllocString(b)新建和拷贝字串,明白了这一点就知道,在交换字串时不要用赋值的方式去交换,而应该直接去交换字串指针,我在指针葵花宝典第一篇里介绍过这种交换字串的方法,这可以大大提高交换字串的速度。但是这种交换至少也要用两次CopyMemory来将指针写回去,对多个字串进行交换时调用CopyMemory的次数程几何增长,效率有很大的损失。而实际上,指针只是32位整数而已,在C里交换两个指针,只需要进行三次Long型整数赋值就行了。所以我们要想想我们能不能将字串数组里所有字串指针拿出来放到一个Long型指针数组里,我们只交换这个Long型数组里的元素,也就相当于交换了字串指针,排好序后,再将这个Long型指针数组重新写回到字串数组的所有字串指针里,而避免了多次使用CopyMemory来一次次两两交换字串指针。这样我们所有的交换操作都是对一个Long型数组来进行,要知道交换两个Long型整数,在VB里和在C里是一样快的。 现在我们的问题成了如何一次性地将字串数组里的字串指针拿出来,又如何将调整后的字串指针数组写回去。 不用动数组的SafeArray结构,我们用StrPtr也能完成它。我们知道,字串数组元素里放的是实际上是字串指针,也就是BSTR指针,把这些指针放到一个Long型数组里很简单,用下面的方法: Private Sub GetStrPtrs() Dim Hi As Long, Lo As Long Hi = UBound(MyArr) Lo = LBound(MyArr) ReDim lStrPtrs(0 To 1, Lo To Hi) As Long Dim i As Long For i = Lo To Hi lStrPtrs(0, i) = StrPtr(MyArr(i)) ''''BSTR指针数组 lStrPtrs(1, i) = i ''''原数组索引 Next End Sub 为什么要用2维数组,这是排序的需要,因为当我们交换lStrPtrs里的Long型指针时,原来的字串数组MyArr里的字串指针并没有同时交换,所以用lStrPtrs里的Long型指针访问字串时,必须通过原来的索引,因此必须用2维数组同时记录下每个Long型指针所指字串在原字串数组里的索引。如果只用1维数组,访问字串时就又要用到CopyMemory了,比如访问lStrPtrs第三个元素所指的字串,得用如下方法: CopyMemory ByVal VarPtr(StrTemp), lStrPtrs(3), 4 虽然只要我们保证StrTemp足够大,再加上一些清理善后的工作,这种做法是可以的,但实际上我们也看到这样还是得多次调用CopyMemory,实际上考虑到原来的字串数组MyArr一直就没变,我们能够通过索引来访问字串,上面同样的功能现在就成了: StrTemp = MyArr(lStrPtrs(1,3)) ''''通过原字串数组索引读出字串。 不过,当我们交换lStrPtrs里的两个Long型指针元素时,还要记得同时交换它们的索引,比如交换第0个和第3个元素,如下: lTe [1] [2] 下一页 没有相关教程
|