周立功教授數年之心血之作《程序設計與數據結構》以及《面向AMetal框架與接口的編程(上)》。書本內容公開后,在電子行業掀起一片學習熱潮。經周立功教授授權,本公眾號特對《程序設計與數據結構》一書內容進行連載,愿共勉之。
第二章為程序設計技術,本文為2.1.1 函數指針和2.1.2指針函數。
>>>>2.1函數指針與指針函數
>>>2.1.1函數指針
變量的指針指向的是一塊數據,指針指向不同的變量,則取到的是不同的數據。而經過編譯后的函數都是一段代碼,系統隨即為相應的代碼分配一段存儲空間,而存儲這段代碼的起始地址(又稱為入口地址)就是這個函數的指針,即跳轉到某一個地址單元的代碼處去執行。函數指針指向的是一段代碼(即函數),指針指向不同的函數,則具有不同的行為。
因為函數名是一個常量地址,所以只要將函數的地址賦給函數指針即可調用相應的函數。如同數組名一樣,我們用的是函數本身的名字,它會返回函數的地址。當一個函數名出現在表達式中時,編譯器就會將其轉換為一個指針,即類似于數組變量名的行為,隱式地取出了它的地址。即函數名直接對應于函數生成的指令代碼在內存中的地址,因此函數名可以直接賦值給指向函數的指針。既然函數指針的值可以改變,那么就可以使用同一個函數指針指向不同的函數。如果有以下定義:
int (*pf)(int); // pf函數指針的類型是什么?
C語言的發明者K&R是這樣解釋的,“因為*是前置運算符,它的優先級低于(),為了讓連接正確地進行,有必要加上括號。”這未免有些牽強附會了,解釋來解釋去反而將人搞暈了。因為聲明中的*、()、[]都不是運算符,而運算符的優先順序在語法規則中是在其它地方定義的。其詳解如下:
int(*pf)(int a); // pf是指向…的指針
int(*pf)(int a); // pf是指向…的函數(參數為int)的指針
int (*pf)(int a); // pf是指向返回int的函數(參數為int)的指針
即pf是一個指向返回int的函數的指針,它所指向的函數接受一個int類型的參數。 “int (*)(int)”類型名被解釋為指向返回int函數(參數為int)的指針類型。如果在該定義前添加typedef,比如:
typedef int (*pf)(int a);
未添加typedef前,pf是一個函數指針變量;而添加typedef后,pf就變成了函數指針類型,習慣的寫法是類型名pf大寫為PF。比如:
typedef int (*PF)(int a);
與其它類型的聲明不同,函數指針的聲明要求使用typedef關鍵字。另外,函數指針的聲明與函數原型的唯一不同是函數名用(*PF)代替了,“*”在此處表示“指向類型名為PF的函數”。顯然,有了PF類型即可定義函數指針變量pf1、pf2。比如:
PF pf1, pf2;
雖然此聲明等價于:
int (*pf1)(int a);
int (*pf2)(int a);
但這種寫法更難理解。既然函數指針變量是一個變量,那么它的值就是可以改變的,因此可以使用同一個函數指針變量指向不同的函數。使用函數指針必須完成以下工作:
●獲取函數的地址,比如,pf = add,pf = sub;
●聲明一個函數指針,比如,“int (*pf)(int, int);”;
●使用函數指針來調用函數,比如,pf(5, 8),(*pf)(5, 8)。為何pf與(*pf)等價呢?
●一種說法是,由于pf是函數指針,假設pf指向add()函數,則*pf就是函數add,因此使用(*pf)()調用函數。雖然這種格式不好看,但它給出了強有力的提示——代碼正在使用函數指針調用函數。
●另一種說法是,由于函數名是指向函數的指針,那么指向函數的指針的行為應該與函數名相似,因此使用pf()調用函數。因為這種調用方式既簡單又優雅,所以人們更愿意選擇——說明人類追隨美好感受的內心是無法抗拒的。
雖然它們在邏輯上互相沖突,但不同的流派有不同的觀點,且容忍邏輯上無法自圓其說的觀點,正是人類思維活動的特點。
在一個袖珍計算器中,經常需要用到加減乘除開方等各種各樣的計算,雖然其調用方法都是一樣,但在運行中需要根據具體情況決定選擇調用支持某一算法的函數。如果使用如圖 2.1(a)所示的直接調用方式,則勢必形成了依賴關系結構,策略會受到細節改變的影響,當使用如圖 2.1(b)所示的函數指針接口倒置(或反轉)了這種依賴關系結構時,則使得細節和策略都依賴于函數指針接口,斷開了不想要的直接依賴。
當將直接訪問抽象成函數指針倒置(或反轉)了依賴的關系時,高層模塊不再依賴于低層模塊。高層模塊依賴于抽象,即一個函數指針形式的接口,同時細節也依賴于抽象,pf()實現了這個接口,即兩者都依賴于函數指針接口。在C語言中,通常用函數指針來實現DIP(倒置依賴關系),斷開不想要的直接依賴。既可以通過函數指針調用服務(被調用代碼),服務也可以通過函數指針回調用戶函數。都是一樣,但在運行中需要根據具體情況決定選擇調用支持某一算法的函數。如果使用如圖 2.1(a)所示的直接調用方式,則勢必形成了依賴關系結構,策略會受到細節改變的影響,當使用如圖 2.1(b)所示的函數指針接口倒置(或反轉)了這種依賴關系結構時,則使得細節和策略都依賴于函數指針接口,斷開了不想要的直接依賴。
圖 2.1 使用函數指針倒置依賴關系
函數指針是程序員經常忽視的一個強大的語言能力,不僅使代碼更靈活可測,而且對消除重復條件邏輯有很大的幫助,同時還可以使調用者免于在編譯時或鏈接時依賴于某個特定的函數,其極大地好處是減少了C語言模塊之間的耦合。但函數指針的使用是有條件的,如果主調函數與被調函數之間的調用關系永遠不會發生改變,則采用直接調用方式是最簡單的,在這種情況下,模塊之間耦合是合理的,不僅代碼簡單直截了當,而且開銷也是最小的。如果需要在運行時使用一個或多個函數指針調用某一函數,則使用函數指針是最佳的選擇,通常將其稱之為動態接口,其范例程序詳見程序清單 2.1。
程序清單2.1通過函數指針調用函數范例程序(1)
1#include
2 int add(int a, int b)
3 {
4 printf("addition function ");
5 return a + b;
6 }
7
8 int sub(int a, int b)
9 {
10 printf("subtration function ");
11 return a - b;
12 }
13
14 int main(void)
15 {
16 int (*pf)(int, int);
17
18 pf = add;
19 printf("addition result:%d ", pf(5, 8));
20 pf = sub;
21 printf("subtration result:%d ", pf(8, 5));
22 return 0;
23 }
由于任何數據類型的指針都可以給void指針變量賦值,且函數指針的本質就是一個地址,因此可以利用這一特性,將pf定義為一個void *類型指針,那么任何指針都可以賦值給void *類型指針變量。其調用方式如下:
void * pf = add;
printf("addition result:%d ", ((int (*)(int, int)) pf)(5, 8));
在函數指針的使用過程中,指針的值表示程序將要跳轉的地址,指針的類型表示程序的調用方式。在使用函數指針調用函數時,務必保證調用的函數類型與指向的函數類型完全相同,所以必須將void *類型轉換為((int (*)(int, int)) pf)來使用,其類型為“int (*)(int, int)”。
>>>2.1.2指針函數
實際上,指針變量的用途非常廣泛,指針不僅可以作為函數的參數,而且指針還可以作為函數的返回值。當函數的返回值是指針時,則這個函數就是指針函數。當給定指向兩個整數的指針時,如程序清單 2.2所示的函數返回指向兩個整數中較大數的指針。當調用max時,用指向兩個int類型變量的指針作為參數,且將結果存儲在一個指針變量中,其中,max函數返回的指針是作為實參傳入的兩個指針的一個。
程序清單2.2求最大值函數(指針作為函數的返回值)
1 #include
2 int *max(int *p1, int *p2)
3 {
4 if(*p1 > *p2)
5 return p1;
6 else
7 return p2;
8 }
9
10 int main(int argc, char *argv[])
11 {
12 int *p, a, b;
13 a = 1; b = 2;
14 p = max(&a, &b);
15 printf("%d ", *p);
16 return 0;
17 }
當然,函數也可以返回字符串,它返回的實際是字符串的地址,但一定要注意如何返回合法的地址。既可以返回是靜態的字符串地址,也可以在堆上分配字符串的內存,然后返回其地址。注意,不要返回局部字符串的地址,因為內存有可能被別的棧幀覆寫。
下面我們再來看一看,指針函數與函數指針變量有什么區別?如果有以下定義:
int *pf(int *, int); // int *(int *, int)類型
int (*pf)(int, int); // int (*)(int, int)類型
雖然兩者之間只差一個括號,但表示的意義卻截然不同。函數指針變量的本質是一個指針變量,其指向的是一個函數;指針函數的本質是一個函數,即將pf聲明為一個函數,它接受2個參數,其中一個是int *,另一個是int,其返回值是一個int類型的指針。
在指針函數中,還有一類這樣的函數,其返回值是指向函數的指針。對于初學者,別說寫出這樣的函數聲明,就是看到這樣的寫法也是一頭霧水。比如,下面這樣的語句:
int (*ff (int))(int, int); // ff是一個函數
int (* ff (int))(int, int); // ff是一個指針函數,其返回值是指針
int(* ff (int))(int, int); //指針指向的是一個函數
這種寫法確實讓人非常難懂,以至于一些初學者產生誤解,認為寫出別人看不懂的代碼才能顯示自己水平高。而事實上恰好相反,能否寫出通俗易懂的代碼是衡量程序員是否優秀的標準。當使用typedef后,則PF就成為了一個函數指針類型。即:
typedef int (*PF)(int, int);
有了這個類型,那么上述函數的聲明就變得簡單多了。即:
PF ff(int);
下面將以程序清單 2.3為例,說明用函數指針作為函數返回值的用法。當用戶分別輸入d、x和p時,求數組的最大值、最小值和平均值。
程序清單2.3求最值與平均值范例程序
1 #include
2 #include
3 double getMin(double *dbData, int iSize) //求最小值
4 {
5 double dbMin;
6
7 assert((dbData != NULL) && (iSize > 0));
8 dbMin = dbData[0];
9 for (int i = 1; i < iSize; i++){?
10 if (dbMin > dbData[i]){
11 dbMin = dbData[i];
12 }
13 }
14 return dbMin;
15 }
16
17 double getMax(double *dbData, int iSize) //求最大值
18 {
19 double dbMax;
20
21 assert((dbData != NULL) && (iSize > 0));
22 dbMax = dbData[0];
23 for (int i = 1; i < iSize; i++){
24 if (dbMax < dbData[i]){?
25 dbMax = dbData[i];
26 }
27 }
28 return dbMax;
29 }
30
31 double getAverage(double *dbData, int iSize) //求平均值
32 {
33 double dbSum = 0;
34
35 assert((dbData != NULL) && (iSize > 0));
36 for (int i = 0; i < iSize; i++){
37 dbSum += dbData[i];
38 }
39 return dbSum/iSize;
40 }
41
42 double unKnown(double *dbData, int iSize) //未知算法
43 {
44 return 0;
45 }
46
47 typede double (*PF)(double *dbData, int iSize); //定義函數指針類型
48 PF getOperation(char c) //根據字符得到操作類型,返回函數指針
49 {
50 switch (c){
51 case 'd':
52 return getMax;
53 case 'x':
54 return getMin;
55 case 'p':
56 return getAverage;
57 default:
58 return unKnown;
59 }
60 }
61
62 int main(void)
63 {
64 double dbData[] = {3.1415926, 1.4142, -0.5, 999, -313, 365};
65 int iSize = sizeof(dbData) / sizeof(dbData[0]);
66 char c;
67
68 printf("Please input the Operation : ");
69 c = getchar();
70 PF pf = getOperation(c);
71 printf("result is %lf ", pf(dbData, iSize));
72 return 0;
73 }
前4個函數分別實現了求最大值、最小值、平均值和未知算法,getOperation()根據輸入字符得到的返回值是以函數指針的形式返回的,從pf(dbData, iSize)可以看出是通過這個指針調用函數的。注意,指針函數可以返回新的內存地址、全局變量的地址和靜態變量的地址,但不能返回局部變量的地址,因為函數結束后,在函數內部的聲明的局部變量的聲明周期已經結束,內存將自動放棄。顯然,在主調函數中訪問這個指針所指向的數據,將會產生不可預料的結果。
想學更多嵌入式課程,請掃描下圖二維碼,馬上學習!
-
指針
+關注
關注
1文章
480瀏覽量
70512 -
數據結構
+關注
關注
3文章
573瀏覽量
40095 -
程序設計
+關注
關注
3文章
261瀏覽量
30368 -
函數指針
+關注
關注
2文章
56瀏覽量
3775 -
指針函數
+關注
關注
0文章
10瀏覽量
2741 -
ametal
+關注
關注
2文章
24瀏覽量
11395
原文標題:周立功:函數指針與指針函數的應用
文章出處:【微信號:ZLG_zhiyuan,微信公眾號:ZLG致遠電子】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論