函數指針
AddressOf得到一個(gè)VB內部的函數指針,我們可以將這個(gè)函數指針傳遞給需要回調這個(gè)函數的API,它的作用就是讓外部的程序可以調用VB內部的函數。
但是VB里函數指針的應用,遠不象C里應用那么廣泛,因為VB文檔里僅介紹了如何將函數指針傳遞給API以實(shí)現回調,并沒(méi)指出函數指針諸多神奇的功能,因為VB是不鼓勵使用指針的,函數指針也不例外。
首先讓我們對函數指針的使用方式來(lái)分個(gè)類(lèi)。
1、回調。這是最基本也是最重要的功能。比如VB文檔里介紹過(guò)的子類(lèi)派生技術(shù),它的核心就是兩個(gè)API:SetWindowLong和CallWindowProc。
我們可以使SetWindowLong這個(gè)API來(lái)將原來(lái)的窗口函數指針換成自己的函數指針,并將原來(lái)的窗口函數指針保存下來(lái)。這樣窗口消息就可以發(fā)到 我們自己的函數里來(lái),并且我們隨時(shí)可以用CallWindowProc來(lái)調用前面保存下來(lái)的窗口指針,以調用原來(lái)的窗口函數。這樣,我們可以在不破壞原有 窗口功能的前提下處理鉤入的消息。
具體的處理,我們應該很熟悉了,VB文檔也講得很清楚了。這里需要注意的就是CallWindowProc這個(gè)API,在后面我們將看到它的妙用。
在這里我們稱(chēng)回調為讓"外部調用內部的函數指針"。
2、程序內部使用。比如在C里我們可以將C函數指針作為參數傳遞給一個(gè)需要函數指針的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);
它需要一個(gè)COMPARE類(lèi)型函數指針,用來(lái)比較兩個(gè)變量大小的,這樣排序函數可以調用這個(gè)函數指針來(lái)比較不同類(lèi)型的變量,所以qsort可以對不同類(lèi)型的變量數組進(jìn)行排序。
我們姑且稱(chēng)這種應用為"從內部調用內部的函數指針"。
3、調用外部的函數
也許你會(huì )問(wèn),用API不就是調用外部的函數嗎?是的,但有時(shí)候我們還是需要直接獲取外部函數的指針。比如通過(guò)LoadLibrary動(dòng)態(tài)加載DLL,然 后再通過(guò)GetProcAddress得到我們需要的函數入口指針,然后再通過(guò)這個(gè)函數指針來(lái)調用外部的函數,這種動(dòng)態(tài)載入DLL的技術(shù)可以讓我們更靈活 的調用外部函數。
我們稱(chēng)這種方式為"從內部調用外部的函數指針"
4、不用說(shuō),就是我們也可控制"從外部調用外部的函數指針"。不是沒(méi)有,比如我們可以加載多個(gè)DLL,將其中一個(gè)DLL中的函數指針傳到另一個(gè)DLL里的函數內。
上面所分的"內"和"外"都是相對而言(DLL實(shí)際上還是在進(jìn)程內),這樣分類(lèi)有助于以后我們談問(wèn)題,請記住我上面的分類(lèi),因為以后的文章也會(huì )用到這個(gè)分類(lèi)來(lái)分析問(wèn)題。
函數指針的使用不外乎上面四種方式。但在實(shí)際使用中卻是靈活多變的。比如在C++里繼承和多態(tài),在COM里的接口,都是一種叫vTable的函數指針表的巧妙應用。使用函數指針,可以使程序的處理方式更加高效、靈活。
VB文檔里除了介紹過(guò)第一方式外,對其它方式都沒(méi)有介紹,并且還明確指出不支持“Basic 到 Basic”的函數指針(也就是上面說(shuō)的第二種方式),實(shí)際上,通過(guò)一定的HACK,上面四種方式均可以實(shí)現。今天,我們就來(lái)看看如何來(lái)實(shí)現第二種方式, 因為實(shí)現它相對來(lái)說(shuō)比較簡(jiǎn)單,我們先從簡(jiǎn)單的入手。至于如何在VB內調用外部的函數指針,如何在VB里通過(guò)處理vTable接口函數指針跳轉表來(lái)實(shí)現各種 函數指針的巧妙應用,由于這將涉及COM內部原理,我將另文詳述。
其實(shí)VB的文檔并沒(méi)有說(shuō)錯,VB的確不支持“Basic 到 Basic”的函數指針,但是我們可以繞個(gè)彎子來(lái)實(shí)現,那就是先從"Basic到API",然后再用第一種方式"外部調用內部的函數指針"來(lái)從"API到 BASIC",這樣就達到了第二種方式從"Basic 到 Basic"的目的,這種技術(shù)我們可以稱(chēng)之為"強制回調",只有VB里才會(huì )有這種古怪的技術(shù)。
說(shuō)得有點(diǎn)繞口,但是仔細想想窗口子類(lèi)派生技術(shù)里CallWindowProc,我們可以用CallWindowProc來(lái)強制外部的操作系統調用我們原來(lái)的保存的窗口函數指針,同樣我們也完全可以用它來(lái)強制調用我們內部的函數指針。
呵呵,前面說(shuō)過(guò)要少講原理多講招式,現在我們就來(lái)開(kāi)始學(xué)習招式吧!
考慮我們在VB里來(lái)實(shí)現和C里一樣支持多關(guān)鍵字比較的qsort。完整的源代碼見(jiàn)本文配套代碼,此處僅給出函數指針應用相關(guān)的代碼。
‘當然少不了的CopyMemory,不用ANY的版本。
Declare Sub CopyMemory Lib "kernel32" Alias _
"RtlMoveMemory" (ByVal dest As Long, ByVal source 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
‘再來(lái)看看我們的比較函數
‘先按薪水比較,再按姓名比較
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
最后再看看我們來(lái)看看我們最終的qsort的聲明。
Sub qsort(ByVal ArrayPtr As Long, ByVal nCount As Long, _
ByVal nElemSize As Integer, ByVal pfnCompare As Long)
上面的ArrayPtr是需要排序數組的第一個(gè)元素的指針,nCount是數組的元素個(gè)數,nElemSize是每個(gè)元素大小,pfnCompare就是我們的比較函數指針。這個(gè)聲明和C庫函數里的qsort是極為相似的。
和C一樣,我們完全可以將Basic的函數指針傳遞給Basic的qsort函數。
使用方式如下:
Dim Employees(1 To 10000) As TEmployee
‘假設下面的調用對Employees數組進(jìn)行了賦值初始化。
Call InitArray()
‘現在就可以調用我們的qsort來(lái)進(jìn)行排序了。
Call qsort(VarPtr(Employees(1)), UBound(Employees), _
LenB(Employees(1)), AddressOf CompareSalaryName)
‘或者先按姓名排,再按薪水排
Call qsort(VarPtr(Employees(1)), UBound(Employees), _
LenB(Employees(1)), AddressOf CompareNameSalary)
聰明的朋友們,你們是不是已經(jīng)看出這里的奧妙了呢?作為一個(gè)測驗,你能現在就給出在qsort里使用函數指針的方法嗎?比如現在我們要通過(guò)調用函數指針來(lái)比較數組的第i個(gè)元素和第j個(gè)元素的大小。
沒(méi)錯,當然要使用前面聲明的Compare(其實(shí)就是CallWindowProc)這個(gè)API來(lái)進(jìn)行強制回調。
具體的實(shí)現如下:
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
‘這里省略快速排序算法的具體實(shí)現,僅給出比較兩個(gè)元素的方法。
If Compare(pfnCompare, ArrayPtr + (i - 1) * nElemSize, _
ArrayPtr + (j - 1) * nElemSize, 0, 0) > 0 Then
‘如果第i個(gè)元素比第j個(gè)元素大則用CopyMemory來(lái)交換這兩個(gè)元素。
End IF
End Sub
招式介紹完了,明白了嗎?我再來(lái)簡(jiǎn)單地講解一下上面Compare的意思,它非常巧妙地利用了CallWindowProc這個(gè)API。這個(gè)API需要 五個(gè)參數,第一個(gè)參數就是一個(gè)普通的函數指針,這個(gè)API能夠強馬上回調這個(gè)函數指針,并將這個(gè)API的后四個(gè)Long型的參數傳遞給這個(gè)函數指針所指向 的函數。這就是為什么我們的比較函數必須要有四個(gè)參數的原因,因為CallWindowProc這個(gè)API要求傳遞給的函數指針必須符合WndProc函 數原形,WndProc的原形如下:
LRESULT (CALLBACK* WNDPROC) (HWND, UINT, WPARAM, LPARAM);
上面的LRESULT、HWND、UINT、WPARAM、LPARAM都可以對應于VB里的Long型,這真是太好了,因為Long型可以用來(lái)作指針嘛!
再來(lái)看看工作流程,當我們用AddressOf CompareSalaryName做為函數指針參數來(lái)調用qsort時(shí),qsort的形參pfnCompare被賦值成了實(shí)參 CompareSalaryName的函數指針。這時(shí),調用Compare來(lái)強制回調pfnCompare,就相當于調用了如下的VB語(yǔ)句:
Call CompareSalaryName(ArrayPtr + (i - 1) * nElemSize, _
ArrayPtr + (j - 1) * nElemSize, 0, 0)
這不會(huì )引起參數類(lèi)型不符錯誤嗎?CompareSalaryName的前兩個(gè)參數不是TEmployee類(lèi)型嗎?的確,在VB里這樣調用
聯(lián)系客服