學過C語言的同僚或其他讀者,大都應該對其指針的概念感到比較敬畏
有人學完C語言後,指針使用的爐火純青,當然也有同學此後的程序再沒用過指針來做
這並不是說沒使用指針的程序不是好程序,不過指針確實是使得C語言之所以是C語言的重要原因之一
本文並不直接再敘述一遍指針的概念,而是向各位介紹C語言指針的一種妙用——鉤子函數
對於追求執行效率的中大型程序而言,一段程序即使能少執行一條簡單的代碼,也可能得到一定的性能提升
這種軟件程序中,經常存在需要按數據類型、業務類型定位處理的函數,這個定位速度如果能提高
整個程序的執行速度也會變快(這裏是理論上的,並不是想說肉眼可見的變快)
鉤子函數本質其實就是靈活的使用函數指針,我們先看看不用鉤子函數時一段程序一般怎麼寫:
(需求:輸入兩個整數和運算法則編號,輸出該法則下兩個數的計算結果)
#include <stdio.h>
#define OPER_TYPE_ADD 0 /* 加法 */
#define OPER_TYPE_MINUS 1 /* 減法 */
#define OPER_TYPE_MULTI 2 /* 乘法 */
int add(int a, int b) { return a + b; }
int minus(int a, int b) { return a - b; }
int multi(int a, int b) { return a * b; }
/***************** Main程序 ********************/
int main(void)
{
int x, y, oper;
scanf("%d %d %d", &x, &y, &oper);
switch(oper)
{
case OPER_TYPE_ADD:
printf("result = %d\n", add(x, y));
break;
case OPER_TYPE_MINUS:
printf("result = %d\n", minus(x, y));
break;
case OPER_TYPE_MULTI:
printf("result = %d\n", multi(x, y));
break;
defult:
printf("Null oper\n");
}
return 0;
}
嘛,這兒我用的switch,當然if-else也沒問題,不過這兩種流程設計的話在這種業務類型很少的時候還OK
對於實際生產中的可能有幾十上百種業務或者數據類型的時候,再這樣做,程序的執行順序就是:
(1)switch結構:從頭開始挨個比對oper,直到匹配到oper再開始執行代碼
(2)if-else結構:各個if-else分支的邏輯表達式挨個計算挨個比較,直到邏輯成立再執行程序
假設有100種計算法則,上述實現最糟糕的情況是每次都在第100次比對才執行,當計算請求很大量很頻繁時
這段程序的執行效率就顯得不是那麼高了
下面我們看鉤子函數怎麼處理這個需求:
#include <stdio.h>
int add(int a, int b) { return a + b; }
int minus(int a, int b) { return a - b; }
int multi(int a, int b) { return a * b; }
/* 註冊消息 */
enum{
OPER_TYPE_ADD = 0, /* 加法 */
OPER_TYPE_MINUS, /* 減法 */
OPER_TYPE_MULTI, /* 乘法 */
OPER_TYPE_DEV, /* 除法 */
OPER_TYPE_MAX
};
typedef struct handle_cb{
int type;
int (*handle)(int a, int b);
}HANDLE_CB;
/*
註冊鉤子,這裏顯式地把消息類型和鉤子函數對應關係體現出來了,
handle_cb裏面的type可以不要的,注意維護好註冊消息表中
每個枚舉值先後順序和下面鉤子函數表中的先後順序一致,能對應上
就行了
*/
HANDLE_CB g_handle [] = {
{ OPER_TYPE_ADD, add },
{ OPER_TYPE_MINUS, minus },
{ OPER_TYPE_MULTI, multi },
{ OPER_TYPE_DEV, NULL}
};
/***************** Main程序 ********************/
int main(void)
{
int i;
int type;
int x, y, oper;
int (*func)(int x, int y);
scanf("%d %d %d", &x, &y, &oper);
/*
這就是掛鉤子的過程,如果回調消息類型很多,
可以用type來定位g_handle中註冊的鉤子函數,
鉤子函數好處在於是提供一個接口,公共平臺部分代碼
不用關係接口內實現,要做新功能的時候直接註冊
消息類型和回調函數即可,不用大動公共部分
*/
func = g_handle[oper].handle;
printf("result = %d\n", func(x, y));
/*
同理,一則消息需要多種處理時可以用for循環
*/
func = NULL;
printf("\n");
for (i = 0; i < OPER_TYPE_MAX; i++)
{
func = g_handle[i].handle;
if (func != NULL)
{
printf("result = %d\n", func(x, y));
}
}
return 0;
}
對代碼敏感的話你已經看出區別來了,鉤子函數在確定業務類型後就直接能確定處理程序
無論需求中的業務和數據類型有多少種,只要類型能確定,就能按數組角標找到對應的鉤子
在大量和頻繁的計算請求到來時,每個處理都不需要和其他類型比對,只執行一次就能正確處理該類型
從代碼維護和迭代的角度看,這種代碼設計方式也是很合理的
在新增業務類型時,直接在全局註冊表中新增enum和鉤子函數指針即可
如果一個類型暫時沒有處理但該類型必須有,我們把鉤子置爲NULL即可
一則是代碼量不會像switch和if-else那樣膨脹式增長,二則代碼簡練,可讀性好,執行效率高
以下這種把函數指針定義爲類型來處理的方式也是鉤子函數的一種應用:
#include <stdio.h>
/* 用typedef定義以後,可以當變量名一樣自定義其他函數名字 */
typedef int(*FUNC)(int x, int y);
/* 兩個鉤子函數 */
int add(int a, int b) { return a + b; }
int minus(int a, int b) { return a - b; }
int main(void)
{
FUNC p; /* p可以理解爲鉤子,掛在哪個函數上就實現哪種流程 */
/* 以下兩個流程就是同一接口調用兩種實現 */
p = add;
printf("%d + %d = %d\n", 100, 211, (*p)(100, 211));
p = minus;
printf("%d - %d = %d\n", 100, 67, (*p)(100, 67));
return 0;
}
如果你對UNIX/Linux系統的源碼實現有一定了解,就會見到這種鉤子函數的實現
函數指針帶來的靈活性賦予了C語言很大的生機
在C語言實現各個經典的設計模式的時候基本都會用到鉤子函數的技巧