本系列文章可见:
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
cDims = 2
fFeatures =
FADF_AUTO AND FADF_FIXEDSIZE
26
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个元素,如下:
lTemp1 = lStrPtrs(0, 3) : lTemp2 = lStrPtrs(1, 3)
lStrPtrs(0, 3) = lStrPtrs(0, 0) : lStrPtrs(1, 3) = lStrPtrs(1, 0)
lStrPtrs(0, 0) = lTemp1 : lStrPtrs(1, 0) = lTemp2
当我们排好序后,我们还要将这个lStrPtrs里的指针元素写回去,如下:
For i = Lo To Hi
CopyMemory(ByVal VarPtr(MyArr(i)), lStrPtrs(0,i), 4)
Next
我已经不想再把这个方法讲下去,虽然它肯定可行,并且也肯定比用CopyMemory来移动数据要快,因为我们实际上移动的仅仅是Long型的指针元素。但我心里已经知道下面有更好更直接的方法,这种转弯抹角的曲线救国实在不值得浪费文字。
四、HACK数组的BSTR结构,实时处理指针。
最精采的来了,实时处理指针动态交换数据,好一个响亮的说法。
我们看到,上一节中所述方法的不足在于我们的Long型指针数组里的指针是独立的,它没有和字串数组里的字串指针联系在一起,要是能联系在一起,我们就能在交换Long型指针的同时,实时地交换字串元素。
这可能吗?
当然,否则我花那么笔墨去写SafeArray干什么!
在上一节,我们的目的是要把字串数组里的BSTR指针数组拿出来放到一个Long型数组里,而在这一节我们的目的是要让我们Long型指针数组就是字串数组里的BSTR指针数组。拿出来再放回去的方法,我们在上一节看到了,现在我们来看看,不拿出来而直接用的方法。
这个方法还是要从字串数组的SafeArray结构来分析,我们已经知道SafeArray结构里的pvData指向的就是一个放实际数据的真数组,而一个字串数组如MyArr它的pvData指向的是一个包含BSTR指针的真数组。现在让我们想想,如果我们将一个Long型数组lStrPtrs的pvData弄得和字串数组MyArr的pvData一样时会怎样?BSTR指针数组就可以通过Long型数组来访问了,先看如何用代码来实现这一点:
'模块级变量
Private MyArr() As String '要排序的字串数组
Private lStrPtrs() As Long '上面数组的字串指针数组,后面会凭空构造它
Private pSA As Long '保存lStrPtrs数组的SafeArray结构指针
Private pvDataOld As Long '保存lStrPtrs数组的SafeArray结构的原
' pvData指针,以便恢复lStrPtrs
'功能: 将Long型数组lStrPtrs的pvData设成字串数组MyArr的pvData
' 以使Long指针数组的变更能实时反应到字串数组里
Private Sub SetupStrPtrs()
Dim pvData As Long
' 初始化lStrPtrs,不需要将数组设得和MyArr一样大
' 我们会在后面构造它
ReDim lStrPtrs(0) As Long
'得到字串数组的pvData
pvData = VarPtr(MyArr(0))
'得到lStrPtrs数组的SafeArray结构指针
CopyMemory pSA, ByVal VarPtrArray(lStrPtrs), 4
'这个指针偏移12个字节后就是pvData指针,将这个指针保存到pvDataOld
' 以便最后还原lStrPtrs,此处也可以用:
' pvDataOld = VarPtr(lStrPtrs(0))
CopyMemory pvDataOld, ByVal pSA + 12, 4
'将MyArr的pvData写到lStrPtrs的pvData里去
CopyMemory ByVal pSA + 12, pvData, 4
'完整构造SafeArray必须要构造它的rgsabound(0).cElements
CopyMemory ByVal pSA + 16, UBound(MyArr) - LBound(MyArr) + 1, 4
'还有rgsabound(0).lLbound
CopyMemory ByVal pSA + 20, LBound(MyArr), 4
End Sub
看不懂,请结合图一再看看,应该可以看出我们是凭空构造了一个lStrPtrs,使它几乎和MyArr一模一样,唯一的不同就是它们的类型不同。MyArr字串数组里的fFeatures包含FADF_BSTR,而lStrPtrs的fFeatures包含FADF_HAVEVARTYPE,并且它的VARTYPE是VT_I4。不用关心这儿,我们只要知道lStrPtrs和MyArr它们指向同一个真数组,管他是BSTR还是VT_I4,我们把真数组里的元素当成指针来使就行了。
注意,由于lStrPtrs是我们经过了我们很大的改造,所以当程序结束前,我们应该将它还原,以便于VB来释放资源。是的,不释放也不一定会引起问题,因为程序运行结束后,操作系统的确是会回收我们在堆栈里分配了却没有释放的lStrPtrs原来的野指针pvOldData,但当你在IDE中运行时,你有60%的机会让VB的IDE死掉。我们是想帮VB做点家务事,而不是想给VB添乱子,所以请记住在做完菜后,一定要把厨房打扫干净,东西该还原的一定要还原。下面看看怎么样来还原:
'还原我们做过手脚的lStrPtr
Private Sub CleanUpStrPtrs()
'lStrPtr的原来声明为:ReDim lStrPtrs(0) As Long
' 按声明的要求还原它
CopyMemory pSA, ByVal VarPtrArray(lStrPtrs), 4
CopyMemory ByVal pSA + 12, pvDataOld, 4
CopyMemory ByVal pSA + 16, 1, 4
CopyMemory ByVal pSA + 20, 0, 4
End Sub
好了,精华已经讲完了,如果你还有点想不通,看看下面的实验:
'实验
Sub Main()
'初始化字串数组
Call InitArray(6)
'改造lStrPtrs
Call SetupStrPtrs
'下面说明两个指针是一样的
Debug.Print lStrPtrs(3)
Debug.Print StrPtr(MyArr(3))
Debug.Print
'先看看原来的字串
Debug.Print MyArr(0)
Debug.Print MyArr(3)
Debug.Print
'现在来交换第0个和第3个字串
Dim lTmp As Long
lTmp = lStrPtrs(3)
lStrPtrs(3) = lStrPtrs(0)
lStrPtrs(0) = lTmp
'再来看看我们的字串,是不是觉得很神奇
Debug.Print MyArr(0)
Debug.Print MyArr(3)
Debug.Print
'还原
Call CleanUpStrPtrs
End Sub
在我的机器上,运行结果如下:
1887420
1887420
OPIIU
WCYKOTC
WCYKOTC
OPIIU
怎么样?如愿已偿!字串通过交换Long型数被实时交换了。
通过这种方式来实现字串数组排序就非常快了,其效率上的提高是惊人的,对冒泡排序这样交换字串次数很多的排序方式,其平均性能能提高一倍以上(要看我们字串平均长度,),对快速排序这样交换次数较少的方法也能有不少性能上的提高,用这种技术实现的快速排序,可以看看本文的配套代码中的QSortPointers。
本道菜最关键的技术已经讲了,至于怎么做完这道菜,怎么把这道菜做得更好,还需要大家自己来实践。
四、我们学到了什么。
仅就SafeArray来说,你可能已经发现我根本就没有直接去用我定义了的SAFEARRAY结构,我也没有展开讲它,实际上对SafeArray我们还可以做很多工作,还有很多巧妙的应用。还有需要注意的,VarPtrArray不能用来返回字串数组和Variant数组的SafeArray结构的指针的指针,为什么会这样和怎样来解决这个问题?这些都需要我们了解BSTR,了解VARIANT,了解VARTYPE,这些也只是COM的冰山一角,要学好VB,乃至整个软件开发技术,COM还有很多很多东西需要学习,我也还在学,在我开始COM的专题之前,大家也应该自学一下。
COM的东西先放一放,下一篇文章,应朋友的要求,我准备来写写内存共享。
后记:
又花了整整一天的时间,希望写的东西有价值,觉得有用就来叫个好吧!
AdamBear
熊超
[email protected]
本文地址:http://com.8s8s.com/it/it2648.htm