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