一回呼函數
我們經常在C++設計時通過使用回呼函數可以使有些應用(如定時器事件回調處理、用回呼函數記錄某操作進度等)變得非常方便和符合邏輯,那麼它的內在機制如何呢,怎麼定義呢?它和其它函數(比如鉤子函數)有何不同呢?
使用回呼函數實際上就是在調用某個函數(通常是API函數)時,將自己的一個函數(這個函數為回呼函數)的地址作為參數傳遞給那個函數。
而 那個函數在需要的時候,利用傳遞的地址調用回呼函數,這時你可以利用這個機會在回呼函數中處理訊息或完成一定的操作。至於如何定義回呼函數,跟具體使用的 API函數有關,一般在協助中有說明回呼函數的參數和傳回值等。C++中一般要求在回呼函數前加CALLBACK(相當於FAR PASCAL),這主要是說明該函數的調用方式。
至於鉤子函數,只是回呼函數的一個特例。習慣上把與SetWindowsHookEx函數一起使用的回呼函數稱為鉤子函數。也有人把利用VirtualQueryEx安裝的函數稱為鉤子函數,不過這種叫法不太流行。
也可以這樣,更容易理解:回呼函數就好像是一個中斷處理函數,系統在符合你設定的條件時自動調用。為此,你需要做三件事:
1. 聲明;
2. 定義;
3. 設定觸發條件,就是在你的函數中把你的回呼函數名稱轉化為地址作為一個參數,以便於系統調用。
聲明和定義時應注意:回呼函數由系統調用,所以可以認為它屬於WINDOWS系統,不要把它當作你的某個類的成員函數。
二回呼函數、訊息和事件常式
調用(calling)機制從彙編時代起已經大量使用:準備一段現成的代碼,調用者可以隨時跳轉至此段代碼的起始地址,執行完後再返回跳轉時的後續地址。 CPU為此準備了現成的調用指令,調用時可以壓棧保護現場,調用結束後從堆棧中彈出現場地址,以便自動返回。借堆棧保護現場真是一項絕妙的發明,它使調用 者和被調者可以互不相識,於是才有了後來的函數和構件。
此調用機制並非完美。回呼函數就是一例。函數之類本是為調用者準備的美餐,其烹制者應對食客了如指掌,但實情並非如此。例如,寫一個快速排序函數供他人調 用,其中必包含比較大小。麻煩來了:此時並不知要比較的是何類資料--整數、浮點數、字串?於是只好為每類資料製作一個不同的排序函數。更通行的辦法是 在函數參數中列一個回呼函數地址,並通知調用者:君需自己準備一個比較函數,其中包含兩個指標類參數,函數要比較此二指標所指資料之大小,並由函數傳回值 說明比較結果。排序函數藉此調用者提供的函數來比較大小,借指標傳遞參數,可以全然不管所比較的資料類型。被調用者回頭調用調用者的函數(夠咬嘴的),故 稱其為回調(callback)。
回呼函數使程式結構亂了許多。Windows API 函數集中有不少回呼函數,儘管有詳盡說明,仍使初學者一頭霧水。恐怕這也是無奈之舉。
無論何種事物,能以樹形結構單向描述畢竟讓人舒服些。如果某家族中孫輩又是某祖輩的祖輩,恐怕無人能理清其中的頭緒。但資料處理之複雜往往需要構成網狀結構,非簡單的客戶/伺服器關係能窮盡。
Windows 系統還包含著另一種更為廣泛的回調機制,即訊息機制。訊息本是 Windows 的基本控制手段,乍看與函數調用無關,其實是一種變相的函數調用。發送訊息的目的是通知收方運行一段預先準備好的代碼,相當於調用一個函數。訊息所附帶的 WParam 和 LParam 相當於函數的參數,只不過比普通參數更通用一些。應用程式可以主動發送訊息,更多情況下是坐等 Windows 發送訊息。一旦訊息進入所屬訊息佇列,便檢感興趣的那些,跳轉去執行相應的訊息處理代碼。作業系統本是為應用程式服務,由應用程式來調用。而應用程式一旦 啟動,卻要反過來等待作業系統的調用。這分明也是一種回調,或者說是一種廣義回調。其實,應用程式之間也可以形成這種回調。假如進程 B 收到進程 A 發來的訊息,啟動了一段代碼,其中又向進程 A 發送訊息,這就形成了回調。這種回調比較隱蔽,弄不好會搞成遞迴調用,若缺少終止條件,將會迴圈不已,直至把程式搞垮。若是故意編寫成此遞迴調用,並設好 終止條件,倒是很有意思。但這種程式結構太隱蔽,除非十分必要,還是不用為好。
利用訊息也可以構成狹義回調。上面所舉排序函數一例,可以把回呼函數地址換成視窗 handle。如此,當需要比較資料大小時,不是去調用回呼函數,而是借 API 函數 SendMessage 向指定視窗發送訊息。收到訊息方負責比較資料大小,把比較結果通過訊息本身的傳回值傳給訊息發送方。所實現的功能與回呼函數並無不同。當然,此例中改為消 息純屬畫蛇添腳,反倒把程式搞得很慢。但其他情況下並非總是如此,特別是需要非同步呼叫時,發送訊息是一種不錯的選擇。假如回呼函數中包含檔案處理之類的低 速處理,調用方等不得,需要把同步調用改為非同步呼叫,去啟動一個單獨的線程,然後馬上執行後續代碼,其餘的事讓線程慢慢去做。一個替代辦法是借 API 函數 PostMessage 發送一個非同步訊息,然後立即執行後續代碼。這要比自己搞個線程省事許多,而且更安全。
如今我們是活在一個 object 時代。只要與編程有關,無論何事都離不開 object。但 object 並未消除回調,反而把它發揚光大,弄得到處都是,只不過大都以事件(event)的身份出現,鑲嵌在某個結構之中,顯得更正統,更容易被人接受。應用程式 要使用某個構件,總要先弄清構件的屬性、方法和事件,然後給構件屬性賦值,在適當的時候調用適當的構件方法,還要給事件編寫處理常式,以備構件代碼來調 用。何謂事件?它不過是一個指向事件常式的地址,與回呼函數地址沒什麼區別。
不過,此種回調方式比傳統回呼函數要高明許多。首先,它把讓人不太舒服的回呼函數變成一種自然而然的處理常式,使編程者頓覺氣順。再者,地址是一個危險的 東西,用好了可使程式加速,用不好處處是陷阱,程式隨時都會崩潰。現代編程方式總是想法把地址隱藏起來(隱藏比較徹底的如 VB 和 Java),其代價是降低了程式效率。事件常式(?)使編程者無需直接操作地址,但並不會使程式減速。
(常式似乎是進程的台灣翻譯。)
三精妙比喻:回呼函數還真有點像您隨身帶的BP機:告訴別人號碼,在它有事情時Call您。
回調用於層間協作,上層將本層函數安裝在下層,這個函數就是回調,而下層在一定條件下觸發回調,例如作為一個驅動,是一個底層,他在收到一個資料時,除了 完成本層的處理工作外,還將進行回調,將這個資料交給上層應用程式層來做進一步處理,這在分層的資料通訊中很普遍。其實回調和API非常接近,他們的共性都是 跨層調用的函數。但區別是API是低層提供給高層的調用,一般這個函數對高層都是已知的;而回調正好相反,他是高層提供給底層的調用,對於低層他是未知 的,必須由高層進行安裝,這個安裝函數其實就是一個低層提供的API,安裝後低層不知道這個回調的名字,但它通過一個函數指標來儲存這個回調,在需要調用 時,只需引用這個函數指標和相關的參數指標。 其實:回調就是該函數寫在高層,低層通過一個函數指標儲存這個函數,在某個事件的觸發下,低層通過該函數指標調用高層那個函數。