VB真是想不到系列之三:VB指標葵花寶典之函式指標 (轉)

gugu99發表於2008-05-28
VB真是想不到系列之三:VB指標葵花寶典之函式指標 (轉)[@more@]

《VB真是想不到系列》
  每次看大師的東西到了精彩之處,我就會拍案叫絕:"哇噻,真是想不到!"。在經過很多次這種感慨之後,我發現只要我們動了腦筋,我們自己也能有讓別人想不到的東西。於是想到要把這些想不到的東拿出來和大家一起分享,希望拋磚引玉,能引出更多讓人想不到的東西。
本系列文章可見:
  /develop/list_article.?author=AdamBear">http://www.csdn.net/develop/list_article.asp?author=AdamBear

  VB真是想不到系列之三:VB指標葵花寶典之指標
關鍵字:VB、HCAK、指標、函式指標、、陣列、、排序
難度:中級
要求:熟悉VB,瞭解基本的排序演算法,會用VC更好。

引言: 
  不知大家在修習過本系列第二篇《VB指標葵花寶典》後有什麼感想,是不是覺得寶典過於偏重內功心法,而少了厲害的招式。所以,今天本文將少講道理,多講招式。不過,還是請大家從名門正派的內功心法開始學起,否則會把九陰真經練成九陰白骨爪。
  今天,我們重點來談談函式指標的實際應用。
  接著上一篇文章,關於字串的問題,聽CSDN上各位網友的建議,我不準備寫什麼《VB字串全攻略》了,關於BSTR的結構,關於時字串在UNICODE和ANSI之間的轉換問題,請參考MSDN的Partial Books裡的《 API Programming with 》裡的第六章《Strings》。今天就讓我們先忘掉字串,專注於函式指標的處理上來。

  一、函式指標
  AddressOf得到一個VB內部的函式指標,我們可以將這個函式指標傳遞給需要回撥這個函式的API,它的作用就是讓外部的可以呼叫VB內部的函式。
  但是VB裡函式指標的應用,遠不象C裡應用那麼廣泛,因為VB文件裡僅介紹瞭如何將函式指標傳遞給API以實現回撥,並沒指出函式指標諸多神奇的功能,因為VB是不鼓勵使用指標的,函式指標也不例外。
  首先讓我們對函式指標的使用方式來分個類。
  1、回撥。這是最基本也是最重要的功能。比如VB文件裡介紹過的子類派生技術,它的核心就是兩個API:SetWindowLong和CallWindowProc。
  我們可以使SetWindowLong這個API來將原來的視窗函式指標換成自己的函式指標,並將原來的視窗函式指標儲存下來。這樣視窗訊息就可以發到我們自己的函式里來,並且我們隨時可以用CallWindowProc來呼叫前面儲存下來的視窗指標,以呼叫原來的視窗函式。這樣,我們可以在不破壞原有視窗功能的前提下處理鉤入的訊息。
  具體的處理,我們應該很熟悉了,VB文件也講得很清楚了。這裡需要注意的就是CallWindowProc這個API,在後面我們將看到它的妙用。
  在這裡我們稱回撥為讓"外部呼叫內部的函式指標"。
  2、程式內部使用。比如在C裡我們可以將C函式指標作為引數傳遞給一個需要函式指標的C函式,如後面還要講到的C庫函式qsort,它的宣告如下:
  #define int (__cdecl *COMPARE)(const void *elem1, const void *elem2)
  void qsort(void *base, size_t num, size_t width,
  COMPARE pfnCompare);
它需要一個COMPARE型別函式指標,用來比較兩個變數大小的,這樣排序函式可以呼叫這個函式指標來比較不同型別的變數,所以qsort可以對不同型別的變數陣列進行排序。
  我們姑且稱這種應用為"從內部呼叫內部的函式指標"。
  3、呼叫外部的函式
  也許你會問,用API不就是呼叫外部的函式嗎?是的,但有時候我們還是需要直接獲取外部函式的指標。比如透過LoadLibrary動態載入DLL,然後再透過GetProcAddress得到我們需要的函式入口指標,然後再透過這個函式指標來呼叫外部的函式,這種動態載入DLL的技術可以讓我們更靈活的呼叫外部函式。
  我們稱這種方式為"從內部呼叫外部的函式指標"
  4、不用說,就是我們也可控制"從外部呼叫外部的函式指標"。不是沒有,比如我們可以載入多個DLL,將其中一個DLL中的函式指標傳到另一個DLL裡的函式內。
  上面所分的"內"和"外"都是相對而言(DLL實際上還是在程式內),這樣分類有助於以後我們談問題,請記住我上面的分類,因為以後的文章也會用到這個分類來分析問題。

  函式指標的使用不外乎上面四種方式。但在實際使用中卻是靈活多變的。比如在C++裡繼承和多型,在COM裡的介面,都是一種叫vTable的函式指標表的巧妙應用。使用函式指標,可以使程式的處理方式更加高效、靈活。
  VB文件裡除了介紹過第一方式外,對其它方式都沒有介紹,並且還明確指出不支援“Basic 到 Basic”的函式指標(也就是上面說的第二種方式),實際上,透過一定的,上面四種方式均可以實現。今天,我們就來看看如何來實現第二種方式,因為實現它相對來說比較簡單,我們先從簡單的入手。至於如何在VB內呼叫外部的函式指標,如何在VB裡透過處理vTable介面函式指標跳轉表來實現各種函式指標的巧妙應用,由於這將涉及COM內部原理,我將另文詳述。
  其實VB的文件並沒有說錯,VB的確不支援“Basic 到 Basic”的函式指標,但是我們可以繞個彎子來實現,那就是先從"Basic到API",然後再用第一種方式"外部呼叫內部的函式指標"來從"API到BASIC",這樣就達到了第二種方式從"Basic 到 Basic"的目的,這種技術我們可以稱之為"強制回撥",只有VB裡才會有這種古怪的技術。
  說得有點繞口,但是仔細想想視窗子類派生技術裡CallWindowProc,我們可以用CallWindowProc來強制外部的操作呼叫我們原來的儲存的視窗函式指標,同樣我們也完全可以用它來強制呼叫我們內部的函式指標。
  呵呵,前面說過要少講原理多講招式,現在我們就來開始學習招式吧!
  考慮我們在VB裡來實現和C裡一樣支援多關鍵字比較的qsort。完整的見本文配套程式碼,此處僅給出函式指標應用相關的程式碼。 
  '當然少不了的CopyMemory,不用ANY的版本。
  Declare Sub CopyMemory Lib "kernel32" Alias _
"RtlMoveMemory" (ByVal dest As Long, ByVal As Long, _
  ByVal numBytes As Long)

  '嘿嘿,看下面是如何將CallWindowProc的宣告做成Compare宣告的。
  Declare Function Compare Lib "user32" Alias _
"CallWindowProcA" (ByVal pfnCompare As Long, ByVal pElem1 As Long, _
  ByVal pElem2 As Long, ByVal unused1 As Long,  _
  ByVal unused2 As Long) As Integer
'注:ByVal xxxxx As Long ,還記得吧!這是標準的指標宣告方法。
 
  '宣告需要比較的陣列元素的結構
  Public Type TEmployee
  Name As String
  Salary As Currency
  End Type 

  '再來看看我們的比較函式
  '先按薪水比較,再按姓名比較
  Function CompareSalaryName(Elem1 As TEmployee, _
  Elem2 As TEmployee, _ 
  unused1 As Long,  _
  unused2 As Long) As Integer
  Dim Ret As Integer
  Ret = Sgn(Elem1.Salary - Elem2.Salary)
  If Ret = 0 Then
  Ret = StrComp(Elem1.Name, Elem2.Name, vbTextCompare)
  End If
  CompareSalaryName = Ret
  End Function
  '先按姓名比較,再按薪水比較
  Function CompareNameSalary(Elem1 As TEmployee, _
  Elem2 As TEmployee, _
  unused1 As Long,  _
  unused2 As Long) As Integer
  Dim Ret As Integer
  Ret = StrComp(Elem1.Name, Elem2.Name, vbTextCompare)
  If Ret = 0 Then
  Ret = Sgn(Elem1.Salary - Elem2.Salary)
  End If
  CompareNameSalary = Ret
  End Function

  最後再看看我們來看看我們最終的qsort的宣告。
  Sub qsort(ByVal ArrayPtr As Long, ByVal nCount As Long, _
  ByVal nElemSize As Integer, ByVal pfnCompare As Long)
  上面的ArrayPtr是需要排序陣列的第一個元素的指標,nCount是陣列的元素個數,nElemSize是每個元素大小,pfnCompare就是我們的比較函式指標。這個宣告和C庫函式里的qsort是極為相似的。
  和C一樣,我們完全可以將Basic的函式指標傳遞給Basic的qsort函式。
  使用方式如下:
  Dim Employees(1 To 10000) As TEmployee
  '假設下面的呼叫對Employees陣列進行了賦值初始化。
  Call InitArray()
  '現在就可以呼叫我們的qsort來進行排序了。
  Call qsort(Vtr(Employees(1)), UBound(Employees), _
  LenB(Employees(1)), AddressOf CompareSalaryName)
  '或者先按姓名排,再按薪水排
  Call qsort(VarPtr(Employees(1)), UBound(Employees), _
  LenB(Employees(1)), AddressOf CompareNameSalary) 

  聰明的朋友們,你們是不是已經看出這裡的奧妙了呢?作為一個測驗,你能現在就給出在qsort裡使用函式指標的方法嗎?比如現在我們要透過呼叫函式指標來比較陣列的第i個元素和第j個元素的大小。
  沒錯,當然要使用前面宣告的Compare(其實就是CallWindowProc)這個API來進行強制回撥。
  具體的實現如下:
  Sub qsort(ByVal ArrayPtr As Long, ByVal nCount As Long, _
  ByVal nElemSize As Integer, ByVal pfnCompare As Long)
  Dim i As Long, j As Long
  '這裡省略排序演算法的具體實現,僅給出比較兩個元素的方法。
  If Compare(pfnCompare, ArrayPtr + (i - 1) * nElemSize, _
  ArrayPtr + (j - 1) * nElemSize, 0, 0) > 0 Then
  '如果第i個元素比第j個元素大則用CopyMemory來這兩個元素。
  End IF
  End Sub 

  招式介紹完了,明白了嗎?我再來簡單地講解一下上面Compare的意思,它非常巧妙地利用了CallWindowProc這個API。這個API需要五個引數,第一個引數就是一個普通的函式指標,這個API能夠強馬上回撥這個函式指標,並將這個API的後四個Long型的引數傳遞給這個函式指標所指向的函式。這就是為什麼我們的比較函式必須要有四個引數的原因,因為CallWindowProc這個API要求傳遞給的函式指標必須符合WndProc函式原形,WndProc的原形如下:
  LRESULT (CALLBACK* WNDPROC) (HWND, UINT, WPARAM, LPARAM);
  上面的LRESULT、HWND、UINT、WPARAM、LPARAM都可以對應於VB裡的Long型,這真是太好了,因為Long型可以用來作指標嘛!
  再來看看工作流程,當我們用AddressOf CompareSalaryName做為函式指標引數來呼叫qsort時,qsort的形參pfnCompare被賦值成了實參CompareSalaryName的函式指標。這時,呼叫Compare來強制回撥pfnCompare,就相當於呼叫瞭如下的VB語句:
  Call CompareSalaryName(ArrayPtr + (i - 1) * nElemSize, _
  ArrayPtr + (j - 1) * nElemSize, 0, 0)
  這不會引起引數型別不符錯誤嗎?CompareSalaryName的前兩個引數不是TEmployee型別嗎?的確,在VB裡這樣呼叫是不行的,因為VB的型別檢查不會允許這樣的呼叫。但是,實際上這個呼叫是API進行的回撥,而VB不可能去檢查API回撥的函式的引數型別是一個普通的Long數值型別還是一個結構指標,所以也可以說我們繞過了VB對函式引數的型別檢查,我們可以將這個Long型引數宣告成任何型別的指標,我們宣告成什麼,VB就認為是什麼。所以,我們要小心地使用這種技術,如上面最終會傳遞給CompareSalaryName函式的引數"ArrayPtr + (i - 1) * nElemSize"只不過是一個地址,VB不會對這個地址進行檢查,它總是將這個地址當做一個TEmployee型別的指標,如果不小心用成了"ArrayPtr + i * nElemSize",那麼當i是最後一個元素時,我們就會引起越權訪問錯誤,所以我們要和在C裡處理指標一樣注意邊界問題。
 
  函式指標的巧妙應用這裡已經可見一斑了,但是這裡介紹的方法還有很大的侷限性,我們的函式必須要有四個引數,更乾淨的做法還是在VC或裡寫一個DLL,做出更加符合要求的API來實現和CallWindowProc相似的功能。我跟蹤過CallWindowProc的內部實現,它要做許多和視窗訊息相關的工作,這些工作在我們這個應用中是多餘的。其實實現強制回撥API只需要將後幾個引數壓棧,再call第一個引數就行了,不過幾條指令而已。
  正是因為CallWindowProc的侷限性,我們不能夠用它來呼叫外部的函式指標,以實現上面說的第三種函式指標呼叫方式。要實現第三種方式,Matt Curland大師提供了一個噩夢一般的HACK方式,我們要在VB裡憑空構造一個IUnknown介面,在IUnknown介面的vTable原有的三個入口後再加入一個新入口,在新入口裡插入機器程式碼,這個機器程式碼要處理掉this指標,最後才能呼叫到我們給的函式指標,這個函式指標無論是內部的還是外部的都一樣沒問題。在我們深入討論COM內部原理時我會再來談這個方法。
  另外,排序演算法是個見仁見智的問題,我本來想,在本文提供一個最通用最好的演算法,這種想法雖好,但是不可能有在任何情況下都“最好”的演算法。本文提供的用各種指標技術來實現的快速排序方法,應該比用物件技術來實現同樣功能快不少,記憶體佔用也少得多。可是就是這個已經經過了我不少的快速排序演算法,還是比不了Sort,因為ShellSort實現上簡單。從演算法的理論上來講qsort應該比ShellSort平均效能好,但是在VB裡這不一定(可見本文配套程式碼,裡面也提供了VBPJ一篇專欄的配套程式碼ShellSort,非常得棒,本文的思想就取自這個ShellSort)。
  但是應當指出無論是這裡的快速排序還是ShellSort,都還可以大大改進,
因為它們在實現上需要大量使用CopyMemroy來複製資料(這是VB裡使用指標的缺點之一)。其實,我們還有更好的方法,那就是Hack一下VB的陣列結構,也就是COM自動化裡的SafeArray,我們可以一次性的將SafeArray裡的各個陣列元素的指標放到一個long型陣列裡,我們無需CopyMemroy,我們僅需交換Long型陣列裡的元素就可以達到實時地交換SafeArray陣列元素指標的目的,資料並沒有移動,移動的僅僅是指標,可以想象這有快多。在下一篇文章《VB指標葵花寶典之陣列指標》中我會來介紹這種方法。
 

後記:
  我學習所以我快樂。
 
 


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

相關文章