深入學習C語言中的函數指針和左右法則
通常的函數調用
一個通常的函數調用的例子:
//自行包含頭文件 void MyFun(int x); //此處的申明也可寫成:void MyFun( int ); int main(int argc, char* argv[]) { MyFun(10); //這里是調用MyFun(10);函數 return 0; } void MyFun(int x) //這里定義一個MyFun函數 { printf(“%d\n”,x); }
這個MyFun函數是一個無返回值的函數,它并不完成什么事情。這種調用函數的格式你應該是很熟悉的吧!看主函數中調用MyFun函數的書寫格式:
MyFun(10);
我們一開始只是從功能上或者說從數學意義上理解MyFun這個函數,知道MyFun函數名代表的是一個功能(或是說一段代碼)。
直到——
學習到函數指針概念時。我才不得不在思考:函數名到底又是什么東西呢?
(不要以為這是沒有什么意義的事噢!呵呵,繼續(xù)往下看你就知道了。)
函數指針變量的申明
就象某一數據變量的內存地址可以存儲在相應的指針變量中一樣,函數的首地址也以存儲在某個函數指針變量里的。這樣,我就可以通過這個函數指針變量來調用所指向的函數了。
在C系列語言中,任何一個變量,總是要先申明,之后才能使用的。那么,函數指針變量也應該要先申明吧?那又是如何來申明呢?以上面的例子為例,我來申明一個可以指向MyFun函數的函數指針變量FunP。下面就是申明FunP變量的方法:
void (*FunP)(int) ; //也可寫成void (*FunP)(int x);
你看,整個函數指針變量的申明格式如同函數MyFun的申明處一樣,只不過——我們把MyFun改成(*FunP)而已,這樣就有了一個能指向MyFun函數的指針FunP了。(當然,這個FunP指針變量也可以指向所有其它具有相同參數及返回值的函數了。)
通過函數指針變量調用函數
有了FunP指針變量后,我們就可以對它賦值指向MyFun,然后通過FunP來調用MyFun函數了??次胰绾瓮ㄟ^FunP指針變量來調用MyFun函數的:
//自行包含頭文件 void MyFun(int x); //這個申明也可寫成:void MyFun( int ); void (*FunP)(int ); //也可申明成void(*FunP)(int x),但習慣上一般不這樣。 int main(int argc, char* argv[]) { MyFun(10); //這是直接調用MyFun函數 FunP=&MyFun; //將MyFun函數的地址賦給FunP變量 (*FunP)(20); //這是通過函數指針變量FunP來調用MyFun函數的。 } void MyFun(int x) //這里定義一個MyFun函數 { printf(“%d\n”,x); }
請看黑體字部分的代碼及注釋。
運行看看。嗯,不錯,程序運行得很好。
哦,我的感覺是:MyFun與FunP的類型關系類似于int 與int *的關系。函數MyFun好像是一個如int的變量(或常量),而FunP則像一個如int *一樣的指針變量。
int i,*pi; pi=&i; //與FunP=&MyFun比較。
(你的感覺呢?)
呵呵,其實不然——
調用函數的其它書寫格式
函數指針也可如下使用,來完成同樣的事情:
//自行包含頭文件 void MyFun(int x); void (*FunP)(int ); //申明一個用以指向同樣參數,返回值函數的指針變量。 int main(int argc, char* argv[]) { MyFun(10); //這里是調用MyFun(10);函數 FunP=MyFun; //將MyFun函數的地址賦給FunP變量 FunP(20); //這是通過函數指針變量來調用MyFun函數的。 return 0; } void MyFun(int x) //這里定義一個MyFun函數 { printf(“%d\n”,x); }
我改了黑體字部分(請自行與之前的代碼比較一下)。
運行試試,?。∫粯拥爻晒?。
咦?
FunP=MyFun;
可以這樣將MyFun值同賦值給FunP,難道MyFun與FunP是同一數據類型(即如同的int 與int的關系),而不是如同int 與int*的關系了?(有沒有一點點的糊涂了?)
看來與之前的代碼有點矛盾了,是吧!所以我說嘛!
請容許我暫不給你解釋,繼續(xù)看以下幾種情況(這些可都是可以正確運行的代碼喲?。?br />
代碼之三:
int main(int argc, char* argv[]) { MyFun(10); //這里是調用MyFun(10);函數 FunP=&MyFun; //將MyFun函數的地址賦給FunP變量 FunP(20); //這是通過函數指針變量來調用MyFun函數的。 return 0; }
代碼之四:
int main(int argc, char* argv[]) { MyFun(10); //這里是調用MyFun(10);函數 FunP=MyFun; //將MyFun函數的地址賦給FunP變量 (*FunP)(20); //這是通過函數指針變量來調用MyFun函數的。 return 0; }
真的是可以這樣的噢!
(哇!真是要暈倒了?。?br />
還有吶!看——
int main(int argc, char* argv[]) { (*MyFun)(10); //看,函數名MyFun也可以有這樣的調用格式 return 0; }
你也許第一次見到吧:函數名調用也可以是這樣寫的?。。ㄖ徊贿^我們平常沒有這樣書寫罷了。)
那么,這些又說明了什么呢?
呵呵!假使我是“福爾摩斯”,依據以往的知識和經驗來推理本篇的“新發(fā)現”,必定會由此分析并推斷出以下的結論:
1. 其實,MyFun的函數名與FunP函數指針都是一樣的,即都是函數指針。MyFun函數名是一個函數指針常量,而FunP是一個函數數指針變量,這是它們的關系。
2. 但函數名調用如果都得如(*MyFun)(10);這樣,那書寫與讀起來都是不方便和不習慣的。所以C語言的設計者們才會設計成又可允許MyFun(10);這種形式地調用(這樣方便多了并與數學中的函數形式一樣,不是嗎?)。
3. 為統(tǒng)一起見,FunP函數指針變量也可以FunP(10)的形式來調用。
4. 賦值時,即可FunP=&MyFun形式,也可FunP=MyFun。
上述代碼的寫法,隨便你愛怎么著!
請這樣理解吧!這可是有助于你對函數指針的應用嘍!
最后——
補充說明一點:在函數的申明處:
void MyFun(int ); //不能寫成void (*MyFun)(int )。 void (*FunP)(int ); //不能寫成void FunP(int )。
(請看注釋)這一點是要注意的。
定義某一函數的指針類型:
就像自定義數據類型一樣,我們也可以先定義一個函數指針類型,然后再用這個類型來申明函數指針變量。
我先給你一個自定義數據類型的例子。
typedef int* PINT; //為int* 類型定義了一個PINT的別名 int main() { int x; PINT px=&x; //與int * px=&x;是等價的。PINT類型其實就是int * 類型 *px=10; //px就是int*類型的變量 return 0; }
根據注釋,應該不難看懂吧?。m然你可能很少這樣定義使用,但以后學習Win32編程時會經常見到的。)
下面我們來看一下函數指針類型的定義及使用:(請與上對照?。?br />
//自行包含頭文件 void MyFun(int x); //此處的申明也可寫成:void MyFun( int ); typedef void (*FunType)(int ); //這樣只是定義一個函數指針類型 FunType FunP; //然后用FunType類型來申明全局FunP變量 int main(int argc, char* argv[]) { //FunType FunP; //函數指針變量當然也是可以是局部的 ,那就請在這里申明了。 MyFun(10); FunP=&MyFun; (*FunP)(20); return 0; } void MyFun(int x) { printf(“%d\n”,x); }
看黑體部分:
首先,在void (*FunType)(int ); 前加了一個typedef 。這樣只是定義一個名為FunType函數指針類型,而不是一個FunType變量。
然后,FunType FunP; 這句就如PINT px;一樣地申明一個FunP變量。
其它相同。整個程序完成了相同的事。
這樣做法的好處是:
有了FunType類型后,我們就可以同樣地、很方便地用FunType類型來申明多個同類型的函數指針變量了。如下:
FunType FunP2; FunType FunP3; //……
函數指針作為某個函數的參數
既然函數指針變量是一個變量,當然也可以作為某個函數的參數來使用的。所以,你還應知道函數指針是如何作為某個函數的參數來傳遞使用的。
給你一個實例:
要求:我要設計一個CallMyFun函數,這個函數可以通過參數中的函數指針值不同來分別調用MyFun1、MyFun2、MyFun3這三個函數(注:這三個函數的定義格式應相同)。
實現:代碼如下:
//自行包含頭文件 void MyFun1(int x); void MyFun2(int x); void MyFun3(int x); typedef void (*FunType)(int ); //②. 定義一個函數指針類型FunType,與①函數類型一至 void CallMyFun(FunType fp,int x); int main(int argc, char* argv[]) { CallMyFun(MyFun1,10); //⑤. 通過CallMyFun函數分別調用三個不同的函數 CallMyFun(MyFun2,20); CallMyFun(MyFun3,30); } void CallMyFun(FunType fp,int x) //③. 參數fp的類型是FunType。 { fp(x);//④. 通過fp的指針執(zhí)行傳遞進來的函數,注意fp所指的函數是有一個參數的 } void MyFun1(int x) // ①. 這是個有一個參數的函數,以下兩個函數也相同 { printf(“函數MyFun1中輸出:%d\n”,x); } void MyFun2(int x) { printf(“函數MyFun2中輸出:%d\n”,x); } void MyFun3(int x) { printf(“函數MyFun3中輸出:%d\n”,x); }
輸出結果:略
分析:(看我寫的注釋。你可按我注釋的①②③④⑤順序自行分析。)
以上部分為轉載網友所述。原文地址為:http://blog.pfan.cn/whyhappy/6030.html
地址跳轉
void(*reset)(void)= (void(*)(void))0。
void(*reset)(void)就是函數指針定義,(void(*)(void))0是強制類型轉換操作,將數值“0”強制轉換為函數指針地址“0”。
通過調用reset()函數,程序就會跳轉到程序執(zhí)行的“0”地址處重新執(zhí)行。在一些其他高級單片機Bootloader中,如NBoot、UBoot、EBoot,經常通過這些Bootloader進行下載程序,然后通過函數指針跳轉到要執(zhí)行程序的地址處。
1 void (*theUboot)(void);
。。。。
theUboot = (void (*)(void))(0x30700000);
theUboot();
。。。。。
2 (*(void (*)(void))(0x30700000))();
強制類型轉換,將一個絕對地址轉換為一個函數指針,并調用這個函數以跳轉到前面提到的絕對地址.
翻譯成匯編就是:
mov r0,0x30700000;
mov pc,r0
對于(*(void (*)(void))(0x30700000))();
可以這樣理解
首先(void( * )(void) )是一個強制類型轉換符,他將后面的0x30700000這個無符號整數強制轉化為一個函數指針,該函數指針所指向的函數入口參數為 void,返回值也是void 。 如果到這步你看懂了,那么設(void (*)(void))(0x30700000)為 fp; 那么上面的表達式就可以簡化為 (*fp)(); OK,這下就清楚了吧,我們將上面轉化好的函數指針進行引用(也就是調用函數指針指向的函數)。
右左法則
c語言有復雜的指針聲明,都是由各種聲明嵌套構成的。各大公司的筆試題里經常會出現理解復雜指針聲明,右左法則是一個著名又常用的方法。
The right-left rule : start reading the declaration from the innermost parentheses, go rigtht, and then go left. when you encounter parentheses, the direction should be reversed. Once everything in the parentheses has been parsed, jump out of it. Continue till the whole declaration has been parsed.
右左規(guī)則:首先從最里面的括號看起,然后往右看,再往左看。每當遇到圓括號時,就應該掉轉閱讀方向。一旦解析完圓括號里面所有的東西,就跳出圓括號。重復這個過程直到整個聲明解析完畢。
原文作者對這里進行了修正:應該是從未定義的標識符開始閱讀,而不是從括號讀起,之所以是未定義的標識符,是因為一個聲明里可能有多個標識符,但未定義的標識符只會有一個
示例
int (*func) (int *p);
首先找到未定義標識符,就是func,它的外面有一對圓括號,而且左邊是一個*,這說明func是一個指針。然后跳出括號,看右邊,也是一個括號,這說明(*func)是一個函數,而func是一個指向這類函數的指針,也就是一個函數指針。這類函數具有int*類型的參數,返回值類型是int
int (*func)(int *p, int (*f)(int *));
func被一對括號包含,且左邊有一個*號,說明func是一個指針,然后跳出這個圓括號,先看右邊,也是一個圓括號,說明func是一個函數指針。這類函數具有int *和 int (*)(int *)這樣的形參,返回值是int。對于int (*f)(int *)的形參,分析方法跟func是一致的
int (*func[5])(int *p);
func右邊是一個[]運算符,說明func是一個具有5個元素的數組,func的左邊有一個*,說明func的元素是指針,要注意這里的*不是修飾func的,而是修飾func[5]的,原因是[]運算符的優(yōu)先級比*高,func先跟[]結合,因此*修飾的是func[5].跳出這個括號,看右邊,也是一對圓括號,說明func數組的元素是函數類型的指針,它指向的函數具有int*類型的形參,返回值類型是int
int (*(*func)[5])(int *p);
func被一對圓括號包圍,左邊又有一個*,那么func是一個指針,跳出括號,右邊是一個[]運算符號,說明func是一個指向數組的指針,現在往左看,左邊有一個*號,說明這個數組的元素是指針,再跳出括號,向右看,右邊又有一個括號,說明這個數組的元素是指向函數的指針??偨Y一下就是:func是一個指向數組的指針,這個數組的元素是函數指針,這些指針具有int *形參,返回值為int類型的函數
int (*(*func)(int *p))[5];
func是一個函數指針,這類函數具有int *類型的形參,返回值是指向數組的指針,所指向的是具有5個int類型元素的數組。
相關文章
C/C++最短路徑算法之迪杰斯特拉Dijkstra的實現詳解
Dijkstra(迪杰斯特拉)算法是典型的單源最短路徑算法,用于計算一個節(jié)點到其他所有節(jié)點的最短路徑。本文將詳解該算法的圖解與實現,需要的可以參考一下2022-07-07