第二章為程序設計技術,本文為2.1.3 回調函數。
>>>>2.1.3 回調函數
>>>1.分層設計
分層設計就是將軟件分成具有某種上下級關系的模塊,由于每一層都是相對獨立的,因此只要定義好層與層之間的接口,從而每層都可以單獨實現。比如,設計一個保險箱電子密碼鎖,其硬件部分大致包括鍵盤、顯示器、蜂鳴器、鎖與存儲器等驅動電路,因此根據需求將軟件劃分為硬件驅動層、虛擬層與應用層三大模塊,當然每個大模塊又可以劃分為幾個小模塊,下面將以鍵盤掃描為例予以說明。
(1)硬件驅動層
硬件驅動層處于模塊的最底層,直接與硬件打交道。其任務是識別哪個鍵按下了,實現與硬件電路緊密相關的部分軟件,更高級的功能將在其它層實現。雖然通過硬件驅動層可以直達應用層,由于硬件電路變化多樣,如果應用層直接操作硬件驅動層,則應用層勢必依賴于硬件層,則最好的方法是增加一個虛擬層應對硬件的變化。顯然,只要鍵盤掃描的方法不變,則產生的鍵值始終保持不變,那么虛擬層的軟件也永遠不會改變。
(2)虛擬層
它是依據應用層的需求劃分的,主要用于屏蔽對象的細節和變化,則應用層就可以用統一的方法來實現了。即便控制方法改變了,也無需重新編寫應用層的代碼。
(3)應用層
應用層處于模塊的最上層,直接用于功能的實現,比如,應用層對外只有一個“人機交互”模塊,當然內部還可以劃分幾個模塊供自己使用。三層之間數據傳遞的關系非常清晰,即應用層->虛擬層->硬件驅動層,詳見圖 2.2,圖中的實線代表依賴關系,即應用層依賴于虛擬層,虛擬層依賴于硬件驅動層。基于分層的架構具有以下優點:
-
降低系統的復雜度:由于每層都是相對獨立的,層與層之間通過定義良好接口交互,每層都可以單獨實現,從而降低了模塊之間的耦合度;
-
隔離變化:軟件的變化通常發生在最上層與最下層,最上層是圖形用戶界面,需求的變化通常直接影響用戶界面,大部分軟件的新老版本在用戶界面上都會有很大差異。最下層是硬件,硬件的變化比軟件的發展更快,通過分層設計可以將這些變化的部分獨立開來,讓它們的變化不會給其它部分帶來大的影響;
-
有利于自動測試:由于每一層具有獨立的功能,則更易于編寫測試用例;
-
有利于提高程序的可移植性:通過分層設計將各種平臺不同的部分放在獨立的層里。比如,下層模塊是對操作系統提供的接口進行包裝的包裝層,上層是針對不同平臺所實現的圖形用戶界面。當移植到不同的平臺時,只需要實現不同的部分,而中間層都可以重用。
圖 2.2 三層結構示意
應用層處于模塊的最上層,直接用于功能的實現,比如,應用層對外只有一個“人機交互”模塊,當然內部還可以劃分幾個模塊供自己使用。三層之間數據傳遞的關系非常清晰,即應用層->虛擬層->硬件驅動層,詳見圖 2.2,圖中的實線代表依賴關系,即應用層依賴于虛擬層,虛擬層依賴于硬件驅動層?;诜謱拥募軜嬀哂幸韵聝烖c:
-
降低系統的復雜度:由于每層都是相對獨立的,層與層之間通過定義良好接口交互,每層都可以單獨實現,從而降低了模塊之間的耦合度;
-
隔離變化:軟件的變化通常發生在最上層與最下層,最上層是圖形用戶界面,需求的變化通常直接影響用戶界面,大部分軟件的新老版本在用戶界面上都會有很大差異。最下層是硬件,硬件的變化比軟件的發展更快,通過分層設計可以將這些變化的部分獨立開來,讓它們的變化不會給其它部分帶來大的影響;
-
有利于自動測試:由于每一層具有獨立的功能,則更易于編寫測試用例;
-
有利于提高程序的可移植性:通過分層設計將各種平臺不同的部分放在獨立的層里。比如,下層模塊是對操作系統提供的接口進行包裝的包裝層,上層是針對不同平臺所實現的圖形用戶界面。當移植到不同的平臺時,只需要實現不同的部分,而中間層都可以重用。
>>>2.隔離變化
(1)好萊塢原則(Hollywood)
類似鍵盤掃描這樣的模塊,其共性是各層之間的調用關系,不可能隨著時間而改變,即便上下層之間形成依賴關系,采用直接調用方式是最簡單的。為了降低層與層之間的耦合,層與層之間的通信必須按照一定的規則進行。即上層可以直接調用下層提供的函數,但下層不能直接調用上層提供的函數,且層與層之間絕對不能循環調用。因為層與層之間的循環依賴會嚴重妨礙軟件的復用性和可擴展性,使得系統中的每一層都無法獨立構成一個可復用的組件。雖然上層也可以調用相鄰下層提供的函數,但不能跨層調用。即下層模塊實現了在上層模塊中聲明并被高層模塊調用的接口,這就是著名的好萊塢(Hollywood)擴展原則:“不要調用我,讓我調用你?!碑斚聦有枰獋鬟f數據給上層時,則采用回調函數指針接口隔離變化。通過倒置依賴的接口所有權,創建了一個更靈活、更持久和更易于修改的結構。
實際上,由上層模塊(即調用者)提供的回調函數的表現形式就是在下層模塊中通過函數指針調用另一個函數,即將回調函數的地址作為實參初始化下層模塊的形參,由下層模塊在某個時刻調用這個函數,這個函數就是回調函數,詳見圖 2.3。其調用方式有兩種:
-
在上層模塊A調用下層模塊B的函數中,直接調用回調函數C;
-
使用注冊的方式,當某個事件發生時,下層模塊調用回調函數。
圖 2.3 回調函數的使用
在初始化時,上層模塊A將回調函數C的地址作為實參傳遞給下層模塊B。在運行中,當下層模塊需要與上層模塊通信時,調用這個回調函數。其調用方式為A→B→C,上層模塊A調用下層模塊B,在B的執行過程中,調用回調函數將信息返回給上層模塊。對于上層模塊來說,C不僅監視B的運行狀態,而且干預B的運行,其本質上依然是上層模塊調用下層模塊。由于增加了回調函數,即可在運行中實現動態綁定,下面將以標準的冒泡排序函數對一個任意類型的數據進行排序為例予以說明。
(2)數據比較函數
假設待排序的數據為int型,即可通過比較相鄰數據的大小,做出是否交換數據的處理。當給定兩個指向int型變量的指針e1和e2時,則比較函數返回一個數。如果*e1小于*e2,那么返回的數為負數;如果*e1大于*e2,那么返回的數為正數;如果*e1等于*e2,那么返回的數為0,詳見程序清單 2.4。
程序清單2.4 compare_int()數據比較函數
1 int compare_int(const int *e1, const int *e2)
2 {
3 return *e1 - *e2;//升序比較
4 }
5
6 int compare_int(const int *e1, const int *e2)
7 {
8 return *e2 - *e1; //降序比較
9 }
由于任何數據類型的指針都可以給void*指針賦值,因此可以利用這一特性,將void*指針作為數據比較函數的形參。當函數的形參聲明為void *類型時,雖然bubbleSort()冒泡排序函數內部不知道調用者會傳遞什么類型的數據過來,但調用者知道數據的類型和對數據的操作方法,那就由調用者編寫數據比較函數。
由于在運行時調用者要根據實際情況才能決定調用哪個數據比較函數,因此根據比較操作的要求,其函數原型如下:
typedef int (*COMPARE)(const void *e1, const void *e2);
其中的e1、e2是指向2個需要進行比較的值的指針。當返回值< 0時,表示e1 < e2;當返回值= 0時,表示e1 = e2;當返回值> 0時,表示e1 > e2。
當用typedef聲明后,COMPARE就成了函數指針類型,有了類型就可以定義該類型的函數指針變量。比如:
COMPARE compare;
此時,只要將函數名(比如,compare_int)作為實參初始化函數的形參,即可調用相應的數據比較函數。比如:
COMPARE compare=compare_int;
雖然編譯器看到的是一個compare,但調用者實現了多種不同類型的compare,即可根據接口函數中的類型改變函數的行為方式,通用數據比較函數的實現詳見程序清單 2.5。
程序清單 2.5 compare數據比較函數的實現
1 int compare_int(const void *e1, const void *e2)
2 {
3 return (*((int *)e1) - *((int *)e2)); //升序比較
4 }
5
6 int compare_int_invert(const void *e1, const void *e2)
7 {
8 return *(int *)e2 - *(int *)e1; //降序比較
9 }
10
11 int compare_vstrcmp(const void *e1, const void *e2)
12 {
13 return strcmp(*(char**)e1, *(char**)e2); //字符串比較
14 }
注意,如果e1是很大的正數,而e2是大負數,或者相反,則計算結果可能會溢出。由于這里假設它們都是正整數,從而避免了風險。
由于該函數的參數聲明為void *類型,因此數據比較函數不再依賴于具體的數據類型。即可將算法的變化部分獨立出來,無論是升序還是降序或字符串比較完全取決于回調函數。注意,之所以不能直接用strcmp()作為字符串的比較,因為bubbleSort()傳遞的是類型為char **的數組元素的地址&array[i],而不是類型為char*的array[i]。
(3)bubbleSort()冒泡排序函數
標準函數bubbleSort()是C中使用函數指針的經典示例,該函數是對一個具有任意類型的數組進行排序,其中單個元素的大小和要比較的元素的函數都是給定的。其原型初定如下:
bubbleSort(參數列表);
既然bubbleSort()是對數組中的數據排序,那么bubbleSort()必須有一個參數保存數組的起始地址,且還有一個參數保存數組中元素的個數。為了通用還是在數組中存放void *類型的元素,這樣一來就可以用數組存儲用戶傳入的任意類型的數據,因此用void *類型參數保存數組的起始地址。其函數原型如下:
bubbleSort(void *base, size_t nmemb);
由于數組的類型是未知的,那么數組中元素的長度也是未知的,同樣也需要一個參數來保存。其函數原型進化為:
bubbleSort(void *base, size_t nmemb, size_t size);
其中,size_t是C標準庫中預定義的類型,專門用于保存變量的大小。參數base和nmemb標識了這個數組,分別用于保存數組的起始地址和數組中元素的個數,size存儲的是打包時單個元素的大小。
此時,如果將指向compare()的指針作為參數傳遞給bubbleSort(),即可“回調”compare()進行值的比較。由于排序是對數據的操作,因此bubbleSort()沒有返回值,其類型為void,bubbleSort()函數接口詳見程序清單 2.6。
程序清單2.6bubbleSort()冒泡排序函數接口(bubbleSort.h)
1 #pragma once;
2 void bubbleSort(void *base, size_t nmemb, size_t size, COMPARE compare);
雖然大多數初學者也會選擇回調函數,但又經常用全局變量保存中間數據。這里提出的解決方法就是給回調函數傳遞一個稱為“回調函數上下文”的參數,其變量名為base。為了能接受任何數據類型,選擇void *表示這個上下文?!吧舷挛摹钡囊馑季褪钦f,如果傳進來的是int類型值,則回調int型數據比較函數;如果傳進來的是字符串,則回調字符串比較函數。
當bubbleSort()將base聲明為一個void *類型時,即允許bubbleSort()用相同的代碼支持不同類型的數據比較實現排序,其關鍵之處是type類型域,它允許在運行時根據數據的類型調用不同的函數。這種在運行時根據數據的類型將函數體與函數調用相關聯的行為稱為動態綁定,因此將一個函數的綁定發生在運行時而非編譯期,就稱該函數是多態的。顯然,多態是一種運行時綁定機制,其目的是將函數名綁定到函數的實現代碼。一個函數的名字與其入口地址是緊密相連的,入口地址是該函數在內存中的起始地址,因此多態就是將函數名動態地綁定到函數入口地址的運行時綁定機制,bubbleSort()的接口與實現詳見程序清單 2.7和程序清單 2.8。
程序清單2.7bubbleSort()接口(bubbleSort.h)
1 #pragma once
2 #include
3
4 typedef int(*COMPARE)(const void * e1, const void *e2);
5 void bubbleSort(void * base, size_t nmemb, size_t size, COMPARE compare);
程序清單2.8bubbleSort()接口的實現(bubbleSort.c)
1 #include"bubbleSort.h"
2
3 void byte_swap(void *pData1, void *pData2, size_t stSize)
4 {
5 unsigned char *pcData1 = pData1;
6 unsigned char *pcData2 = pData2;
7 unsigned char ucTemp;
8
9 while (stSize--){
10 ucTemp = *pcData1; *pcData1 = *pcData2; *pcData2 = ucTemp;
11 pcData1++; pcData2++;
12 }
13 }
14
15 void bubbleSort(void * base, size_t nmemb, size_t size, COMPARE compare)
16 {
17 int hasSwap=1;
18
19 for (size_t i = 1; hasSwap&&i < nmemb; i++) {
20 hasSwap = 0;
21 for (size_t j = 0; j < numData - 1; j++) {
22 void *pThis = ((unsigned char *)base) + size*j;
23 void *pNext = ((unsigned char *)base) + size*(j+1);
24 if (compare(pThis, pNext) > 0) {
25 hasSwap = 1;
26 byte_swap(pThis, pNext, size);
27 }
28 }
29 }
30 }
靜態類型和動態類型
類型的靜態和動態指的是名字與類型綁定的時間,如果所有的變量和表達式的類型在編譯時就固定了,則稱之為靜態綁定;如果所有的變量和表達式的類型直到運行時才知道,則稱之為動態綁定。
假設要實現一個用于任意數據類型的冒泡排序函數并簡單測試,其要求是同一個函數既可以從大到小排列,也可以從小到大排列,且同時支持多種數據類型。比如:
int array[] = {39, 33, 18, 64, 73, 30, 49, 51, 81};
顯然,只要將比較函數的入口地址compare_int傳遞給compare,即可調用bubbleSort():
int array[] = {39, 33, 18, 64, 73, 30, 49, 51, 81};
bubbleSort(array, numArray , sizeof(array[0]), compare_int);
在數量不大時,所有排序算法性能差別不大,因為高級算法只有在元素個數多于1000時,性能才出現顯著提升。其實90%以上的情況下,我們存儲的元素個數只有幾十到幾百個,冒泡排序可能是更好的選擇,bubbleSort()的實現與使用范例程序詳見程序清單 2.9。
程序清單 2.9 bubbleSort()冒泡排序范例程序
1 #include
2 #include
3 #include"bubbleSort.h"
4
5 int compare_int(const void * e1, const void * e2)
6 {
7 return *(int *)e1 - *(int *)e2;
8 }
9
10 int compare_int_r(const void * e1, const void * e2)
11 {
12 return *(int *)e2 - *(int *)e1 ;
13 }
14
15 int compare_str(const void * e1, const void *e2)
16 {
17 return strcmp(*(char **)e1, *(char **)e2);
18 }
19
20 void main()
21 {
22 int arrayInt[] = { 39, 33, 18, 64, 73, 30, 49, 51, 81 };
23 int numArray = sizeof(arrayInt) / sizeof(arrayInt[0]);
24 bubbleSort(arrayInt, numArray, sizeof(arrayInt[0]), compare_int);
25 for (int i = 0; i
26 printf("%d ", arrayInt[i]);
27 }
28 printf("\n");
29
30 bubbleSort(arrayInt, numArray, sizeof(arrayInt[0]), compare_int_r);
31 for (int i = 0; i
32 printf("%d ", arrayInt[i]);
33 }
34 printf("\n");
35
36 char * arrayStr[] = { "Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday" };
37 numArray = sizeof(arrayStr) / sizeof(arrayStr[0]);
38 bubbleSort(arrayStr, numArray, sizeof(arrayStr[0]), compare_str);
39 for (int i = 0; i < numArray; i++) {?
40 printf("%s\n", arrayStr[i]);
41 }
42 }
由此可見,調用者main()與compare_int()回調函數都同屬于上層模塊,bubbleSort()屬于下層模塊。當上層模塊調用下層模塊bubbleSort()時,將回調函數的地址compare_int作為參數傳遞給bubbleSort(),進而調用compare_int()。顯然,使用參數傳遞回調函數的方式,下層模塊不必知道需要調用上層模塊的哪個函數,從而減少了上下層之間的聯系,這樣上下層可以獨立修改,而不影響另一層代碼的實現。這樣一來,在每次調用bubbleSort()時,只要給出不同的函數名作為實參,則bubbleSort()不必做任何修改。
使用回調函數的最大優點就是便于軟件模塊的分層設計,降低軟件模塊之間的耦合度。即回調函數可以將調用者與被調用者隔離,調用者無需關心誰是被調用者。當特定的事件或條件發生時,調用者將使用函數指針調用回調函數對事件進行處理。
-
周立功
+關注
關注
38文章
130瀏覽量
37672 -
回調函數
+關注
關注
0文章
87瀏覽量
11583 -
單片機程序設計
+關注
關注
0文章
2瀏覽量
6331
原文標題:周立功:做好軟件模塊的分層設計必須掌握的回調函數
文章出處:【微信號:ZLG_zhiyuan,微信公眾號:ZLG致遠電子】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論