VB真是想不到系列之四:VB指標葵花寶典之SafeArray (轉)

worldblog發表於2007-12-12
VB真是想不到系列之四:VB指標葵花寶典之SafeArray (轉)[@more@]

本系列文章可見:
  /develop/list_article.?author=AdamBear">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、還是小巧的,她們都能用COM自動化,還能透過COM自動化閒話家常、交流感情。這是因為COM自動化提供了一種通用的資料結構和資料轉換傳遞的方式。而VB的資料結構基本上就是COM自動化的資料結構,比如VB裡的陣列,在COM裡叫做SafeArray。所以在VB裡處理陣列時我們要清楚的知道我們是在處理SafeArray,COM裡的一種的陣列。
  準備下廚,來做一道陣列指標排序的菜,在看主料SafeArray的真實結構這前,先讓我們來了解一下C裡的陣列。
  在C和C++裡一個陣列指標和陣列第一個元素的指標是一回事,如對下:
  #include
  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 ArrayBOUND
  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 = VtrArray(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陣列字串指標
  這已經是第三篇講指標的東西了,我總在說指標能夠讓我們怎麼樣怎麼樣,不過你是不是覺得除了我說過的幾個用途外,你也沒覺得它有什麼用,其實這是因為我和大家一樣急於求成。在講下去之前,我再來理一理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,但當你在中執行時,你有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
 
  '下面說明兩個指標是一樣的
  De.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
熊超
to:xcbear@netease.com">xcbear@netease.com


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752043/viewspace-991691/,如需轉載,請註明出處,否則將追究法律責任。

相關文章