一、函數指針
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,它的宣告如下:
它需要一個COMPARE類型函數指針,用來比較兩個變數大小的,這樣排序函數可以呼叫這個函數指標來比較不同類型的變量,所以qsort可以對不同類型的變數數組進行排序。
我們姑且稱這種應用為"從內部呼叫內部的函數指標"。
3、呼叫外部的函數
也許你會問,用API不就是呼叫外部的函數嗎?是的,但有時候我們還是需要直接取得外部函數的指標。例如透過LoadLibrary動態載入DLL,然後再透過GetProcAddress得到我們需要的函數入口指針,然後再透過這個函數指標來呼叫外部的函數,這個動態載入DLL的技術可以讓我們更靈活的呼叫外部函數。
我們稱這種方式為"從內部呼叫外部的函數指標"
4.不用說,就是我們也可控制"從外部呼叫外部的函數指標"。不是沒有,像是我們可以載入多個DLL,將其中一個DLL中的函式指標傳到另一個DLL裡的函式內。
上面所分的"內"和"外"都是相對而言(DLL實際上還是在進程內),這樣分類有助於以後我們談問題,請記住我上面的分類,因為以後的文章也會用到這個分類來分析問題。
函數指標的使用不外乎上面四種方式。但在實際使用上卻是靈活多變的。例如在C 裡繼承和多態,在COM裡的接口,都是一種叫做vTable的函數指標表的巧妙應用。使用函數指針,可以使程式的處理方式更有效率、更靈活。
VB文檔裡除了介紹過第一方式外,對其它方式都沒有介紹,並且還明確指出不支持“Basic到Basic”的函數指針(也就是上面說的第二種方式),實際上,透過一定的HACK,上面四種方式都可以實現。今天,我們就來看看如何實現第二種方式,因為實現它相對來說比較簡單,我們先從簡單的入手。至於如何在VB內部呼叫外部的函數指針,如何在VB裡透過處理vTable介面函數指針跳躍表來實現各種函數指針的巧妙應用,由於這將涉及COM內部原理,我將另文詳述。
其實VB的文檔並沒有說錯,VB的確不支援「Basic到Basic」的函數指針,但是我們可以繞個彎子來實現,那就是先從"Basic到API",然後再用第一種方式"外部呼叫內部的函數指標"來從"API到BASIC",這樣就達到了第二種方式從"Basic到Basic"的目的,這種技術我們可以稱之為"強制回調",只有VB裡才會有這種古怪的技術。
說得有點繞口,但仔細想想視窗子類別派生技術裡CallWindowProc,我們可以用CallWindowProc來強制外部的作業系統呼叫我們原來的保存的視窗函數指針,同樣我們也完全可以用它來強制呼叫我們內部的函數指標。
呵呵,前面說過要少講原理多講招式,現在我們就來開始學招式吧!
考慮我們在VB裡來實作和C裡一樣支援多關鍵字比較的qsort。完整的原始碼請參閱本文配套程式碼,此處僅給出函數指標應用相關的程式碼。
最後再看看我們來看看我們最終的qsort的聲明。
上面的ArrayPtr是需要排序數組的第一個元素的指針,nCount是數組的元素個數,nElemSize是每個元素大小,pfnCompare就是我們的比較函數指針。這個宣告和C函式庫函數裡的qsort是極為相似的。
和C一樣,我們完全可以將Basic的函數指標傳遞給Basic的qsort函數。
使用方式如下:
聰明的朋友們,你們是不是已經看出這裡的奧妙了呢?作為一個測驗,你能現在就給出在qsort裡使用函數指標的方法嗎?例如現在我們要透過呼叫函數指標來比較陣列的第i個元素和第j個元素的大小。
沒錯,當然要使用前面宣告的Compare(其實就是CallWindowProc)這個API來進行強制回調。
具體的實現如下:
招式介紹完了,懂了嗎?我再來簡單地講解一下上面Compare的意思,它非常巧妙地利用了CallWindowProc這個API。這個API需要五個參數,第一個參數就是一個普通的函數指針,這個API能夠強馬上回調這個函數指針,並將這個API的後四個Long型的參數傳遞給這個函數指針所指向的函數。這就是為什麼我們的比較函數必須要有四個參數的原因,因為CallWindowProc這個API要求傳遞給的函數指標必須符合WndProc函數原形,WndProc的原形如下:
上面的LRESULT、HWND、UINT、WPARAM、LPARAM都可以對應到VB裡的Long型,這真是太好了,因為Long型可以用來當指針嘛!
再來看看工作流程,當我們用AddressOfCompareSalaryName做為函數指標參數來呼叫qsort時,qsort的形參pfnCompare被賦值成了實參CompareSalaryName的函數指標。這時,呼叫Compare來強制回呼pfnCompare,就等於呼叫如下的VB語句:
這不會引起參數類型不符錯誤嗎? CompareSalaryName的前兩個參數不是TEmployee類型嗎?的確,在VB裡這樣呼叫是不行的,因為VB的型別檢查不會允許這樣的呼叫。但是,實際上這個呼叫是API進行的回調,而VB不可能去檢查API回調的函數的參數類型是一個普通的Long數值類型還是一個結構指針,所以也可以說我們繞過了VB對函數參數的類型檢查,我們可以將這個Long型參數宣告成任何類型的指針,我們宣告成什麼,VB就認為是什麼。所以,我們要小心地使用這種技術,如上面最終會傳遞給CompareSalaryName函數的參數"ArrayPtr (i-1)*nElemSize"只不過是一個地址,VB不會對這個地址進行檢查,它總是將這個位址當做一個TEmployee類型的指針,如果不小心用成了"ArrayPtr i*nElemSize",那麼當i是最後一個元素時,我們就會引起記憶體越權存取錯誤,所以我們要和在C裡處理指標一樣注意邊界問題。
函數指標的巧妙應用這裡已經可見一斑了,但這裡介紹的方法還有很大的局限性,我們的函數必須要有四個參數,更乾淨的做法還是在VC或Delphi裡寫一個DLL,做出更符合要求的API來實現和CallWindowProc相似的功能。我追蹤過CallWindowProc的內部實現,它要做許多和視窗訊息相關的工作,這些工作在我們這個應用中是多餘的。其實實作強制回呼API只需要將後幾個參數壓棧,再call第一個參數就行了,不過幾條彙編指令而已。
正是因為CallWindowProc的局限性,我們不能夠用它來呼叫外部的函數指針,以實現上面所說的第三種函數指針呼叫方式。要實現第三種方式,MattCurland大師提供了一個噩夢一般的HACK方式,我們要在VB裡憑空構造一個IUnknown接口,在IUnknown接口的vTable原有的三個入口後再加入一個新入口,在新入口裡插入機器碼,這個機器碼要處理掉this指針,最後才能呼叫到我們給的函數指針,這個函數指針無論是內部的還是外部的都一樣沒問題。在我們深入討論COM內部原理時我會再來談這個方法。
另外,排序演算法是個見仁見智的問題,我本來想,在本文提供一個最通用性能最好的演算法,這種想法雖好,但是不可能有在任何情況下都「最好」的演算法。本文提供的用各種指標技術來實現的快速排序方法,應該比用物件技術來實現同樣功能快不少,記憶體佔用也少得多。但就是這個已經經過了我不少優化的快速排序演算法,還是比不了ShellSort,因為ShellSort實作上簡單。從演算法的理論上來講qsort應該比ShellSort平均表現好,但是在VB裡這不一定(可見本文配套程式碼,裡面也提供了VBPJ一篇專欄的配套程式碼ShellSort,非常得棒,本文的想法就取自這個ShellSort)。
但是應當指出無論是這裡的快速排序還是ShellSort,都還可以大大改進,因為它們在實作上需要大量使用CopyMemroy來拷貝資料(這是VB裡使用指標的缺點之一)。其實,我們還有更好的方法,那就是Hack一下VB的陣列結構,也就是COM自動化裡的SafeArray,我們可以一次性的將SafeArray裡的各個陣列元素的指標放到一個long型陣列裡,我們無需CopyMemroy,我們只需交換Long型數組裡的元素就可以達到實時地交換SafeArray數組元素指針的目的,數據並沒有移動,移動的只是指針,可以想像這有快多。
->