【轉】嗯,讓我們徹底搞懂C/C++函式指標吧
摘要:這篇文章詳細介紹C/C++的函式指標,請先看以下幾個主題:使用函式指標定義新的型別、使用函式指標作為引數、使用函式指標作為返回值、使用函式指標作為回撥函式、使用函式指標陣列,使用類的靜態函式成員的函式指標、使用類的普通函式成員的指標、定義函式指標陣列型別、使用函式指標實現後繫結以及在結構體中定義函式指標。如果您對以上這幾個主題都很瞭解,那麼恭喜您,這篇文章不適合您啦~。在一些開源軟體中,如Boost, Qt, lam-mpi中我們經常看到函式指標,本文目的是徹底搞定函式指標的語法和語義,至於怎樣將函式指標應用到系統架構中不在此文的討論範圍中。各位看官,有磚拍磚啊~
無處不見的函式指標
使用函式指標可以設計出更優雅的程式,比如設計一個叢集的通訊框架的底層通訊系統:首先將要每個訊息的對應處理函式的指標儲存對映表中(使用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++語言函式指標難學的主要原因是由於其形式上的定義過於複雜,但是在內容上我們一定要搞清楚函式的本質。函式的本質就是表示式的抽象,它在記憶體中對應的資料結構為堆疊幀,它表示一段連續指令序列,這段連續指令序列在記憶體中有一個確定的起始地址,它執行時一般需要傳入引數,執行結束後會返回一個引數。和函式相關的,應該大致就是這些內容吧。
函式指標簡單介紹
什麼是函式指標
函式指標是一個指向函式的指標(呃,貌似是廢話),函式指標表示一個函式的入口地址。使用函式指標的好處就是在處理“在執行時根據資料的具體狀態來選擇相應的處理方式”這種需求時更加靈活。
一個簡單的例子
下面是一個簡單的使用函式指標取代switch-case
語句的例子,為了能夠比較出二者效率差異,所以在迴圈中進行了大量的計算。
#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”
行刪掉即可。
C/C++函式指標的語法
從語法上講,有兩種不相容的函式指標形式:
(1) 指向C語言函式和C++靜態成員函式的函式指標
(2) 指向C++非靜態成員函式的函式指標
不相容的原因是因為在使用C++非靜態成員函式的函式指標時,需要一個指向類的例項的this
指標,而前一類不需要。
定義一個函式指標
指標是變數,所以函式指標也是變數,因此可以使用變數定義的方式來定義函式指標,對於普通的指標,可以這麼定義:
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++來說,下面這樣的定義形式也就不難理解了(加上類名稱是為了區分不同類中定義的相同名稱的成員函式):
返回型別 (類名稱::*函式成員名稱)(引數型別,引數型別,引數型別,….)
函式的呼叫規則
一般來說,不用太關注這個問題。呼叫規則主要是指函式被呼叫的方式,常見的有_stdcall,_fastcall,_pascal,_cdecl
等規則。不同的規則在引數壓入堆疊的順序是不同的,同時在有呼叫者清理壓入堆疊的引數還是由被呼叫者清理壓入堆疊的引數上也是不同的。一般來說,如果你沒有顯式的說明呼叫規則的話,編譯器會統一按照_cdecl
來處理。
給函式指標賦值和呼叫
給函式指標賦值,就是為函式指標指定一個函式名稱。這個過程很簡單,下面是兩個例子:
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++中,對於賦值,你必須要加“&”,(注:這裡原作者說的並不準確,對於類的成員函式,也可以不用取地址符,但對於類的非靜態成員函式,必須定義好類例項。原因是非靜態成員函式一般要處理物件的非靜態資料成員,這就需要傳遞this指標,所以必須例項化類為物件。)而且你還必須再次之前已經定義好了一個類例項,取地址符號要操作於這個類例項的對應的函式成員上。在使用成員函式的指標呼叫成員函式時,你必須要加類例項的名稱,然後再使用.或者->來使用成員函式指標。舉例如下:
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; //必須要加取地址符號 ps.其實並不用 0.0
pConstMemberFunction = &mc.func2;
接下來,呼叫函式:
(mc.*pMemberFunction)(10.0,’a’,’b’); //例項化!
(mc.*pConstMemberFunction)(10.0,’a’,’b’);
我感覺,C++簡直在虐待函式指標啊。
下面是一個完整的例子:
#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語言的函式很類似,這裡僅僅給出一個例子和其執行結果:
程式程式碼為:
#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
函式指標作為引數
如果你已經明白了函式的引數機制,而且完全理解並實踐了上一節的內容,這一節其實是很簡單的。只需要在函式的引數列表中,宣告一個函式指標型別的引數即可,然後再呼叫的時候傳給它一個實參就可以了。你可以這麼想象,就是把函式指標的賦值語句的等號換成了形參和實參結合的模式就行。
下面給一個簡單的例子:
#include<stdio.h>
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;}
int pass_func_pointer(float (*pFunction)(float a,float b))
{
float result=pFunction(10.0,12.0);
printf("result=%f\n",result);
}
int main()
{
pass_func_pointer(add);
pass_func_pointer(minus);
pass_func_pointer(multiply);
pass_func_pointer(divide);
return 0;
}
輸出結果為:
result=22.000000
result=-2.000000
result=120.000000
result=0.833333
使用函式指標作為返回值
函式指標可以作為返回值。我們先類比的思考一下,如果說整型可以作為返回值,你會怎麼宣告函式?嗯,應該是下面這個樣子的:
int func(){}
整數對應的型別為int
。同樣再類比以下,如果說整型指標可以作為返回值,你會怎麼宣告?嗯,這個貌似難度也不大:
int * func(){}
好吧,現在說函式指標如果可以作為返回值,你該怎麼宣告?首先要保證的一點就是返回的函式指標的型別必須是能夠明顯的表達在這個函式的宣告或者定義形式中的,也就是說在這個形式中,要能夠包含函式指標所對應的能夠確定函式型別的資訊:這個函式型別的返回值型別,這個函式型別的引數個數,這個函式型別的引數型別。
現在我們在類比一次,如果要返回浮點型指標,那麼返回型別應該表達為:
float *
如果要函式指標對應的函式是返回值為浮點型,帶有兩個引數,兩個引數都是浮點型,那麼返回型別應該表達為下面的表達形式:
float (*)(float ,float )
嗯,沒辦法,函式的語義比較複雜,對應的表現就是形式的複雜性了。對於返回為浮點型指標的情況,定義的函式的名稱放在“float *”
的後面,而對於返回為上面型別的函式指標的話,定義的函式就要放在“(*)”
這個括號中的*
的後面了。
所以對於以下形式:
float (* func(char op) ) (float ,float)
其具體含義就是,聲明瞭這樣一個函式:
- 名稱為func,其引數的個數為1個;
- 其各個引數的型別為:op—char;
- 其返回變數(函式指標)型別為:
float(*)(float,float)
再次強調:函式指標時變數哦。
到了這裡之後,我們再來分析一下unix的系統呼叫函式signal的定義形式:
void (*signal(int signo,void (*func)(int)))(int);
其具體含義為就是,聲明瞭這樣一個函式:
- 其函式名稱為:
signal
- 其引數個數為:
2
- 其各個引數的型別為:
signo--int, func— void (*)(int)
- 其返回的變數(函式指標)的型別為:
void(*)(int)
上面這個函式比較經典,有一個引數型別為函式指標,返回值還是函式指標。
哦,我的天,如果你一步一步看到這裡了,就快大功告成啦。嘿嘿,接下來看一個例子:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
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;}
float(* FunctionMap(char op) )(float,float)
{
switch(op)
{
case '+':
return add;
break;
case '-':
return minus;
break;
case '*':
return multiply;
break;
case '\\':
return divide;
break;
default:
exit(1);
}
}
int main()
{
float a=10,b=5;
char ops[]={'+','-','*','\\'};
int len=strlen(ops);
int i=0;
float (*returned_function_pointer)(float,float);
for(i=0;i<len;i++)
{
returned_function_pointer=FunctionMap(ops[i]);
printf("the result caculated by the operator %c is %f\n",ops[i],returned_function_pointer(a,b));
}
return 0;
}
計算的結果為:
the result caculated by the operator + is 15.000000
the result caculated by the operator - is 5.000000
the result caculated by the operator * is 50.000000
the result caculated by the operator \ is 2.000000
使用函式指標陣列
函式指標有意思的地方在於,它使用從0到n-1這個n個連續的整數下標直接對映到函式上。
和前面一樣,我們也是類比著定義普通指標陣列來定義函式指標陣列。首先,考慮一個浮點數指標陣列,陣列的長度為10.我們都知道用下面的形式來定義:
float * pFloatArray[10];
從形式上分析,用中括號明確了是定義指標變數還是定義指標陣列這個語義。用數字10明確了這個陣列能容納多少個函式指標這個語義。形式上看,中括號是緊接在指標名稱的後面再中括號裡面是一個需要在編譯時期間就能夠確定的常數。
現在我們來類比函式指標陣列的定義,定義一個指向函式指標型別為:float (*)(float,float)
的函式指標陣列,陣列長度為10。正確的形式為:
float(* pFunctionArray[10])(float,float);
從形式上看,這種定義方式和定義普通指標的定義方式是一致的:都是在指標名稱後面緊接著一箇中括號,然後裡面是一個編譯期間能夠確定的常數。這種形式上的一致性,可以方便我們對形式的記憶,進而達到對內容的理解。
下面是一個例子程式:
#include<stdio.h>
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;}
int main()
{
float(*func_pointers[4])(float,float)={add,minus,multiply,divide};
int i=0;
float a=10.0,b=5.0;
for(i=0;i<4;i++)
{
printf("result is %f\n",func_pointers[i](a,b));
}
return 0;
}
以下為對應的執行結果:
result is 15.000000
result is 5.000000
result is 50.000000
result is 2.000000
使用typedef
從哲學角度講,形式過於複雜的話,還是抽象的層次太低。如果我們使用多層次的抽象,這樣最上層的表示就會簡化很多。這就是引入typedef
的原因,使用typedef
可以簡化函式指標的定義,因為typedef
可以定義新的型別:
同樣,在使用typedef
定義函式指標型別的時候,也和普通的使用typedef
引入新型別的方式不一樣。我們和前面一樣對照著普通的定義方式來學習:
typedef int bool;
這在C語言中很常用,由於C語言中沒有bool
型別,這樣定義之後可以從形式上引入一個bool
型別,提高程式碼可讀性。所以形式為:
typedef 已知型別 新型別;
現在我們要將float (*)(float,float)
型別宣告為一種新型別,按照上面的方式,貌似為:typedef float(*)(float,float) fpType;
然而,前面的經驗告訴我們應該這樣定義啊:
typedef float(*fpType)(float,float);
這樣我們就可以用fpType
來表示float (*)(float,float)
這種型別了。所以定義一個新的指向float (*)(float,float)
型別的指標變數的時候,我們就可以採用下面這種形式了:
fpType pFunction;
在定義函式指標陣列的時候可以這樣定義:
fpType pFunctions[10];
在定義函式指標型別引數時可以這樣定義:
void func(fpType pFunction);
在定義函式指標型別的返回值時可以這樣定義:
fpType func(int a);
現在我們再來看一下,unix
中的那個signal
函式,其形式為:
void (*signal(int signo,void (*func)(int)))(int);
現在我們定義一個型別為:
typedef void (*pSgnType)(int);
這樣上面的函式就能表達為:
pSgnType signal(int signo,pSgnType func);
這樣是不是看起來清爽多了。
其實上面的signal
函式也能這樣定義:
首先引入新型別:
typedef void SgnType(int);
然後signal
函式的宣告改為:
SgnType *signal(int signo,SgnType *func);
按照前面對這些形式的解釋,理解這個應該沒難度~~
現在在引入最後一個例子,關於使用typedef
來簡化函式指標定義的:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
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 (*pArithmeticOperations)(float,float);
typedef float ArithmeticOperations(float,float);
int main()
{
pArithmeticOperations pao=add;
pArithmeticOperations paos[4]={add,minus,multiply,divide};
ArithmeticOperations *ao=add;
ArithmeticOperations *aos[4]={add,minus,multiply,divide};
float a=10.0,b=5.0;
float result=0.0;
int i=0;
result=pao(a,b);
printf("the result of pao is %f\n",result);
printf("the results of paos are:\n");
for(i=0;i<4;i++)
{
result=paos[i](a,b);
printf("result=%f\n",result);
}
result=ao(a,b);
printf("\n\nthe result of ao is :%f\n",result);
printf("the results of aos are:\n");
for(i=0;i<4;i++)
{
result=aos[i](a,b);
printf("result=%f\n",result);
}
return 0;
}
輸出結果為:
result=15.000000
result=5.000000
result=50.000000
result=2.000000
the result of ao is :15.000000
the results of aos are:
result=15.000000
result=5.000000
result=50.000000
result=2.000000