天天看點

嗯,讓我們徹底搞懂C/C++函數指針吧(一) 1.     無處不見的函數指針 2 函數指針簡單介紹 3 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~~@~@            )- - &&\ \

    C   >/                \<   |&/

     \_O/ - 哇塞          _`*-'_/ /

   ,- >o<-.              / ____ _/

  /   \/   \            / /\  _)_)

 / /|  | |\ \          / /  )   |

 \ \|  | |/ /          \ \ /    |

  \_\  | |_/            \ \_    |

  /_/`___|_\            /_/\____|

    |  | |                  \  \|

    |  | |                   `. )

    |  | |                   / /

    |__|_|_                 /_/|

    (____)_)                |\_\_

而現在我們隻需要用語言來抽象就行,即用兩個漢字“美女”或者英文“beauty”就行了。這就是形式上的簡化,也就友善了我們的交流。另外一種就是内容上的複雜度過高,一個高度抽象的表達式後面蘊含着巨大的複雜度對于我們了解問題也是很難的,例如:

P=NP?

由于接觸過的書上所講的關于函數指針方面的都是蜻蜓點水一樣,讓我很不滿足。我認為C/C++語言函數指針難學的主要原因是由于其形式上的定義過于複雜,但是在内容上我們一定要搞清楚函數的本質。函數的本質就是表達式的抽象,它在記憶體中對應的資料結構為堆棧幀,它表示一段連續指令序列,這段連續指令序列在記憶體中有一個确定的起始位址,它執行時一般需要傳入參數,執行結束後會傳回一個參數。和函數相關的,應該大緻就是這些内容吧。

函數指針是一個指向函數的指針(呃,貌似是廢話),函數指針表示一個函數的入口位址。使用函數指針的好處就是在處理“在運作時根據資料的具體狀态來選擇相應的處理方式”這種需求時更加靈活。

下面是一個簡單的使用函數指針取代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); 

             case '*': 

                    result=multiply(a,b); 

             case '/': 

                    result=divide(a,b); 

      } 

void switch_fp_impl(float a,float b,pf p) 

      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; 

      struct timeval start_point,end_point; 

      gettimeofday(&start_point,NULL); 

      for(i=0;i<N;i++) 

             switch_impl(12.32,54.14,'-'); 

      gettimeofday(&end_point,NULL); 

      printf("check point 1:%d\n",conversion(end_point)-conversion(start_point)); 

             switch_fp_impl(12.32,54.14,minus); 

      printf("check point 2:%d\n",conversion(end_point)-conversion(start_point)); 

      return 0; 

下面是執行結果:

[lichao@sg01 replaceswitch]$ ./replaceswitch 

check point 1:22588 

check point 2:19407 

check point 1:22656 

check point 2:19399 

check point 1:22559 

check point 2:19380 

check point 1:22181 

check point 2:19667 

check point 1:22226 

check point 2:19813 

check point 1:22141 

check point 2:19893 

check point 1:21640 

check point 2:19745 

從上面可以看出,使用函數指針:(一)在某種程度上簡化程式的設計(二)可以提高效率。在這個例子中,使用函數指針可以提高10%的效率。

注意:以上代碼在unix環境下實作的,如果要在windows下運作,可以稍微改下,把“#define UNIXENV”行删掉即可

從文法上講,有兩種不相容的函數指針形式:

(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++中,對于指派,你必須要加“&”,而且你還必須再次之前已經定義好了一個類執行個體,取位址符号要操作于這個類執行個體的對應的函數成員上。在使用成員函數的指針調用成員函數時,你必須要加類執行個體的名稱,然後再使用.*或者->*來使用成員函數指針。舉例如下:

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++簡直在虐待函數指針啊。

下面是一個完整的例子:

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; 

      result=pFunction(f,a,b); 

      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); 

      pConstMemberFunction=&MyClass::func2; 

      MyClass mc2(2); 

      printf("pointer pConstMemberFunction's address is:%x\n",pConstMemberFunction); 

      result=(mc2.*pConstMemberFunction)(f,a,b); 

運作結果為:

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 

#include<iostream> 

      static float plus(float a,float b) 

             return a+b; 

      }     

      float result,a=10.0,b=10.0; 

      float (*p)(float,float); 

      p=&MyClass::plus; 

      printf("result=%f\n",result); 

執行結果為:

result=20.000000 

本文轉自hipercomer 51CTO部落格,原文連結:http://blog.51cto.com/hipercomer/792300

繼續閱讀