摘要:這篇文章詳細介紹C/C++的函數指針,請先看以下幾個主題:使用函數指針定義新的類型、使用函數指針作爲參數、使用函數指針作爲返回值、使用函數指針作爲回調函數、使用函數指針數組,使用類的靜態函數成員的函數指針、使用類的普通函數成員的指針、定義函數指針數組類型、使用函數指針實現後綁定以及在結構體中定義函數指針。如果您對以上這幾個主題都很瞭解,那麼恭喜您,這篇文章不適合您啦~。在一些開源軟件中,如Boost, Qt, lam-mpi中我們經常看到函數指針,本文目的是徹底搞定函數指針的語法和語義,至於怎樣將函數指針應用到系統架構中不在此文的討論範圍中。各位看官,有磚拍磚啊~
1. 無處不見的函數指針
使用函數指針可以設計出更優雅的程序,比如設計一個集羣的通信框架的底層通信系統:首先將要每個消息的對應處理函數的指針保存映射表中(使用STL的map,鍵是消息的標誌,值是對應的函數指針),然後啓動一個線程在結點上的某個端口偵聽,收到消息後,根據消息的編號,從映射表中找到對應的函數入口,將消息體數據作爲參數傳給相應的函數。我曾看過lam-mpi在啓動集羣中每個結點的進程時的實現,該模塊的最上層就是一個結構體,這個結構體中僅是由函數指針構成,每個函數指針都指向一個子模塊,這樣做的好處就是在運行時期間可以自由的切換子模塊。比如某個子模塊不適合某個體系結構,只需要改動函數指針,指向另外一個模塊就可。
在平時的程序設計中,經常遇到函數指針。如EnumWindows這個函數的參數,C語言庫函數qsort的參數,定義新的線程時,這些地方函數指針都是作爲回調函數來應用的。
還有就是unix的庫函數signal(sys/signal.h)(這個函數我們將多次用到)的聲明形式爲:
void (*signal)(int signo,void (*func)(int)))(int);
這個形式是相當複雜的,因爲它不僅使用函數指針作爲參數,而且返回類型還是函數指針(雖然這個函數在POSIX中不被推薦使用了)。
還有些底層實現實際上也用到了函數指針,可能你已經猜到了。嗯,就是C++中的多態。這是一個典型的遲綁定(late-binding)的例子,因爲在編譯時是無法確定到底綁定到哪個函數上執行,只有在運行時的時候才能確定。這個可以通過下面這個例子來幫助理解:
Shape *pSh;
scanf(“%d”,&choice);
if(choice)
{
pSh= new Rectangle();
}
else
{
pSh= new Square();
}
pSh->display();
對於上面這段代碼,做以下幾個假設:
(1) Square繼承自Rectange
(2) Rectangle繼承自Shape
(3) display爲虛函數,在每個Shape的子類鏈中都必須實現
正是因爲在編譯期間無法確定choice的值,所以在編譯到最後一行的時候無法確定應該綁定到那個一個函數上,只能在運行期間根據choice的值,來確定要綁定的函數的地址。
總之,使用指針可以讓我們寫出更加優雅,高效,靈活的程序。另外,和普通指針相比,函數指針還有一個好處就是你不用擔心內存釋放問題。
但是,函數指針確實很難學的,我認爲難學的東西主要有兩個原因:(1)語法過於複雜。(2)語義過於複雜。從哲學上講,可以對應爲(1)形式過於複雜。(2)內容過於複雜。
比如,如果我們要描述“美女”這種動物(老婆不要生氣啊~),如果在原始時代,我們可能需要通過以下這種方式:
_____ &&&&_) )
\/,---< &&&&&&\ \
( )c~c~~@~@ )- - &&\ \
C >/ \< |&/
\_O/ - 哇塞 _`*-'_/ /
,- >o<-. / ____ _/
/ \/ \ / /\ _)_)
/ /| | |\ \ / / ) |
\ \| | |/ / \ \ / |
\_\ | |_/ \ \_ |
/_/`___|_\ /_/\____|
| | | \ \|
| | | `. )
| | | / /
|__|_|_ /_/|
(____)_) |\_\_
而現在我們只需要用語言來抽象就行,即用兩個漢字“美女”或者英文“beauty”就行了。這就是形式上的簡化,也就方便了我們的交流。另外一種就是內容上的複雜度過高,一個高度抽象的表達式後面蘊含着巨大的複雜度對於我們理解問題也是很難的,例如:
P=NP?
由於接觸過的書上所講的關於函數指針方面的都是蜻蜓點水一樣,讓我很不滿足。我認爲C/C++語言函數指針難學的主要原因是由於其形式上的定義過於複雜,但是在內容上我們一定要搞清楚函數的本質。函數的本質就是表達式的抽象,它在內存中對應的數據結構爲堆棧幀,它表示一段連續指令序列,這段連續指令序列在內存中有一個確定的起始地址,它執行時一般需要傳入參數,執行結束後會返回一個參數。和函數相關的,應該大致就是這些內容吧。
2 函數指針簡單介紹
2.1 什麼是函數指針
函數指針是一個指向函數的指針(呃,貌似是廢話),函數指針表示一個函數的入口地址。使用函數指針的好處就是在處理“在運行時根據數據的具體狀態來選擇相應的處理方式”這種需求時更加靈活。
2.2 一個簡單的例子
下面是一個簡單的使用函數指針取代switch-case語句的例子,爲了能夠比較出二者效率差異,所以在循環中進行了大量的計算。
- /*
- *Author:Choas Lee
- *Date:2012-02-28
- */
- #include<stdio.h>
- #define UNIXEVN
- #if defined(UNIXENV)
- #include<sys/time.h>
- #endif
- #define N 1000000
- #define COE 1000000
- float add(float a,float b){return a+b;}
- float minus(float a,float b){return a-b;}
- float multiply(float a,float b){return a*b;}
- float divide(float a,float b){return a/b;}
- typedef float (*pf)(float,float);
- void switch_impl(float a,float b,char op)
- {
- float result=0.0;
- switch(op)
- {
- case '+':
- result=add(a,b);
- break;
- case '-':
- result=minus(a,b);
- break;
- case '*':
- result=multiply(a,b);
- break;
- case '/':
- result=divide(a,b);
- break;
- }
- }
- void switch_fp_impl(float a,float b,pf p)
- {
- float result=0.0;
- result=p(a,b);
- }
- int conversion(struct timeval tmp_time)
- {
- return tmp_time.tv_sec*COE+tmp_time.tv_usec;
- }
- int main()
- {
- int i=0;
- #if defined(UNIXENV)
- struct timeval start_point,end_point;
- gettimeofday(&start_point,NULL);
- #endif
- for(i=0;i<N;i++)
- {
- switch_impl(12.32,54.14,'-');
- }
- #if defined(UNIXENV)
- gettimeofday(&end_point,NULL);
- printf("check point 1:%d\n",conversion(end_point)-conversion(start_point));
- gettimeofday(&start_point,NULL);
- #endif
- for(i=0;i<N;i++)
- {
- switch_fp_impl(12.32,54.14,minus);
- }
- #if defined(UNIXENV)
- gettimeofday(&end_point,NULL);
- printf("check point 2:%d\n",conversion(end_point)-conversion(start_point));
- #endif
- return 0;
- }
下面是執行結果:
- [lichao@sg01 replaceswitch]$ ./replaceswitch
- check point 1:22588
- check point 2:19407
- [lichao@sg01 replaceswitch]$ ./replaceswitch
- check point 1:22656
- check point 2:19399
- [lichao@sg01 replaceswitch]$ ./replaceswitch
- check point 1:22559
- check point 2:19380
- [lichao@sg01 replaceswitch]$ ./replaceswitch
- check point 1:22181
- check point 2:19667
- [lichao@sg01 replaceswitch]$ ./replaceswitch
- check point 1:22226
- check point 2:19813
- [lichao@sg01 replaceswitch]$ ./replaceswitch
- check point 1:22141
- check point 2:19893
- [lichao@sg01 replaceswitch]$ ./replaceswitch
- check point 1:21640
- check point 2:19745
從上面可以看出,使用函數指針:(一)在某種程度上簡化程序的設計(二)可以提高效率。在這個例子中,使用函數指針可以提高10%的效率。
注意:以上代碼在unix環境下實現的,如果要在windows下運行,可以稍微改下,把“#define UNIXENV”行刪掉即可
3 C/C++函數指針的語法
從語法上講,有兩種不兼容的函數指針形式:
(1) 指向C語言函數和C++靜態成員函數的函數指針
(2) 指向C++非靜態成員函數的函數指針
不兼容的原因是因爲在使用C++非靜態成員函數的函數指針時,需要一個指向類的實例的this指針,而前一類不需要。
3.1 定義一個函數指針
指針是變量,所以函數指針也是變量,因此可以使用變量定義的方式來定義函數指針,對於普通的指針,可以這麼定義:
int a=10;
int *pa=&a;
這裏,pa是一個指向整型的指針,定義這個指針的形式爲:
int * pa;
區別於定義非指針的普通變量的“形式”就是在類型中間和指針名稱中間加了一個“*”,所以能夠表達不同的“內容”。這種形式對於表達的內容是完備的,因爲它說明了兩點:(1)這是一個指針(2)這是一個指向整型變量的指針
以下給出三個函數指針定義的形式,第一個是C語言的函數指針,第二個和第三個是C++的函數指針的定義形式(都是指向非靜態函數成員的函數指針):
int (*pFunction)(float,char,char)=NULL;
int (MyClass::*pMemberFunction)(float,char,char)=NULL;
int (MyClass::*pConstMemberFunction)(float,char,char) const=NULL;
我們先不管函數指針的定義形式,如果讓我們自己來設計指向函數的函數指針的定義形式的話,我們會怎麼設計?
首先,要記住一點的就是形式一定要具備完備性,能表達出我們所要表達的內容,即指向函數這個事實。我們知道普通變量指針可以指向對應類型的任何變量,同樣函數指針也應該能夠指向對應類型的任何變量。對應的函數類型靠什麼來確定?這個我們可以想一下C++的函數重載靠什麼來區分不同的函數?這裏,函數類型是靠這幾個方面來確定的:(1)函數的參數個數(2)函數的參數類型(3)函數的返回值類型。所以我們要設計一種形式,這種形式定義的函數指針能夠準確的指向這種函數類型的任何函數。
在C語言中這種形式爲:
返回類型 (*函數指針名稱)(參數類型,參數類型,參數類型,…);
嗯,定義變量的形式顯然不是我們通常見到的這種形式:
類型名稱 變量名稱;
但是,這也是爲了表達函數這種相對複雜的語義而不得已採用的非一致表示形式的方法。因爲定義的這個函數指針變量,能夠明確的表達出它指向什麼類型的函數,這個函數都有哪些類型的參數這些信息,確切的說,它是完備的。你可能會問爲什麼要加括號?形式上講能不能更簡潔點?不能,因爲不加括號就會產生二義性:
返回類型 *函數指針名稱(參數類型,參數類型,參數類型,…);
這樣的定義形式定義了一個“返回類型爲‘返回類型*’參數爲(參數類型,參數類型,參數類型,…)的函數而不是函數指針了。
接下來,對於C++來說,下面這樣的定義形式也就不難理解了(加上類名稱是爲了區分不同類中定義的相同名稱的成員函數):
返回類型 (類名稱::*函數成員名稱)(參數類型,參數類型,參數類型,….)
3.2 函數的調用規則
一般來說,不用太關注這個問題。調用規則主要是指函數被調用的方式,常見的有_stdcall,_fastcall,_pascal,_cdecl等規則。不同的規則在參數壓入堆棧的順序是不同的,同時在有調用者清理壓入堆棧的參數還是由被調用者清理壓入堆棧的參數上也是不同的。一般來說,如果你沒有顯式的說明調用規則的話,編譯器會統一按照_cdecl來處理。
3.3 給函數指針賦值和調用
給函數指針賦值,就是爲函數指針指定一個函數名稱。這個過程很簡單,下面是兩個例子:
int func1(float f,int a,int b){return f*a/b;}
int func2(float f,int a,int b){return f*a*b}
然後我們給函數指針pFunction賦值:
pFunction=func1;
pFunction=&func2;
上面這段代碼說明了兩個問題:(1)一個函數指針可以多次賦值(想想C++中的引用)(2)取地址符號是可選的,卻是推薦使用的。
我們可以思考一下爲什麼取地址符號是可選的,在普通的指針變量賦值時,如上面所示,需要加取地址符號,而這裏卻是可選的?這是由於要同時考慮到兩個因素(1)避免二義性(2)形式一致性。在普通指針賦值,需要加取地址符號是爲了區別於將地址還是將內容賦給指針。而在函數賦值時沒有這種考慮,因爲這裏的語義是清晰的,加上&符號是爲了和普通指針變量一致---“因爲一致的時候就不容易出錯”。
最後我們來使用這個函數
pFunction(10.0,’a’,’b’);
(*pFunction)(10.0,’a’,’b’);
上面這兩種使用函數指針調用函數的方式都是可以的,原因和上面一樣。
下面來說明C++中的函數指針賦值和調用,這裏說明非靜態函數成員的情況,C++中規則要求的嚴格的多了。讓我感覺C++就像函數指針的後爸一樣,對函數指針要求特別死,或許是因爲他有一個函數對象這個親兒子。
在C++中,對於賦值,你必須要加“&”,而且你還必須再次之前已經定義好了一個類實例,取地址符號要操作於這個類實例的對應的函數成員上。在使用成員函數的指針調用成員函數時,你必須要加類實例的名稱,然後再使用.*或者->*來使用成員函數指針。舉例如下:
MyClass
{
public:
int func1(float f,char a,char b)
{
return f*a*b;
}
int func2(float f,char a,char b) const
{
return f*a/b;
}
}
首先來賦值:
MyClass mc;
pMemberFunction= &mc.func1; //必須要加取地址符號
pConstMemberFunction = &mc.func2;
接下來,調用函數:
(mc.*pMemberFunction)(10.0,’a’,’b’);
(mc.*pConstMemberFunction)(10.0,’a’,’b’);
我感覺,C++簡直在虐待函數指針啊。
下面是一個完整的例子:
- /*
- *Author:Choas Lee
- *Date:2012-02-28
- */
- #include<stdio.h>
- float func1(float f,char a,char b)
- {
- printf("func1\n");
- return f*a/b;
- }
- float func2(float f,char a,char b)
- {
- printf("func2\n");
- return f*a*b;
- }
- class MyClass
- {
- public:
- MyClass(float f)
- {
- factor=f;
- }
- float func1(float f,char a,char b)
- {
- printf("MyClass::func1\n");
- return f*a/b*factor;
- }
- float func2(float f,char a,char b) const
- {
- printf("MyClass::func2\n");
- return f*a*b*factor;
- }
- private:
- float factor;
- };
- int main(int argc,char *argv[])
- {
- float (*pFunction)(float,char,char)=NULL;
- float (MyClass::*pMemberFunction)(float,char,char)=NULL;
- float (MyClass::*pConstMemberFunction)(float,char,char)const=NULL;
- float f=10.0;
- char a='a',b='b';
- float result;
- pFunction=func1;
- printf("pointer pFunction's address is:%x\n",pFunction);
- result=(*pFunction)(f,a,b);
- printf("result=%f\n",result);
- pFunction=&func2;
- printf("pointer pFunction's address is:%x\n",pFunction);
- result=pFunction(f,a,b);
- printf("result=%f\n",result);
- if(func1!=pFunction)
- printf("not equal.\n");
- pMemberFunction=&MyClass::func1;
- MyClass mc1(0.2);
- printf("pointer pMemberFunction's address is:%x\n",pMemberFunction);
- result=(mc1.*pMemberFunction)(f,a,b);
- printf("result=%f\n",result);
- pConstMemberFunction=&MyClass::func2;
- MyClass mc2(2);
- printf("pointer pConstMemberFunction's address is:%x\n",pConstMemberFunction);
- result=(mc2.*pConstMemberFunction)(f,a,b);
- printf("result=%f\n",result);
- return 0;
- }
運行結果爲:
- pointer pFunction's address is:400882
- func1
- result=9.897959
- pointer pFunction's address is:400830
- func2
- result=95060.000000
- not equal.
- pointer pMemberFunction's address is:400952
- MyClass::func1
- result=1.979592
- pointer pConstMemberFunction's address is:4008f2
- MyClass::func2
- result=190120.000000
注意:上面的代碼還說明了一點就是函數指針的一些基本操作,函數指針沒有普通變量指針的算術操作,但是可以進行比較操作。如上面代碼所示。
使用類的靜態函數成員的函數指針和使用C語言的函數很類似,這裏僅僅給出一個例子和其執行結果:
程序代碼爲:
- /*
- *Author:Chaos Lee
- *Date:2012-02-28
- */
- #include<iostream>
- class MyClass
- {
- public:
- static float plus(float a,float b)
- {
- return a+b;
- }
- };
- int main()
- {
- float result,a=10.0,b=10.0;
- float (*p)(float,float);
- p=&MyClass::plus;
- result=p(a,b);
- printf("result=%f\n",result);
- return 0;
- }
執行結果爲:
- result=20.000000
由於字數比較多,一篇發不完,和函數指針相關的更精彩的內容請看下一篇博文哈~
本文出自 “相信並熱愛着” 博客,請務必保留此出處http://hipercomer.blog.51cto.com/4415661/792300